// ==UserScript== // @name Stack Exchange Archivist // @namespace https://github.com/Glorfindel83/ // @description Adds a button to archive all external links and images in a post // @author Glorfindel // @updateURL https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/archivist/archivist.user.js // @downloadURL https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/archivist/archivist.user.js // @version 0.2 // @match *://*.stackexchange.com/questions/* // @match *://*.stackoverflow.com/questions/* // @match *://*.superuser.com/questions/* // @match *://*.serverfault.com/questions/* // @match *://*.askubuntu.com/questions/* // @match *://*.stackapps.com/questions/* // @match *://*.mathoverflow.net/questions/* // @exclude *://*.stackexchange.com/questions/ask // @exclude *://*.stackoverflow.com/questions/ask // @exclude *://*.superuser.com/questions/ask // @exclude *://*.serverfault.com/questions/ask // @exclude *://*.askubuntu.com/questions/ask // @exclude *://*.stackapps.com/questions/ask // @exclude *://*.mathoverflow.net/questions/ask // @connect web.archive.org // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // ==/UserScript== (function () { "use strict"; $("a.short-link").each(function() { let shareButton = $(this); // Find links & images let post = shareButton.parents("div.question")[0]; if (post == null) { post = shareButton.parents("div.answer")[0]; } let body = $(post).find("div.post-text")[0]; var images = new Set(); $(body).find("img").each(function() { let tmp = document.createElement('a'); tmp.href = this.src; if (shouldArchive(tmp.hostname)) { images.add(this.src); } }); var links = new Set(); $(body).find("a").each(function() { if (shouldArchive(this.hostname) && !images.has(this.href)) { links.add(this.href); } }); // Are there any links to archive? let disabled = links.size == 0 && images.size == 0; let hoverMessage = disabled ? 'No external links or images found.' : 'Archive ' + getMessage(links, images, false); // Add button let menu = shareButton.parent(); menu.append($('|')); let button = $('archive'); menu.append(button); function startArchiving(event) { event.preventDefault(); if (disabled) { alert(hoverMessage); return; } // TODO: overview of age of last snapshots, with checkmarks let message = getMessage(links, images, true); if (!confirm('Are you sure you want to archive ' + message + ' in this post?')) return; // Disable further clicks - the button becomes a progress indicator button.off('click', startArchiving); button.on('click', function(e) { e.preventDefault(); }); button.css("color", "#BBB"); button.removeAttr("title"); button.text("archiving ..."); let linksToArchive = Array.from(links).concat(Array.from(images)); var index = -1, successCount = 0, failureCount = 0; function next(success) { // Success? if (typeof success != 'undefined') { if (success) { successCount++; } else { failureCount++; } } // Archive next link if (++index < linksToArchive.length) { archive(); } // Update archive button let totalCount = successCount + failureCount; if (totalCount < linksToArchive.length) { button.text("archiving ... (" + totalCount + "/" + linksToArchive.length + ")"); } else if (failureCount == 0) { button.text("archiving finished!"); } else { button.text("archiving finished! (" + failureCount + " link" + (failureCount > 1 ? "s" : "") + " failed)"); } } function archive() { let link = linksToArchive[index]; console.log("Archiving: " + link); // Call Wayback Machine let archiveLink = "https://web.archive.org/save/" + link; GM.xmlHttpRequest({ method: 'GET', url: archiveLink, onload: function (response) { console.log("Saved: " + link); next(true); }, onerror: function(response) { console.log(["An error occurred while calling: " + archiveLink, response.status, response.statusText, response.readyState, response.responseHeaders, response.responseText, response.finalUrl].join("\n")); next(false); } }); } // Parallelize archiving over multiple threads for (var i = 0; i < 5; i++) { next(); } } button.on('click', startArchiving); }); })(); function getMessage(links, images, use1) { let linksMessage = getMessagePart("link", links.size, use1); let imagesMessage = getMessagePart("image", images.size, use1); return linksMessage + (linksMessage != "" && imagesMessage != "" ? " and " : "") + imagesMessage; } function getMessagePart(type, count, use1) { if (count == 0) return ""; return (count == 1 && use1 ? "the" : count) + " external " + type + (count == 1 ? "" : "s"); } function shouldArchive(hostname) { // Only archive 'external' links return !hostname.contains("stackexchange.com") && !hostname.contains("stackoverflow.com") && !hostname.contains("superuser.com") && !hostname.contains("serverfault.com") && !hostname.contains("askubuntu.com") && !hostname.contains("mathoverflow.net") && !hostname.contains("stackapps.com") && !hostname.contains("stack.imgur.com"); }