Imgur Archivist

Checks whether dead Imgur links and images are archived and replaces them.

  1. // ==UserScript==
  2. // @name Imgur Archivist
  3. // @namespace https://reddit.com/u/VladVV/
  4. // @version 0.12
  5. // @description Checks whether dead Imgur links and images are archived and replaces them.
  6. // @author /u/VladVV
  7. // @match *://*/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=archive.org
  9. // @grant none
  10. // @license EUPL v1.2
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. // Configuration options for the script:
  17. const scriptConfig = {
  18. DOMElements: {
  19. // Any DOM element can be added in the format 'tagName' : 'attribute'
  20. // The attribute will be treated as a URL and replaced with archived URLs
  21. 'A' : 'href',
  22. 'IMG' : 'src'
  23. },
  24. archiveIcon: '', //TODO: implement little icon to display inside archived elements
  25. linkTooltips: {
  26. // Tooltips when hovering over links. Set to a blank string ("") for nothing.
  27. available: "Archived image available. (Imgur link is dead)",
  28. unavailable: "No archived image available. (Imgur link is dead)"
  29. },
  30. changeLinkStyleOnHover: true, // Archived links will become green/red; set to false to disable
  31. };
  32.  
  33. // A dictionary to store the dead Imgur URLs and their archived versions
  34. var imgurArchive = {};
  35.  
  36. // A function to check if an Imgur URL is dead or alive
  37. function checkImgurURL(url) {
  38. // Force HTTPS to prevent cross-domain request issues
  39. url = url.replace('http://','https://');
  40. if (url in imgurArchive) return; //Abort if url has already been checked
  41. var xhr = new XMLHttpRequest();
  42. xhr.open('HEAD', url);
  43. xhr.onreadystatechange = function () {
  44. if (this.readyState === this.DONE) {
  45. if (this.responseURL.indexOf('removed.png') !== -1 || this.status === 404) {
  46. // Imgur image is removed or deleted
  47. // Setup a GET request to the archive.org API
  48. var archiveUrl = 'https://archive.org/wayback/available?url=' + url;
  49. var archiveXhr = new XMLHttpRequest();
  50. archiveXhr.open('GET', archiveUrl);
  51. archiveXhr.onreadystatechange = function () {
  52. if (this.readyState === this.DONE) {
  53. var response = JSON.parse(this.responseText);
  54. if (response.archived_snapshots.closest) {
  55. // Archived image found
  56. // Add the dead link and the archived link to the dictionary
  57. imgurArchive[url] = response.archived_snapshots.closest.url;
  58. } else {
  59. // The removed image is not archived :(
  60. imgurArchive[url] = false;
  61. }
  62. }
  63. };
  64. archiveXhr.send();
  65. } else {
  66. // Imgur image is live; our services aren't needed.
  67. imgurArchive[url] = true;
  68. }
  69. }
  70. };
  71. xhr.send();
  72. }
  73.  
  74.  
  75. const archive_org_re = /^(?:https?:\/\/)?(?:web\.archive\.org\/web\/\d+\/)/
  76. // A function to replace an Imgur link with its archived version if it exists in the dictionary
  77. function replaceImgurLink(link) {
  78. var url = link.href.replace(archive_org_re, '');
  79. if (url in imgurArchive) {
  80. if (imgurArchive[url] !== true && imgurArchive[url] !== false) {
  81. // Set archive link if not already set
  82. if (url in imgurArchive) link.setAttribute('href', imgurArchive[url]);
  83.  
  84. if (scriptConfig.changeLinkStyleOnHover) {
  85. // Save old element attributes to be restored
  86. let old_style = link.style; let old_title = link.title;
  87.  
  88. // Set temporary element attributes
  89. link.style.color = 'green';
  90. link.style.outline = 'thin dotted green';
  91. link.title = scriptConfig.linkTooltips.available;
  92.  
  93. // Add event listener to restore old element attributes
  94. link.addEventListener('mouseleave', function () {
  95. link.style = old_style; link.title = old_title;
  96. }, { once: true });
  97. }
  98. } else if (imgurArchive[url] === false && scriptConfig.changeLinkStyleOnHover) {
  99. // Save old element attributes to be restored
  100. let old_style = link.style; let old_title = link.title;
  101.  
  102. // Set temporary element attributes
  103. link.style.color = 'red';
  104. link.style.outline = 'thin dotted red';
  105. link.title = scriptConfig.linkTooltips.unavailable;
  106.  
  107. // Add event listener to restore old element attributes
  108. link.addEventListener('mouseleave', function () {
  109. link.style = old_style; link.title = old_title;
  110. }, { once: true });
  111. }
  112. }
  113. }
  114.  
  115. // A list of elements that have already been processed (for dynamic content loading)
  116. var processed_elements = {};
  117. function updateURLs() {
  118. // Get all the relevant elements in the document
  119. var elements = {};
  120. for (let el in scriptConfig.DOMElements) {
  121. elements[el] = Array.from(document.getElementsByTagName(el));
  122. }
  123. //var links = Array.from(document.getElementsByTagName('a'));
  124.  
  125. // Filter out link tags that have already been processed
  126. if (processed_elements !== {}) {
  127. for (let el in elements) {
  128. for (let i = 0; i < elements[el].length; i++) {
  129. if (elements[el][i] in processed_elements) {
  130. elements[el].splice(i, 1);
  131. } else {
  132. if (!processed_elements[el]) processed_elements[el] = [];
  133. processed_elements[el].push(elements[el][i]);
  134. }
  135. }
  136. }
  137. } else {
  138. processed_elements = elements;
  139. }
  140.  
  141. // Loop through the elements and check if they are associated with Imgur links
  142. for (let el in elements) {
  143. for (let i = 0; i < elements[el].length; i++) {
  144. let elementURL = elements[el][i][scriptConfig.DOMElements[el]];
  145. if (elementURL.indexOf('imgur') !== -1 && elementURL.indexOf('archive.org') === -1) {
  146. checkImgurURL(elementURL);
  147. if (elementURL in imgurArchive) {
  148. elements[el][i].setAttribute(scriptConfig.DOMElements[el], imgurArchive[elementURL]);
  149. }
  150. }
  151. }
  152. }
  153. }
  154. updateURLs();
  155.  
  156. // Create an observer instance to respond to new content loaded dynamically by the page
  157. var observer = new MutationObserver (function (mutations) {
  158. mutations.forEach (function (mutation) {
  159. if (mutation.type === "childList") {
  160. mutation.addedNodes.forEach (function (node) {
  161. var elements = {};
  162. if (node.tagName in scriptConfig.DOMElements) {
  163. elements[node.tagName] = [node];
  164. } else if (node.childNodes && String(node) !== '[object Text]') {
  165. for (let el in scriptConfig.DOMElements) {
  166. elements[el] = node.querySelectorAll(el);
  167. }
  168. } else return;
  169. for (let el in elements) {
  170. for (let i = 0; i < elements[el].length; i++) {
  171. let elementURL = elements[el][i][scriptConfig.DOMElements[el]].replace('http://','https://');
  172. if (elementURL.indexOf('imgur') !== -1 && elementURL.indexOf('archive.org') === -1) {
  173. checkImgurURL(elementURL);
  174. if (elementURL in imgurArchive) {
  175. elements[el][i].setAttribute(scriptConfig.DOMElements[el], imgurArchive[elementURL]);
  176. }
  177. if (!processed_elements[el]) processed_elements[el] = [];
  178. processed_elements[el].push(elements[el][i]);
  179. }
  180. }
  181. }
  182. });
  183. }
  184. });
  185. });
  186. observer.observe(document.documentElement || document.body, {childList: true, subtree: true});
  187.  
  188. // Add an event listener to the whole document that waits for hovers on links
  189. document.addEventListener('mouseover', function (event) {
  190. // Get the target element of the hover event
  191. var target = event.target;
  192. //Traverse the DOM tree until we find a link (or not)
  193. while (target && target.tagName !== 'A') {
  194. target = target.parentNode;
  195. }
  196. // If a link is found, replace it with its archived version if possible
  197. if (target) {
  198. replaceImgurLink(target);
  199. }
  200. });
  201. })();