LocalCDN

LocalCDN: Webresource manager to request and cache web resources, aiming to make web requests faster and more reliable.

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/449580/1081620/LocalCDN.js

  1. /* eslint-disable no-multi-spaces */
  2.  
  3. // ==UserScript==
  4. // @name LocalCDN
  5. // @namespace LocalCDN
  6. // @version 0.1.1
  7. // @description LocalCDN: Webresource manager to request and cache web resources, aiming to make web requests faster and more reliable.
  8. // @author PY-DNG
  9. // @license GPL-v3
  10. // @grant none
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM_xmlhttpRequest
  14. // ==/UserScript==
  15.  
  16.  
  17.  
  18.  
  19. // Loads web resources and saves them to GM-storage
  20. // Tries to load web resources from GM-storage in subsequent calls
  21. // Updates resources every $(this.expire) hours, or use $(this.refresh) function to update all resources instantly
  22. // Dependencies: GM_getValue(), GM_setValue(), requestText(), AsyncManager()
  23. function LocalCDN(expire=72) {
  24. // Arguments: level=LogLevel.Info, logContent, asObject=false
  25. // Needs one call "DoLog();" to get it initialized before using it!
  26. function DoLog() {
  27. // Get window
  28. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;
  29.  
  30. // Global log levels set
  31. win.LogLevel = {
  32. None: 0,
  33. Error: 1,
  34. Success: 2,
  35. Warning: 3,
  36. Info: 4,
  37. }
  38. win.LogLevelMap = {};
  39. win.LogLevelMap[LogLevel.None] = {
  40. prefix: '',
  41. color: 'color:#ffffff'
  42. }
  43. win.LogLevelMap[LogLevel.Error] = {
  44. prefix: '[Error]',
  45. color: 'color:#ff0000'
  46. }
  47. win.LogLevelMap[LogLevel.Success] = {
  48. prefix: '[Success]',
  49. color: 'color:#00aa00'
  50. }
  51. win.LogLevelMap[LogLevel.Warning] = {
  52. prefix: '[Warning]',
  53. color: 'color:#ffa500'
  54. }
  55. win.LogLevelMap[LogLevel.Info] = {
  56. prefix: '[Info]',
  57. color: 'color:#888888'
  58. }
  59. win.LogLevelMap[LogLevel.Elements] = {
  60. prefix: '[Elements]',
  61. color: 'color:#000000'
  62. }
  63.  
  64. // Current log level
  65. DoLog.logLevel = win.isPY_DNG ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  66.  
  67. // Log counter
  68. DoLog.logCount === undefined && (DoLog.logCount = 0);
  69.  
  70. // Get args
  71. let level, logContent, asObject;
  72. switch (arguments.length) {
  73. case 1:
  74. level = LogLevel.Info;
  75. logContent = arguments[0];
  76. asObject = false;
  77. break;
  78. case 2:
  79. level = arguments[0];
  80. logContent = arguments[1];
  81. asObject = false;
  82. break;
  83. case 3:
  84. level = arguments[0];
  85. logContent = arguments[1];
  86. asObject = arguments[2];
  87. break;
  88. default:
  89. level = LogLevel.Info;
  90. logContent = 'DoLog initialized.';
  91. asObject = false;
  92. break;
  93. }
  94.  
  95. // Log when log level permits
  96. if (level <= DoLog.logLevel) {
  97. let msg = '%c' + LogLevelMap[level].prefix;
  98. let subst = LogLevelMap[level].color;
  99.  
  100. if (asObject) {
  101. msg += ' %o';
  102. } else {
  103. switch (typeof(logContent)) {
  104. case 'string':
  105. msg += ' %s';
  106. break;
  107. case 'number':
  108. msg += ' %d';
  109. break;
  110. case 'object':
  111. msg += ' %o';
  112. break;
  113. }
  114. }
  115.  
  116. if (++DoLog.logCount > 512) {
  117. console.clear();
  118. DoLog.logCount = 0;
  119. }
  120. console.log(msg, subst, logContent);
  121. }
  122. }
  123. DoLog();
  124.  
  125. const LC = this;
  126. const _GM_getValue = GM_getValue;
  127. const _GM_setValue = GM_setValue;
  128.  
  129. const KEY_LOCALCDN = 'LOCAL-CDN';
  130. const KEY_LOCALCDN_VERSION = 'version';
  131. const VALUE_LOCALCDN_VERSION = '0.3';
  132.  
  133. // Default expire time (by hour)
  134. LC.expire = expire;
  135.  
  136. // Try to get resource content from loaclCDN first, if failed/timeout, request from web && save to LocalCDN
  137. // Accepts callback only: onload & onfail(optional)
  138. // Returns true if got from LocalCDN, false if got from web
  139. LC.get = function(url, onload, args=[], onfail=function(){}) {
  140. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  141. const resource = CDN[url];
  142. const time = (new Date()).getTime();
  143.  
  144. if (resource && resource.content !== null && !expired(time, resource.time)) {
  145. onload.apply(null, [resource.content].concat(args));
  146. return true;
  147. } else {
  148. LC.request(url, _onload, [], onfail);
  149. return false;
  150. }
  151.  
  152. function _onload(content) {
  153. onload.apply(null, [content].concat(args));
  154. }
  155. }
  156.  
  157. // Generate resource obj and set to CDN[url]
  158. // Returns resource obj
  159. // Provide content means load success, provide null as content means load failed
  160. LC.set = function(url, content) {
  161. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  162. const time = (new Date()).getTime();
  163. const resource = {
  164. url: url,
  165. time: time,
  166. content: content,
  167. success: content !== null ? (CDN[url] ? CDN[url].success + 1 : 1) : (CDN[url] ? CDN[url].success : 0),
  168. fail: content === null ? (CDN[url] ? CDN[url].fail + 1 : 1) : (CDN[url] ? CDN[url].fail : 0),
  169. };
  170. CDN[url] = resource;
  171. _GM_setValue(KEY_LOCALCDN, CDN);
  172. return resource;
  173. }
  174.  
  175. // Delete one resource from LocalCDN
  176. LC.delete = function(url) {
  177. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  178. if (!CDN[url]) {
  179. return false;
  180. } else {
  181. delete CDN[url];
  182. _GM_setValue(KEY_LOCALCDN, CDN);
  183. return true;
  184. }
  185. }
  186.  
  187. // Delete all resources in LocalCDN
  188. LC.clear = function() {
  189. _GM_setValue(KEY_LOCALCDN, {});
  190. upgradeConfig();
  191. }
  192.  
  193. // List all resource saved in LocalCDN
  194. LC.list = function() {
  195. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  196. const urls = LC.listurls();
  197. return LC.listurls().map((url) => (CDN[url]));
  198. }
  199.  
  200. // List all resource's url saved in LocalCDN
  201. LC.listurls = function() {
  202. return Object.keys(_GM_getValue(KEY_LOCALCDN, {})).filter((url) => (url !== KEY_LOCALCDN_VERSION));
  203. }
  204.  
  205. // Request content from web and save it to CDN[url]
  206. // Accepts callbacks only: onload & onfail(optional)
  207. LC.request = function(url, onload, args=[], onfail=function(){}) {
  208. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  209. requestText(url, _onload, [], _onfail);
  210.  
  211. function _onload(content) {
  212. LC.set(url, content);
  213. onload.apply(null, [content].concat(args));
  214. }
  215.  
  216. function _onfail() {
  217. LC.set(url, null);
  218. onfail(url);
  219. }
  220. }
  221.  
  222. // Re-request all resources in CDN instantly, ignoring LC.expire
  223. LC.refresh = function(callback, args=[]) {
  224. const urls = LC.listurls();
  225.  
  226. const AM = new AsyncManager();
  227. AM.onfinish = function() {
  228. callback.apply(null, [].concat(args))
  229. };
  230.  
  231. for (const url of urls) {
  232. AM.add();
  233. LC.request(url, function() {
  234. AM.finish();
  235. });
  236. }
  237.  
  238. AM.finishEvent = true;
  239. }
  240.  
  241. // Sort src && srcset, to get a best request sorting
  242. LC.sort = function(srcset) {
  243. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  244. const result = {srclist: [], lists: []};
  245. const lists = result.lists;
  246. const srclist = result.srclist;
  247. const suc_rec = lists[0] = []; // Recent successes take second (not expired yet)
  248. const suc_old = lists[1] = []; // Old successes take third
  249. const fails = lists[2] = []; // Fails & unused take the last place
  250. const time = (new Date()).getTime();
  251.  
  252. // Make lists
  253. for (const s of srcset) {
  254. const resource = CDN[s];
  255. if (resource && resource.content !== null) {
  256. if (!expired(resource.time, time)) {
  257. suc_rec.push(s);
  258. } else {
  259. suc_old.push(s);
  260. }
  261. } else {
  262. fails.push(s);
  263. }
  264. }
  265.  
  266. // Sort lists
  267. // Recently successed: Choose most recent ones
  268. suc_rec.sort((res1, res2) => (res2.time - res1.time));
  269. // Successed long ago or failed: Sort by success rate & tried time
  270. [suc_old, fails].forEach((arr) => (arr.sort(sorting)));
  271.  
  272. // Push all resources into seclist
  273. [suc_rec, suc_old, fails].forEach((arr) => (arr.forEach((res) => (srclist.push(res)))));
  274.  
  275. return result;
  276.  
  277. function sorting(res1, res2) {
  278. const sucRate1 = (res1.success+1) / (res1.fail+1);
  279. const sucRate2 = (res2.success+1) / (res2.fail+1);
  280.  
  281. if (sucRate1 !== sucRate2) {
  282. // Success rate: high to low
  283. return sucRate2 - sucRate1;
  284. } else {
  285. // Tried time: less to more
  286. // Less tried time means newer added source
  287. return (res1.success+res1.fail) - (res2.success+res2.fail);
  288. }
  289. }
  290. }
  291.  
  292. function upgradeConfig() {
  293. const CDN = _GM_getValue(KEY_LOCALCDN, {});
  294. switch(CDN[KEY_LOCALCDN_VERSION]) {
  295. case undefined:
  296. init();
  297. break;
  298. case '0.1':
  299. v01_To_v02();
  300. logUpgrade();
  301. break;
  302. case '0.2':
  303. v01_To_v02();
  304. v02_To_v03();
  305. logUpgrade();
  306. break;
  307. case VALUE_LOCALCDN_VERSION:
  308. DoLog('LocalCDN is in latest version.');
  309. break;
  310. default:
  311. DoLog(LogLevel.Error, 'LocalCDN.upgradeConfig: Invalid config version({V}) for LocalCDN. '.replace('{V}', CDN[KEY_LOCALCDN_VERSION]));
  312. }
  313. CDN[KEY_LOCALCDN_VERSION] = VALUE_LOCALCDN_VERSION;
  314. _GM_setValue(KEY_LOCALCDN, CDN);
  315.  
  316. function logUpgrade() {
  317. DoLog(LogLevel.Success, 'LocalCDN successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', CDN[KEY_LOCALCDN_VERSION]).replaceAll('{V2}', VALUE_LOCALCDN_VERSION));
  318. }
  319.  
  320. function init() {
  321. // Nothing to do here
  322. }
  323.  
  324. function v01_To_v02() {
  325. const urls = LC.listurls();
  326. for (const url of urls) {
  327. if (url === KEY_LOCALCDN_VERSION) {continue;}
  328. CDN[url] = {
  329. url: url,
  330. time: 0,
  331. content: CDN[url]
  332. };
  333. }
  334. }
  335.  
  336. function v02_To_v03() {
  337. const urls = LC.listurls();
  338. for (const url of urls) {
  339. CDN[url].success = CDN[url].fail = 0;
  340. }
  341. }
  342. }
  343.  
  344. function clearExpired() {
  345. const resources = LC.list();
  346. const time = (new Date()).getTime();
  347.  
  348. for (const resource of resources) {
  349. expired(resource.time, time) && LC.delete(resource.url);
  350. }
  351. }
  352.  
  353. function expired(t1, t2) {
  354. return (t1 - t2) > (LC.expire * 60 * 60 * 1000);
  355. }
  356.  
  357. upgradeConfig();
  358. clearExpired();
  359.  
  360.  
  361. function requestText(url, callback, args=[], onfail=function(){}) {
  362. GM_xmlhttpRequest({
  363. method: 'GET',
  364. url: url,
  365. responseType: 'text',
  366. timeout: 45*1000,
  367. onload: function(response) {
  368. const text = response.responseText;
  369. const argvs = [text].concat(args);
  370. callback.apply(null, argvs);
  371. },
  372. onerror: onfail,
  373. ontimeout: onfail,
  374. onabort: onfail,
  375. })
  376. }
  377.  
  378. function AsyncManager() {
  379. const AM = this;
  380.  
  381. // Ongoing xhr count
  382. this.taskCount = 0;
  383.  
  384. // Whether generate finish events
  385. let finishEvent = false;
  386. Object.defineProperty(this, 'finishEvent', {
  387. configurable: true,
  388. enumerable: true,
  389. get: () => (finishEvent),
  390. set: (b) => {
  391. finishEvent = b;
  392. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  393. }
  394. });
  395.  
  396. // Add one task
  397. this.add = () => (++AM.taskCount);
  398.  
  399. // Finish one task
  400. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  401. }
  402. }
  403.  
  404.