Last active
April 20, 2025 15:32
-
-
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!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Schoology quick-download files helper | |
// @namespace http://nicholastay.github.io/ | |
// @version 0.3.0 | |
// @author Nicholas Tay <[email protected]> | |
// @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/[email protected]/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>`); | |
}); | |
} | |
})(); |
@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.
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello, I installed ViolentMonkey on Chrome, but it seems not working for me. I saw there is another package requirement [email protected]. 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!