Jsdelivr Auto Fallback

修复 cdn.jsdelivr.net 无法访问的问题

  1. // ==UserScript==
  2. // @name Jsdelivr Auto Fallback
  3. // @namespace https://github.com/PipecraftNet/jsdelivr-auto-fallback
  4. // @version 0.3.3
  5. // @author PipecraftNet&DreamOfIce
  6. // @description 修复 cdn.jsdelivr.net 无法访问的问题
  7. // @homepage https://github.com/PipecraftNet/jsdelivr-auto-fallback
  8. // @supportURL https://github.com/PipecraftNet/jsdelivr-auto-fallback/issues
  9. // @license MIT
  10. // @match *://*/*
  11. // @run-at document-start
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // ==/UserScript==
  15. ((document) => {
  16. 'use strict';
  17. let fastNode;
  18. let failed;
  19. let isRunning;
  20. const DEST_LIST = [
  21. 'cdn.jsdelivr.net',
  22. 'fastly.jsdelivr.net',
  23.  
  24. 'testingcf.jsdelivr.net',
  25. 'test1.jsdelivr.net',
  26. 'gcore.jsdelivr.net'
  27. ];
  28. const PREFIX = '//';
  29. const SOURCE = DEST_LIST[0];
  30. const starTime = Date.now();
  31. const TIMEOUT = 2000;
  32. const STORE_KEY = 'jsdelivr-auto-fallback';
  33. const TEST_PATH = '/gh/PipecraftNet/jsdelivr-auto-fallback@main/empty.css?';
  34. const shouldReplace = (text) => text && text.includes(PREFIX + SOURCE);
  35. const replace = (text) => text.replace(PREFIX + SOURCE, PREFIX + fastNode);
  36. const setTimeout = window.setTimeout;
  37. const $ = document.querySelectorAll.bind(document);
  38. const replaceElementSrc = () => {
  39. let element;
  40. let value;
  41. for (element of $('link[rel="stylesheet"]')) {
  42. value = element.href;
  43. if (shouldReplace(value) && !value.includes(TEST_PATH)) {
  44. element.href = replace(value);
  45. }
  46. }
  47. for (element of $('script')) {
  48. value = element.src;
  49. if (shouldReplace(value)) {
  50. const newNode = document.createElement('script');
  51. newNode.src = replace(value);
  52. element.defer = true;
  53. element.src = '';
  54. element.before(newNode);
  55. element.remove();
  56. }
  57. }
  58. for (element of $('img')) {
  59. value = element.src;
  60. if (shouldReplace(value)) {
  61. // Used to cancel loading. Without this line it will remain pending status.
  62. element.src = '';
  63. element.src = replace(value);
  64. }
  65. }
  66. // All elements that have a style attribute
  67. for (element of $('*[style]')) {
  68. value = element.getAttribute('style');
  69. if (shouldReplace(value)) {
  70. element.setAttribute('style', replace(value));
  71. }
  72. }
  73. for (element of $('style')) {
  74. value = element.innerHTML;
  75. if (shouldReplace(value)) {
  76. element.innerHTML = replace(value);
  77. }
  78. }
  79. };
  80. const tryReplace = () => {
  81. if (!isRunning && failed && fastNode) {
  82. console.warn(SOURCE + ' is not available. Use ' + fastNode);
  83. isRunning = true;
  84. setTimeout(replaceElementSrc, 0);
  85. // Some need to wait for a while
  86. setTimeout(replaceElementSrc, 20);
  87. // Replace dynamically added elements
  88. setInterval(replaceElementSrc, 500);
  89. }
  90. };
  91. const checkAvailable = (url, callback) => {
  92. let timeoutId;
  93. const newNode = document.createElement('link');
  94. const handleResult = (isSuccess) => {
  95. if (!timeoutId) {
  96. return;
  97. }
  98. clearTimeout(timeoutId);
  99. timeoutId = 0;
  100. // Used to cancel loading. Without this line it will remain pending status.
  101. if (!isSuccess) newNode.href = 'data:text/plain;base64,';
  102. newNode.remove();
  103. callback(isSuccess);
  104. };
  105. timeoutId = setTimeout(handleResult, TIMEOUT);
  106. newNode.addEventListener('error', () => handleResult(false));
  107. newNode.addEventListener('load', () => handleResult(true));
  108. newNode.rel = 'stylesheet';
  109. newNode.text = 'text/css';
  110. newNode.href = url + TEST_PATH + starTime;
  111. document.head.insertAdjacentElement('afterbegin', newNode);
  112. };
  113. const cached = (() => {
  114. try {
  115. // eslint-disable-next-line new-cap
  116. return Object.assign({}, GM_getValue(STORE_KEY));
  117. } catch {
  118. return {};
  119. }
  120. })();
  121. const main = () => {
  122. cached.time = starTime;
  123. cached.failed = false;
  124. cached.fastNode = null;
  125. for (const url of DEST_LIST) {
  126. checkAvailable('https://' + url, (isAvailable) => {
  127. // console.log(url, Date.now() - starTime, Boolean(isAvailable));
  128. if (!isAvailable && url === SOURCE) {
  129. failed = true;
  130. cached.failed = true;
  131. }
  132. if (isAvailable && !fastNode) {
  133. fastNode = url;
  134. }
  135. if (isAvailable && !cached.fastNode) {
  136. cached.fastNode = url;
  137. }
  138. tryReplace();
  139. });
  140. }
  141. setTimeout(() => {
  142. // If all domains are timeout
  143. if (failed && !fastNode) {
  144. fastNode = DEST_LIST[1];
  145. tryReplace();
  146. }
  147. // eslint-disable-next-line new-cap
  148. GM_setValue(STORE_KEY, cached);
  149. }, TIMEOUT + 100);
  150. };
  151. if (
  152. cached.time &&
  153. starTime - cached.time < 60 * 60 * 1000 &&
  154. cached.failed &&
  155. cached.fastNode
  156. ) {
  157. failed = true;
  158. fastNode = cached.fastNode;
  159. tryReplace();
  160. setTimeout(main, 1000);
  161. } else {
  162. main();
  163. }
  164. })(document);