PolitoDownloader

Download all your Polito material in one click

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         PolitoDownloader
// @namespace    https://github.com/giuseppe-dandrea
// @version      0.15
// @description  Download all your Polito material in one click
// @author       giuseppe-dandrea
// @supportURL   https://github.com/giuseppe-dandrea/PolitoDownloader/issues
// @match        https://didattica.polito.it/pls/portal30/sviluppo.pagina_corso.main*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js
// ==/UserScript==

(function() {
	"use strict";

	const URL = "https://didattica.polito.it/pls/portal30/sviluppo.filemgr.handler";

	// List the content of a directory
	// callback(pathList, parentPath, parentZipFolder, downloadAll)
	// pathList contains the objects of the files and dirs
	function listPath(path, code, callback, parentZipFolder, downloadAll) {
		N_FILE++;
		if (path === "/") {
			code = "";
		}
		let params = "?action=list&path=" + encodeURIComponent(path) + "&code=" + code;
		let xhttp = new XMLHttpRequest();

		activeDownloadButton.innerText = "Retrieving files...";

		xhttp.open("POST", URL + params);
		xhttp.send();
		xhttp.onreadystatechange = function() {
			if (xhttp.readyState == 4 && xhttp.status == 200) {
				let pathList = JSON.parse(xhttp.responseText);
				pathList = pathList.result.filter(o => !o.name.includes("ZZZZZZZZZZZZZZZZZZZZLezioni on-line"));
				N_FILE--;
				if (pathList.length === 0) {
					return;
				}
				if (callback) callback(pathList, path, parentZipFolder, downloadAll);
			}
		}
	}

	// callback for listPath:
	// for every object in path list:
	//   if the object is a dir: create the dir in the zip and call listFiles in that dir
	//   else download and add the file to the zip
	function listPathHandler(pathList, parentPath, parentFolder, downloadAll) {
		pathList.forEach(o => {
			if (o.type == "dir") {
				// console.log("Created dir " + o.name);
				let newFolder = parentFolder.folder(o.name);
				listPath(parentPath + o.name + "/", o.code, listPathHandler, newFolder, downloadAll);
			} else if (o.type == "file" && (downloadAll || (DOWNLOADED_FILES[o.code] ? o.date > DOWNLOADED_FILES[o.code] : true))) {
				N_FILE++;
				// console.log("Added " + o.name);
				DOWNLOADED_FILES[o.code] = o.date;
				let params = "?action=download&path=" + encodeURIComponent(parentPath + o.name) + "&code=" + o.code;
				let xhttp = new XMLHttpRequest();
				xhttp.open("POST", URL + params);
				xhttp.responseType = "blob";
				xhttp.send();
				xhttp.onreadystatechange = function() {
					if (xhttp.readyState == 4 && xhttp.status == 200) {
						parentFolder.file(o.name, xhttp.response, { binary: true });
						// console.log("1 file added!");
						N_DOWNLOADED++;
						updateProgressBar(( (N_DOWNLOADED + N_PREVIOUS_DOWNLOADED) / TOTAL_FILES ) * 100);
						N_FILE--;
					}
				}
			} else {
				N_PREVIOUS_DOWNLOADED++;
				updateProgressBar(( (N_DOWNLOADED + N_PREVIOUS_DOWNLOADED) / TOTAL_FILES ) * 100);

			}

		});
	}

	function saveFile(blob, name) {
		let a = document.createElement("a");
		document.body.appendChild(a);
		a.style = "display: none";
		let url = window.URL.createObjectURL(blob);
		a.href = url;
		a.download = name;
		a.click();
		window.URL.revokeObjectURL(url);
		a.parentNode.removeChild(a);
	}

	function downloadZip(zip, name) {
		// console.log("Inizio a comprimere!");
		zip.generateAsync({ type:"blob" }).then(function(content) {
			saveFile(content, name);
			activeDownloadButton.innerText = activeButtonText;
		});
	}

	function onCompleted(callback) {
		setTimeout(function() {
			// console.log(N_FILE);
			if (N_FILE === 0) {
				activeDownloadButton.innerText = "Downloading...";
				callback();
			} else {
				onCompleted(callback);
			}
		}, 1000);
	}

	function initGlobals(button) {
		zip = new JSZip();
		N_FILE = 0;
		N_DOWNLOADED = 0;
		N_PREVIOUS_DOWNLOADED = 0;
		DOWNLOADED_FILES = GM_getValue("downloadedFiles", {});
		activeDownloadButton = button;
		activeButtonText = button.innerText;
	}

	function onButtonClick(button, downloadAll, failText) {
		initGlobals(button);
		progressBar.parentNode.style.display = "block"
		if (TOTAL_FILES == 0) {
			activeDownloadButton.innerText = "No files!";
			return
		}
		listPath("/", 0, listPathHandler, zip, downloadAll);
		// console.log("File download completed\nStarting onCompleted");
		onCompleted(function() {
			GM_setValue("downloadedFiles", DOWNLOADED_FILES);
			if (N_DOWNLOADED > 0) {
				downloadZip(zip, title);
			} else {
				activeDownloadButton.innerText = failText;
			}
			badge.style.display = "none";
			progressBar.parentNode.style.display = "none"
			updateProgressBar(0);
			GMlastUpdate[code] = lastUpdate;
			GM_setValue("lastUpdate", GMlastUpdate);
		});
	}

	function updateProgressBar(percent) {
		percent = Math.round(percent);
		progressBar.style.width = percent + "%";
		progressBar.setAttribute("aria-valuenow", percent);
	}

	// download all
	let downloadAllButton = document.createElement("button");
	downloadAllButton.innerText = "Download All Files";
	downloadAllButton.setAttribute("id", "downloadAllButton");
	downloadAllButton.setAttribute("class", "btn btn-primary");

	// download new
	let downloadNewButton = document.createElement("button");
	downloadNewButton.innerText = "Download New Files";
	downloadNewButton.setAttribute("id", "downloadNewButton");
	downloadNewButton.setAttribute("class", "btn btn-primary");
	downloadNewButton.style["margin-left"] = "5px";

	// new badge
	let badge = document.createElement("div");
	badge.style.cssText = `
		background: red;
		width: 10px;
		height: 10px;
		border-radius: 50%;
		position: relative;
		z-index: 1000;
		top: -30px;
		left: 142px;
		margin-bottom: -10px;
		display: none;`
	downloadNewButton.append(badge);

	// progress bar
	let progressBarStr = 	'<div class="progress">\
							<div class="progress-bar" id="progressbar" role="progressbar"\
							  aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:0%">\
							</div>\
						</div>'
	let progressBar = document.createElement("div");
	progressBar.innerHTML = progressBarStr;
	progressBar = progressBar.firstChild.children[0];
	progressBar.parentNode.style.display = "none";
	progressBar.parentNode.style.margin = "10px";

	// page title
	let title = document.querySelector("body > div:nth-child(9) > div > div > h2 > strong").innerText;
	let code = title.match(/\w+/)[0];
	let TOTAL_FILES = 0;

	let lastUpdate = 0;
	let GMlastUpdate = GM_getValue("lastUpdate", {});

	// Code of the root directory, used to obtain TOTAL_FILES and lastUpdate
	let rootCode = document.documentElement.innerHTML.match(/rootCode: "(\d+)/)[1];
	if (rootCode) {
		GM_xmlhttpRequest({
			method: "POST",
			url:    "https://didattica.polito.it/pls/portal30/sviluppo.filemgr.get_process_amount?items=" + rootCode,
			onload: function(resp) {
				let result = JSON.parse(resp.response).result;
				TOTAL_FILES = result.files;
				lastUpdate = Date.parse(result.lastUpload);
				if (!GMlastUpdate[code] || GMlastUpdate[code] < lastUpdate) {
					badge.style.display = "block";
				}
			}
		});
	}

	// center tag
	let centerTag = document.createElement("center");
	centerTag.appendChild(downloadAllButton);
	centerTag.appendChild(downloadNewButton);
	document.querySelector("#portlet_corso_container > div > div > div.row.text-left > div > div:nth-child(2)").prepend(centerTag);
	centerTag.parentNode.insertBefore(progressBar.parentNode, centerTag.nextSibling)

	// global vars
	let zip;
	let N_FILE;
	let N_DOWNLOADED;
	let N_PREVIOUS_DOWNLOADED;
	let DOWNLOADED_FILES;
	let activeDownloadButton;
	let activeButtonText;

	// download all listener
	document.getElementById("downloadAllButton").onclick = function() {
		onButtonClick(downloadAllButton, true, "No files!");
	}

	// download new listener
	document.getElementById("downloadNewButton").onclick = function() {
		onButtonClick(downloadNewButton, false, "No new files!");
	}
})();