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.2.4
  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. // @grant GM_addElement
  15. // ==/UserScript==
  16.  
  17. (async (document) => {
  18. 'use strict';
  19. let fastNode;
  20. let failed;
  21. let isRunning;
  22. const DEST_LIST = [
  23. 'cdn.jsdelivr.net',
  24. 'fastly.jsdelivr.net',
  25. 'gcore.jsdelivr.net',
  26. 'cdn.zenless.top',
  27. 'testingcf.jsdelivr.net',
  28. 'test1.jsdelivr.net'
  29. ];
  30. const PREFIX = '//';
  31. const SOURCE = DEST_LIST[0];
  32. const starTime = Date.now();
  33. const TIMEOUT = 2000;
  34. const STORE_KEY = 'jsdelivr-auto-fallback';
  35. const TEST_PATH = '/gh/PipecraftNet/jsdelivr-auto-fallback@main/empty.css?';
  36. const shouldReplace = (text) => text && text.includes(PREFIX + SOURCE);
  37. const replace = (text) => text.replace(PREFIX + SOURCE, PREFIX + fastNode);
  38. const $ = document.querySelectorAll.bind(document);
  39. const setAttributes = (element, attributes) => {
  40. if (element && attributes) {
  41. for (const name in attributes) {
  42. if (Object.hasOwn(attributes, name)) {
  43. const value = attributes[name];
  44. if (value === undefined) {
  45. continue;
  46. }
  47.  
  48. element.setAttribute(name, value);
  49. }
  50. }
  51. }
  52.  
  53. return element;
  54. };
  55.  
  56. const createElement = (tagName, attributes) =>
  57. setAttributes(document.createElement(tagName), attributes);
  58. const addElement =
  59. typeof GM_addElement === 'function'
  60. ? GM_addElement
  61. : (parentNode, tagName, attributes) => {
  62. if (!parentNode) {
  63. return;
  64. }
  65.  
  66. if (typeof parentNode === 'string') {
  67. attributes = tagName;
  68. tagName = parentNode;
  69. parentNode = document.head;
  70. }
  71.  
  72. const element = createElement(tagName, attributes);
  73. parentNode.append(element);
  74. return element;
  75. };
  76.  
  77. const replaceElementSrc = () => {
  78. let element;
  79. let value;
  80. for (element of $('link[rel="stylesheet"]')) {
  81. value = element.href;
  82. if (shouldReplace(value) && !value.includes(TEST_PATH)) {
  83. element.href = replace(value);
  84. }
  85. }
  86.  
  87. for (element of $('script')) {
  88. value = element.src;
  89. if (shouldReplace(value)) {
  90. addElement(element.parentNode, 'script', {
  91. src: replace(value)
  92. });
  93. element.defer = true;
  94. element.src = '';
  95. element.remove();
  96. }
  97. }
  98.  
  99. for (element of $('img')) {
  100. value = element.src;
  101. if (shouldReplace(value)) {
  102. // Used to cancel loading. Without this line it will remain pending status.
  103. element.src = '';
  104. element.src = replace(value);
  105. }
  106. }
  107.  
  108. // All elements that have a style attribute
  109. for (element of $('*[style]')) {
  110. value = element.getAttribute('style');
  111. if (shouldReplace(value)) {
  112. element.setAttribute('style', replace(value));
  113. }
  114. }
  115.  
  116. for (element of $('style')) {
  117. value = element.innerHTML;
  118. if (shouldReplace(value)) {
  119. element.innerHTML = replace(value);
  120. }
  121. }
  122. };
  123.  
  124. const tryReplace = () => {
  125. if (!isRunning && failed && fastNode) {
  126. console.warn(SOURCE + ' is not available. Use ' + fastNode);
  127. isRunning = true;
  128. setTimeout(replaceElementSrc, 0);
  129. // Some need to wait for a while
  130. setTimeout(replaceElementSrc, 20);
  131. // Replace dynamically added elements
  132. setInterval(replaceElementSrc, 500);
  133. }
  134. };
  135.  
  136. const checkAvailable = (url, callback) => {
  137. let timeoutId;
  138. const newNode = addElement(document.head, 'link', {
  139. rel: 'stylesheet',
  140. text: 'text/css',
  141. href: url + TEST_PATH + starTime
  142. });
  143.  
  144. const handleResult = (isSuccess) => {
  145. if (!timeoutId) {
  146. return;
  147. }
  148.  
  149. clearTimeout(timeoutId);
  150. timeoutId = 0;
  151. // Used to cancel loading. Without this line it will remain pending status.
  152. if (!isSuccess) newNode.href = 'data:text/css;base64,';
  153. newNode.remove();
  154. callback(isSuccess);
  155. };
  156.  
  157. timeoutId = setTimeout(handleResult, TIMEOUT);
  158.  
  159. newNode.addEventListener('error', () => handleResult(false));
  160. newNode.addEventListener('load', () => handleResult(true));
  161. };
  162.  
  163. const cached = await (async () => {
  164. try {
  165. return Object.assign({}, await GM.getValue(STORE_KEY));
  166. } catch {
  167. return {};
  168. }
  169. })();
  170.  
  171. const main = () => {
  172. cached.time = starTime;
  173. cached.failed = false;
  174. cached.fastNode = null;
  175.  
  176. for (const url of DEST_LIST) {
  177. checkAvailable('https://' + url, (isAvailable) => {
  178. // console.log(url, Date.now() - starTime, Boolean(isAvailable));
  179. if (!isAvailable && url === SOURCE) {
  180. failed = true;
  181. cached.failed = true;
  182. }
  183.  
  184. if (isAvailable && !fastNode) {
  185. fastNode = url;
  186. }
  187.  
  188. if (isAvailable && !cached.fastNode) {
  189. cached.fastNode = url;
  190. }
  191.  
  192. tryReplace();
  193. });
  194. }
  195.  
  196. setTimeout(() => {
  197. // If all domains are timeout
  198. if (failed && !fastNode) {
  199. fastNode = DEST_LIST[1];
  200. tryReplace();
  201. }
  202.  
  203. GM.setValue(STORE_KEY, cached);
  204. }, TIMEOUT + 100);
  205. };
  206.  
  207. if (
  208. cached.time &&
  209. starTime - cached.time < 60 * 60 * 1000 &&
  210. cached.failed &&
  211. cached.fastNode
  212. ) {
  213. failed = true;
  214. fastNode = cached.fastNode;
  215. tryReplace();
  216. setTimeout(main, 1000);
  217. } else if (document.head) {
  218. main();
  219. } else {
  220. const observer = new MutationObserver(() => {
  221. if (document.head) {
  222. observer.disconnect();
  223. main();
  224. }
  225. });
  226. const observerOptions = {
  227. childList: true,
  228. subtree: true
  229. };
  230. observer.observe(document, observerOptions);
  231. }
  232. })(document);