Wayback Machine Auto Hopper

自动跳转到 INTERNET ARCHIVE Wayback Machine 上搜索结果的最早或最新页面。

目前为 2020-05-19 提交的版本,查看 最新版本

  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
  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]
  19.  
  20. [bug]
  21.  
  22. [todo]
  23.  
  24. [possible]
  25.  
  26. [research]
  27.  
  28. [memo]
  29. */
  30. if(window === top && console.time) console.time(SCRIPTID);
  31. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  32. const HOPTO = 'EARLIEST';/* EARLIEST or LATEST */
  33. const DELAY = 0;/* set some time to keep chance to press [Esc] to cancel the hop */
  34. const site = {
  35. targets: {
  36. reactWaybackSearch: () => $('#react-wayback-search'),
  37. capturesRangeInfo: () => $('.captures-range-info'),
  38. },
  39. get: {
  40. EARLIEST: () => $('.captures-range-info a[href]:first-of-type', a => a.href),
  41. LATEST: () => $('.captures-range-info a[href]:last-of-type', a => a.href),
  42. selector: () => {
  43. switch(HOPTO){
  44. case('EARLIEST'): return site.get.EARLIEST;
  45. case('LATEST'): return site.get.LATEST;
  46. default:
  47. console.error(SCRIPTNAME, 'Unknown HOPTO:', HOPTO);
  48. return null;
  49. }
  50. },
  51. },
  52. };
  53. let elements = {}, timers = {};
  54. const core = {
  55. initialize: function(){
  56. elements.html = document.documentElement;
  57. elements.html.classList.add(SCRIPTID);
  58. core.ready();
  59. },
  60. ready: function(){
  61. let canceled = false, selector = site.get.selector(), url, observer;
  62. if(selector === null) return;
  63. window.addEventListener('keydown', function(e){
  64. if(canceled) return;
  65. if(e.key !== 'Escape') return;
  66. if(observer) observer.disconnect();
  67. canceled = true;
  68. log('Canceled.');
  69. });
  70. core.getTargets(site.targets, 40, 250*MS).then(() => {
  71. log("I'm ready.");
  72. if(canceled) return;
  73. url = selector();
  74. if(url) core.hop(url);
  75. else observer = observe(elements.reactWaybackSearch, function(records){
  76. url = selector();
  77. if(url) core.hop(url), observer.disconnect();
  78. }, {childList: true, subtree: true});
  79.  
  80. }).catch(selector => {
  81. log(`Not found: ${selector.name}, I give up.`);
  82. });
  83. },
  84. hop: function(url){
  85. setTimeout(function(){
  86. log('Jump to:', url);
  87. location.assign(url);
  88. }, DELAY);
  89. },
  90. getTarget: function(selector, retry = 10, interval = 1*SECOND){
  91. const key = selector.name;
  92. const get = function(resolve, reject){
  93. let selected = selector();
  94. if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
  95. else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
  96. else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, interval, resolve, reject);
  97. else return reject(selector);
  98. elements[key] = selected;
  99. resolve(selected);
  100. };
  101. return new Promise(function(resolve, reject){
  102. get(resolve, reject);
  103. });
  104. },
  105. getTargets: function(selectors, retry = 10, interval = 1*SECOND){
  106. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry, interval)));
  107. },
  108. };
  109. 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);
  110. const alert = window.alert.bind(window), confirm = window.confirm.bind(window), getComputedStyle = window.getComputedStyle.bind(window), fetch = window.fetch.bind(window);
  111. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  112. const $ = function(s, f){
  113. let target = document.querySelector(s);
  114. if(target === null) return null;
  115. return f ? f(target) : target;
  116. };
  117. const $$ = function(s, f){
  118. let targets = document.querySelectorAll(s);
  119. return f ? Array.from(targets).map(t => f(t)) : targets;
  120. };
  121. const createElement = function(html = '<span></span>'){
  122. let outer = document.createElement('div');
  123. outer.innerHTML = html;
  124. return outer.firstElementChild;
  125. };
  126. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  127. let observer = new MutationObserver(callback.bind(element));
  128. observer.observe(element, options);
  129. return observer;
  130. };
  131. const log = function(){
  132. if(!DEBUG) return;
  133. let l = log.last = log.now || new Date(), n = log.now = new Date();
  134. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  135. //console.log(error.stack);
  136. console.log(
  137. SCRIPTID + ':',
  138. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  139. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  140. /* :00 */ ':' + line,
  141. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  142. /* caller */ (callers[1] || '') + '()',
  143. ...arguments
  144. );
  145. };
  146. log.formats = [{
  147. name: 'Firefox Scratchpad',
  148. detector: /MARKER@Scratchpad/,
  149. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  150. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  151. }, {
  152. name: 'Firefox Console',
  153. detector: /MARKER@debugger/,
  154. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  155. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  156. }, {
  157. name: 'Firefox Greasemonkey 3',
  158. detector: /\/gm_scripts\//,
  159. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  160. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  161. }, {
  162. name: 'Firefox Greasemonkey 4+',
  163. detector: /MARKER@user-script:/,
  164. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  165. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  166. }, {
  167. name: 'Firefox Tampermonkey',
  168. detector: /MARKER@moz-extension:/,
  169. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  170. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  171. }, {
  172. name: 'Chrome Console',
  173. detector: /at MARKER \(<anonymous>/,
  174. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  175. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  176. }, {
  177. name: 'Chrome Tampermonkey',
  178. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?name=/,
  179. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 4,
  180. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  181. }, {
  182. name: 'Chrome Extension',
  183. detector: /at MARKER \(chrome-extension:/,
  184. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  185. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  186. }, {
  187. name: 'Edge Console',
  188. detector: /at MARKER \(eval/,
  189. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  190. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  191. }, {
  192. name: 'Edge Tampermonkey',
  193. detector: /at MARKER \(Function/,
  194. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  195. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  196. }, {
  197. name: 'Safari',
  198. detector: /^MARKER$/m,
  199. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  200. getCallers: (e) => e.stack.split('\n'),
  201. }, {
  202. name: 'Default',
  203. detector: /./,
  204. getLine: (e) => 0,
  205. getCallers: (e) => [],
  206. }];
  207. log.format = log.formats.find(function MARKER(f){
  208. if(!f.detector.test(new Error().stack)) return false;
  209. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  210. return true;
  211. });
  212. core.initialize();
  213. if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
  214. })();