Wayback Machine Auto Hopper

Automatically jump to the earliest or latest page of the search results on INTERNET ARCHIVE Wayback Machine.

  1. // ==UserScript==
  2. // @name Wayback Machine Auto Hopper
  3. // @name:ja Wayback Machine Auto Hopper
  4. // @name:zh-CN Wayback Machine Auto Hopper
  5. // @description Automatically jump to the earliest or latest page of the search results on INTERNET ARCHIVE Wayback Machine.
  6. // @description:ja INTERNET ARCHIVE の Wayback Machine でURL検索をした際、最古または最新のページに自動で飛びます。
  7. // @description:zh-CN 自动跳转到 INTERNET ARCHIVE Wayback Machine 上搜索结果的最早或最新页面。
  8. // @namespace knoa.jp
  9. // @include /^http:\/\/web\.archive\.org\/web\/\*\/https?:\/\//
  10. // @version 1.0.1
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function(){
  15. const SCRIPTID = 'WaybackMachineAutoHopper';
  16. const SCRIPTNAME = 'Wayback Machine Auto Hopper';
  17. const DEBUG = false;/*
  18. [update] 1.0.1
  19. No updates on code. Just confirmed to work.
  20.  
  21. [bug]
  22.  
  23. [todo]
  24.  
  25. [possible]
  26.  
  27. [research]
  28. [Esc]のキャンセルを捉えたことがわかる反応がほしいね。
  29.  
  30. [memo]
  31. */
  32. if(window === top && console.time) console.time(SCRIPTID);
  33. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  34. const HOPTO = 'EARLIEST';/* EARLIEST or LATEST */
  35. const DELAY = 0;/* set some time to keep chance to press [Esc] to cancel the hop */
  36. const site = {
  37. targets: {
  38. reactWaybackSearch: () => $('#react-wayback-search'),
  39. },
  40. get: {
  41. EARLIEST: () => $('.captures-range-info a[href]:first-of-type'),
  42. LATEST: () => $('.captures-range-info a[href]:last-of-type'),
  43. selector: () => {
  44. switch(HOPTO){
  45. case('EARLIEST'): return site.get.EARLIEST;
  46. case('LATEST'): return site.get.LATEST;
  47. default:
  48. console.error(SCRIPTNAME, 'Unknown HOPTO:', HOPTO);
  49. return null;
  50. }
  51. },
  52. },
  53. };
  54. let elements = {};
  55. const core = {
  56. initialize: function(){
  57. elements.html = document.documentElement;
  58. elements.html.classList.add(SCRIPTID);
  59. core.ready();
  60. },
  61. ready: function(){
  62. let canceled = false, selector = site.get.selector(), a, observer;
  63. if(selector === null) return;
  64. window.addEventListener('keydown', function(e){
  65. if(canceled) return;
  66. if(e.key !== 'Escape') return;
  67. if(observer) observer.disconnect();
  68. canceled = true;
  69. log('Canceled.');
  70. });
  71. core.getTargets(site.targets).then(() => {
  72. log("I'm ready.");
  73. if(canceled) return;
  74. a = selector();
  75. if(a) core.hop(a);
  76. else observer = observe(elements.reactWaybackSearch, function(records){
  77. a = selector();
  78. if(a){
  79. observer.disconnect();
  80. core.hop(a);
  81. }
  82. }, {childList: true, subtree: true});
  83. }).catch(selector => {
  84. log(`Not found: ${selector.name}, I give up.`);
  85. });
  86. },
  87. hop: function(a){
  88. setTimeout(function(){
  89. log('Jump to:', a.href);
  90. a.click();
  91. }, DELAY);
  92. },
  93. getTarget: function(selector, retry = 10, interval = 1*SECOND){
  94. const key = selector.name;
  95. const get = function(resolve, reject){
  96. let selected = selector();
  97. if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
  98. else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
  99. else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, interval, resolve, reject);
  100. else return reject(selector);
  101. elements[key] = selected;
  102. resolve(selected);
  103. };
  104. return new Promise(function(resolve, reject){
  105. get(resolve, reject);
  106. });
  107. },
  108. getTargets: function(selectors, retry = 10, interval = 1*SECOND){
  109. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry, interval)));
  110. },
  111. };
  112. const setTimeout = window.setTimeout.bind(window), clearTimeout = window.clearTimeout.bind(window), setInterval = window.setInterval.bind(window), clearInterval = window.clearInterval.bind(window), requestAnimationFrame = window.requestAnimationFrame.bind(window);
  113. const alert = window.alert.bind(window), confirm = window.confirm.bind(window), prompt = window.prompt.bind(window), getComputedStyle = window.getComputedStyle.bind(window), fetch = window.fetch.bind(window);
  114. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  115. const $ = function(s, f){
  116. let target = document.querySelector(s);
  117. if(target === null) return null;
  118. return f ? f(target) : target;
  119. };
  120. const $$ = function(s, f){
  121. let targets = document.querySelectorAll(s);
  122. return f ? Array.from(targets).map(t => f(t)) : targets;
  123. };
  124. const createElement = function(html = '<span></span>'){
  125. let outer = document.createElement('div');
  126. outer.innerHTML = html;
  127. return outer.firstElementChild;
  128. };
  129. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  130. let observer = new MutationObserver(callback.bind(element));
  131. observer.observe(element, options);
  132. return observer;
  133. };
  134. const log = function(){
  135. if(!DEBUG) return;
  136. let l = log.last = log.now || new Date(), n = log.now = new Date();
  137. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  138. //console.log(error.stack);
  139. console.log(
  140. SCRIPTID + ':',
  141. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  142. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  143. /* :00 */ ':' + line,
  144. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  145. /* caller */ (callers[1] || '') + '()',
  146. ...arguments
  147. );
  148. };
  149. log.formats = [{
  150. name: 'Firefox Scratchpad',
  151. detector: /MARKER@Scratchpad/,
  152. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  153. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  154. }, {
  155. name: 'Firefox Console',
  156. detector: /MARKER@debugger/,
  157. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  158. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  159. }, {
  160. name: 'Firefox Greasemonkey 3',
  161. detector: /\/gm_scripts\//,
  162. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  163. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  164. }, {
  165. name: 'Firefox Greasemonkey 4+',
  166. detector: /MARKER@user-script:/,
  167. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  168. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  169. }, {
  170. name: 'Firefox Tampermonkey',
  171. detector: /MARKER@moz-extension:/,
  172. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  173. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  174. }, {
  175. name: 'Chrome Console',
  176. detector: /at MARKER \(<anonymous>/,
  177. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  178. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  179. }, {
  180. name: 'Chrome Tampermonkey',
  181. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?name=/,
  182. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 4,
  183. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  184. }, {
  185. name: 'Chrome Extension',
  186. detector: /at MARKER \(chrome-extension:/,
  187. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  188. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  189. }, {
  190. name: 'Edge Console',
  191. detector: /at MARKER \(eval/,
  192. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  193. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  194. }, {
  195. name: 'Edge Tampermonkey',
  196. detector: /at MARKER \(Function/,
  197. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  198. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  199. }, {
  200. name: 'Safari',
  201. detector: /^MARKER$/m,
  202. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  203. getCallers: (e) => e.stack.split('\n'),
  204. }, {
  205. name: 'Default',
  206. detector: /./,
  207. getLine: (e) => 0,
  208. getCallers: (e) => [],
  209. }];
  210. log.format = log.formats.find(function MARKER(f){
  211. if(!f.detector.test(new Error().stack)) return false;
  212. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  213. return true;
  214. });
  215. core.initialize();
  216. if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
  217. })();