Skip to content

Instantly share code, notes, and snippets.

@nicholastay
Last active August 21, 2026 12:20
Show Gist options
  • Select an option

  • Save nicholastay/e6c108cc49bc297f50a1c417624e6601 to your computer and use it in GitHub Desktop.

Select an option

Save nicholastay/e6c108cc49bc297f50a1c417624e6601 to your computer and use it in GitHub Desktop.
Schoology userscript - Quickly download files off folder browsers (press raw to install) - Be sure to allow popups!
// ==UserScript==
// @name Schoology quick-download files helper
// @namespace http://nicholastay.github.io/
// @version 0.3.0
// @author Nicholas Tay <nexerq@gmail.com>
// @license MIT
// @icon https://i.imgur.com/QmmYqzZ.png
// @match *://app.schoology.com/*
// @match *://schoology.cgs.vic.edu.au/*
// @grant none
// @homepage https://gist.github.com/nicholastay/e6c108cc49bc297f50a1c417624e6601
// @downloadURL https://gist.github.com/nicholastay/e6c108cc49bc297f50a1c417624e6601/raw/schoology-quickdl.user.js
// @noframes
// @require https://unpkg.com/bottleneck@2.13.0/es5.js
// ==/UserScript==
;(function() {
// lets try not to kill schoology: 10 per 5s
var limiter = new Bottleneck({
reservoir: 10,
reservoirRefreshAmount: 100,
reservoirRefreshInterval: 5 * 1000
});
injectCSS();
if (injectLabels())
injectDownloadAll();
injectLabelEventHandler();
injectSubfolderHandler();
injectGroupResources();
console.log("[nexerq/quickDL] Initialized.");
function noop() {}
function injectCSS() {
$("head").append("<style type=\"text/css\">.nexerq-quickdl-style:hover { text-decoration: underline; cursor: pointer; }</style>");
console.log("[nexerq/quickDL] CSS injected");
}
function injectLabels(subfolder) {
var $contentFiles = $("#folder-contents-table");
if (!$contentFiles || ($contentFiles.length < 1))
return console.log("[nexerq/quickDL] Not a folder listing page, not injecting that.");
console.log("[nexerq/quickDL] Found folder listings.");
var $documents = $contentFiles.find(".type-document:not(.nexerq-quickdl-processed)");
if (!$documents || ($documents.length < 1)) {
console.log("[nexerq/quickDL] No download documents found.");
return false;
}
console.log("[nexerq/quickDL] Found " + $documents.length + " documents to process");
$documents.each(function() {
var $this = $(this);
var $attachment = $this.find(".attachments-file-name");
var $link = $attachment.find("a");
// mark as processed
$this.addClass("nexerq-quickdl-processed");
// create dl btn
var $newBtn = $attachment.find(".attachments-file-size").clone().appendTo($attachment);
$newBtn.addClass("nexerq-quickdl-style");
$newBtn.addClass("nexerq-quickdl-link");
$newBtn.text("Download");
$newBtn.attr("orig-link", $link.attr("href"));
// weird subfolder bullshit
var $subContent = $link.find(".infotip-content");
$newBtn.attr("orig-name", ($subContent.length > 0 ? $subContent.text() : $link.text()));
if (subfolder) // detection for quickdl all - do not include these!
$newBtn.addClass("nexerq-quickdl-subfolder");
});
console.log("[nexerq/quickDL] Labels injected");
return true;
}
function injectLabelEventHandler() {
$(document).on("click", ".nexerq-quickdl-link", function() {
var $this = $(this);
var origName = $this.attr("orig-name");
var origLink = $this.attr("orig-link");
// kinda hacky: ajax http get the other page
var req = function() {
console.log("[nexerq/quickDL] Attempting to ajax get the target page (" + origName + ").");
limiter.submit($.ajax, origLink, {
success: function(data) {
var $dl = $(data).find(".attachments-file a:first");
if (!$dl || ($dl.length < 1)) {
console.log("[nexerq/quickDL] File could not be found in success: " + origName);
return alert("[nexerq/quickDL] Sorry, this file (" + origName + ") cannot be quick-downloaded at this time. You may have to just open the page and download manually.");
}
// dl
invokeDownload(origName, $dl.attr("href"));
},
error: function(jqXHR, textStatus, err) {
if (jqXHR.status === 429) {// 429 rate limited
console.log("[nexerq/quickDL] Rate limited 429 on " + origName + ". Retry in 10s");
setTimeout(req, 30000);
}
}
}, noop); // noop cb
};
req();
});
console.log("[nexerq/quickDL] Event handler registered");
}
function invokeDownload(fileName, url) {
console.log("[nexerq/quickDL] Downloading " + fileName + ".");
// ugh - https://stackoverflow.com/questions/18451856/how-can-i-let-a-user-download-multiple-files-when-a-button-is-clicked
//var tmpLink = document.createElement("a");
//tmpLink.style.display = "none";
//document.body.appendChild(tmpLink);
//tmpLink.setAttribute("href", url);
//tmpLink.setAttribute("download", fileName);
//tmpLink.click();
//document.body.removeChild(tmpLink);
window.open(url, fileName + " - nexerq/quickDL");
}
function injectDownloadAll() {
$(".materials-top").append(`<span class="attachments-file-size gray nexerq-quickdl-all nexerq-quickdl-style">Quick-download all (non-recursive)</span>`);
$(".nexerq-quickdl-all").click(function() {
console.log("[nexerq/quickDL] Downloading all available files...");
// oh dear... recursion
var $links = $(".nexerq-quickdl-link:not(.nexerq-quickdl-subfolder)");
$links.each(function() {
$(this).click();
});
});
console.log("[nexerq/quickDL] Download all injected");
}
function injectSubfolderHandler() {
// attach to document
$(document).on("click", ".folder-contents-cell .folder-expander", function() {
var $this = $(this);
console.log("[nexerq/quickDL] Subfolder open detected");
// wait for ajax to complete...
var waitCount = 0;
var $parentTd = $this.closest("td");
var wait = setInterval(function() {
var $subtree = $parentTd.children(".folder-subtree");
if ($subtree && $subtree.length > 0) {
clearInterval(wait);
if (injectLabels(true)) {
// inject dl all
$parentTd.find(".folder-title").first().append(`<span class="attachments-file-size gray nexerq-quickdl-all-subfolder nexerq-quickdl-style">Quick-download all</span>`);
$parentTd.find(".nexerq-quickdl-all-subfolder").click(function() {
$parentTd.find(".nexerq-quickdl-link").each(function() {
$(this).click();
});
});
console.log("[nexerq/quickDL] Injected subfolder event and dl all");
}
}
if (++waitCount > 5) {
console.log("[nexerq/quickDL] Subfolder open - something went wrong while waiting.");
clearInterval(wait);
}
}, 750);
});
console.log("[nexerq/quickDL] Event handler for subfolder handling injected");
}
// probably the worst implemented one
function injectGroupResources() {
if (!window.location.pathname.includes("/group") || !window.location.pathname.includes("/materials"))
return console.log("[nexerq/quickDL] Not a group resource page, not injecting that.");
// wait. AJAX PLS
var i = 0;
console.log("[nexerq/quickDL] Waiting for group page...");
var wait = setInterval(function() {
var $groupResources = $("#collection-view");
if (!$groupResources || ($groupResources.length < 1)) {
if (++i > 5) {
console.log("[nexerq/quickDL] Failed to find resources.");
clearInterval(wait);
}
return;
}
clearInterval(wait);
_injectGroupResources();
}, 750);
}
function _injectGroupResources() {
var $groupResources = $("#collection-view");
// event handler
$(document).on("click", ".nexerq-quickdl-group-link", function() {
var $resource = $(this).closest(".template-s-content-generic-post-docviewer");
var resId = $resource.attr("id").replace("t-", "");
var origName = $resource.find(".item-title").text();
// ajax get template, then the ID docviewer and parse its config
// ... ughhhhhhhhhh
console.log("[nexerq/quickDL] Trying group resource: " + origName);
limiter.submit($.get, window.location.origin + "/template/" + resId, function(templateData) {
console.log("[nexerq/quickDL] Got template data: " + origName);
// time to docviewer
limiter.submit($.get, window.location.origin + $(templateData).find(".docviewer-iframe").attr("src"), function(docData) {
var $configFunc = $(docData).find("script:contains('pdfPath')");
if (!$configFunc || ($configFunc.length < 1))
return console.log("[nexerq/quickDL] Could not find pdfPath config: " + origName);
var _docConfig = $configFunc.text().match(/PdfTronWebViewer, ({.*"}"}})/);
if (!_docConfig)
return console.log("[nexerq/quickDL] Could not parse pdfConfig: " + origName);
var docConfig = JSON.parse(_docConfig[1]);
var customConfig = JSON.parse(docConfig.options.custom);
invokeDownload(origName, customConfig.downloadLink);
});
});
});
var $resources = $groupResources.find(".template-s-content-generic-post-docviewer:not(.nexerq-quickdl-processed)");
if (!$resources || ($resources.length < 1))
return console.log("[nexerq/quickDL] No download-docviewers found.");
console.log("[nexerq/quickDL] Found " + $resources.length + " group resources to process");
$resources.each(function() {
var $resource = $(this);
$resource.addClass("nexerq-quickdl-processed");
$resource.find(".resource-details").append(`<span class="nexerq-quickdl-style nexerq-quickdl-group-link"> · Download</span>`);
});
}
})();
@jamestclin

Copy link
Copy Markdown

Hello, I installed ViolentMonkey on Chrome, but it seems not working for me. I saw there is another package requirement bottleneck@2.13.0. I'm not familiar with these .js scripts. would you mind sharing how to set up the whole flow to crawl materials in the Schoology? Thanks a lot!

@soarn

soarn commented Mar 1, 2021

Copy link
Copy Markdown

@k2973363 if you still need to use the script, add this line in the ==UserScript== section:

// @match        *://*.schoology.com/*

This script was not originally made to run on all schoology.com domains, but that line will fix it.

@david122s

Copy link
Copy Markdown

This userscript is a great utility for anyone managing multiple resources on Schoology. Automating downloads from folders or group resources can seriously save time — especially for students in large districts like LAUSD, where Schoology is a primary platform. For those accessing it through LAUSD, schoologylausd.com serves as a central point for login and support, so tools like this script pair well with that ecosystem.

Thanks for putting this together, Nicholas — really thoughtful approach to enhancing the user experience.

@granmadave

Copy link
Copy Markdown

noob here. How do you use this? I installed ViolentMonkey for Edge. I can edit the script so it includes my district's site. When I'm on the site, though, and I select the extension and script, nothing happens. Please advise.

@nicholastay

Copy link
Copy Markdown
Author

@granmadave Unfortunately (or fortunately?) I have not been a student for 8+ years! So I no longer have access to any Schoology platform nor do I have any interest in doing so.

Do feel free to fork and point others towards it here :) I’m glad people have found utility in it all these years later. Honestly very surprised it even still worked at all and that there was interest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment