download

download.lib

当前为 2020-06-29 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/398502/821561/download.js

  1. // ==UserScript==
  2. // @name download
  3. // @version 1.2.3
  4. // @include *
  5. // ==/UserScript==
  6. // TODO: 支持fetch,xhr
  7. (function (window) {
  8. const storageInit = {
  9. default: {
  10. debug: false,
  11. mode: 'gm_xhr', // one of gm_xhr,fetch,xhr
  12. retry: 5,
  13. css: [
  14. '#gmDownloadDialog{position:fixed;bottom:0;right:0;z-index:999999;background-color:white;border:1px solid black;text-align:center;color:black;overflow-x:hidden;overflow-y:auto;display:none;}',
  15.  
  16. '#gmDownloadDialog>.nav-bar>button{width:24px;height:24px;z-index:1000001;padding:0;margin:0;}',
  17. '#gmDownloadDialog>.nav-bar>[name="pause"]{float:left;}',
  18. '#gmDownloadDialog>.nav-bar>[name="pause"][value="pause"]::before{content:"⏸️"}',
  19. '#gmDownloadDialog>.nav-bar>[name="pause"][value="resume"]::before{content:"▶"}',
  20. '#gmDownloadDialog>.nav-bar>[name="hide"]{float:right;}',
  21. '#gmDownloadDialog>.nav-bar>[name="hide"]::before{content:"×";color:red;}',
  22. '#gmDownloadDialog>.nav-bar>[name="total-progress"]{cursor:pointer;width:calc(100% - 65px);margin:4px;}',
  23. '#gmDownloadDialog>.nav-bar>[name="total-progress"]::before{content:attr(value)" / "attr(max);}',
  24.  
  25. '#gmDownloadDialog>.task{overflow-x:hidden;overflow-y:auto;width:300px;height:40vh;}', // display:flex;flex-direction:column;
  26. '#gmDownloadDialog>.task>div{display:flex;}',
  27. '#gmDownloadDialog>.task>div>*{margin:0 2px;white-space:nowrap;display:inline-block;}',
  28.  
  29. '#gmDownloadDialog>.task>div>a[name="title"]{width:206px;overflow:hidden;text-overflow:ellipsis;text-align:justify;}',
  30. '#gmDownloadDialog>.task>div>a[name="title"]:empty::before{content:attr(href)}',
  31.  
  32. '#gmDownloadDialog>.task>div[status="downloading"]>progress{width:120px;display:inline-block!important;}',
  33. '#gmDownloadDialog>.task>div[status="downloading"]>progress::before{content:attr(value)" / "attr(max);}',
  34.  
  35. '#gmDownloadDialog>.task>div>[name="status"]{width:32px;}',
  36. '#gmDownloadDialog>.task>div[status="downloading"]>[name="status"]{width:48px;}',
  37. '#gmDownloadDialog>.task>div[status="downloading"]>[name="status"]::before{content:"下载中";color:#00f;}',
  38. '#gmDownloadDialog>.task>div[status="error"]>[name="status"]::before{content:"错误";color:#f00;}',
  39. '#gmDownloadDialog>.task>div[status="timeout"]>[name="status"]::before{content:"超时";color:#f00;}',
  40. '#gmDownloadDialog>.task>div[status="abort"]>[name="status"]::before{content:"取消";color:#f00;}',
  41. '#gmDownloadDialog>.task>div[status="load"]>[name="status"]::before{content:"完成";color:#0f0;}',
  42.  
  43. '#gmDownloadDialog>.task>div[status="downloading"]>[name="abort"]{width:32px;cursor:pointer;}',
  44. '#gmDownloadDialog>.task>div[status="downloading"]>[name="abort"]::before{content:"abort";color:#f00;}'
  45. ].join(''),
  46. progress: '{order}{title}{progress}{status}{abort}',
  47. thread: 5,
  48. onComplete (list) { }, // 当list任务全部完成时(不管是否有下载错误)
  49. onfailed (res, request) { }, // 当某次请求失败(error/timeout)超过重复次数(之后不再尝试请求)
  50. onfailedEvery (res, request, type) { }, // 当某次请求失败(error/timeout)
  51. async checkLoad (res) {}, // 返回布尔,当false时,执行onerror并再次请求
  52.  
  53. method: 'GET',
  54. user: null,
  55. password: null,
  56. overrideMimeType: null,
  57. headers: {
  58. // 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
  59. },
  60. responseType: 'text',
  61. timeout: null,
  62. anonymous: false,
  63. onabort (res, request) { },
  64. onerror (res, request) { },
  65. onload (res, request) { },
  66. onprogress (res, request) { },
  67. onreadystatechange (res, request) { },
  68. ontimeout (res, request) { }
  69. },
  70. list: [
  71. // request 请求信息
  72. // status 状态 undefined,downloading,error,timeout,abort,load
  73. // retry 重复请求次数
  74. // abort 终止请求
  75. // response
  76. ],
  77. pause: false,
  78. downloading: false,
  79. element: {},
  80. cache: []
  81. };
  82.  
  83. let storage = Object.assign({}, JSON.parse(JSON.stringify(storageInit)));
  84.  
  85. const updateProgress = (task, res = {}) => {
  86. let elem;
  87. let max = res.lengthComputable ? res.total : 1;
  88. let value = res.statusText === 'OK' ? max : res.lengthComputable ? res.loaded : 0;
  89. if (max !== 1 && value !== 0) {
  90. value = Math.floor(value / max * 100);
  91. max = 100;
  92. }
  93. if (storage.element.dialog.querySelector(`.task>[index="${task.request.index}"]`)) {
  94. elem = storage.element.dialog.querySelector(`.task>[index="${task.request.index}"]`);
  95. if (res.lengthComputable) {
  96. elem.querySelector('progress').setAttribute('value', value);
  97. elem.querySelector('progress').setAttribute('max', max);
  98. }
  99. if (task.request.title) {
  100. elem.querySelector('[name="title"]').textContent = task.request.title;
  101. } else if (res.statusText === 'OK' && !elem.querySelector('[name="title"]').textContent) {
  102. let dom;
  103. if (typeof res.response === 'string') {
  104. dom = new window.DOMParser().parseFromString(res.response, 'text/html');
  105. } else if (res.response instanceof window.Document) {
  106. dom = res.response;
  107. }
  108. if (dom instanceof window.Document) elem.querySelector('[name="title"]').textContent = dom.title;
  109. }
  110. } else {
  111. elem = document.createElement('div');
  112. elem.setAttribute('index', task.request.index);
  113. elem.innerHTML = storage.config.progress.replace(/\{(.*?)\}/g, (all, $1) => {
  114. if ($1 === 'order') {
  115. return `<span>${task.request.index + 1}</span>`;
  116. } else if ($1 === 'title') {
  117. const title = task.request.title || '';
  118. return `<a name="title" href="${task.request.url}" target="_blank">${title}</a>`;
  119. } else if ($1 === 'progress') {
  120. return `<progress value="${value}" max="${max}" style="display:none;"></progress>`;
  121. } else if ($1 === 'status') {
  122. return '<span name="status"></span>';
  123. } else if ($1 === 'abort') {
  124. return '<a name="abort"></a>';
  125. } else {
  126. return '';
  127. }
  128. });
  129. storage.element.dialog.querySelector('.task').appendChild(elem);
  130. }
  131. elem.setAttribute('status', task.status);
  132. elem.scrollIntoView();
  133. storage.element.dialog.querySelector('[name="total-progress"]').setAttribute('value', storage.list.filter(i => i.status && i.status !== 'downloading').length);
  134. };
  135.  
  136. const main = xhr;
  137. main.sync = xhrSync;
  138. main.init = (option) => {
  139. main.stop();
  140. for (const elem of Object.values(storage.element)) elem.parentNode.removeChild(elem);
  141. storage = Object.assign({}, JSON.parse(JSON.stringify(storageInit)));
  142. storage.config = Object.assign(storage.default, option);
  143. for (const listener of ['onComplete', 'onfailed', 'onfailedEvery', 'checkLoad',
  144. 'onabort', 'onerror', 'onload', 'onprogress', 'onreadystatechange', 'ontimeout']) {
  145. if (typeof storage.config[listener] !== 'function') storage.config[listener] = function () {};
  146. }
  147.  
  148. const style = document.createElement('style');
  149. style.id = 'gmDownloadStyle';
  150. style.textContent = storage.config.css;
  151. document.head.appendChild(style);
  152. storage.element.style = style;
  153.  
  154. const dialog = document.createElement('div');
  155. dialog.id = 'gmDownloadDialog';
  156. dialog.innerHTML = [
  157. '<div class="nav-bar">',
  158. ' <button name="pause" value="pause"></button>',
  159. ' <progress name="total-progress" value="0" max="1" title="点击清除已完成"></progress>',
  160. ' <button name="hide"></button>',
  161. '</div>',
  162. '<div class="task"></div>',
  163. '<div class="bottom-bar"></div>'
  164. ].join('');
  165. dialog.addEventListener('click', (e) => {
  166. // TODO
  167. const name = e.target.getAttribute('name');
  168. if (name === 'pause') {
  169. let value = e.target.getAttribute('value');
  170. if (value === 'pause') {
  171. main.pause();
  172. value = 'resume';
  173. } else {
  174. main.resume();
  175. value = 'pause';
  176. }
  177. e.target.setAttribute('value', value);
  178. } else if (name === 'hide') {
  179. main.hideDialog();
  180. } else if (name === 'total-progress') {
  181. for (const i of storage.element.dialog.querySelectorAll('.task>[status="load"]')) {
  182. i.style.display = 'none';
  183. }
  184. } else if (name === 'abort') {
  185. const index = e.target.parentNode.getAttribute('index') * 1;
  186. const task = storage.list.find(i => i.request.index === index);
  187. if (task && task.abort && typeof task.abort === 'function') task.abort();
  188. } else {
  189. // console.log(e.target, name);
  190. }
  191. });
  192. document.body.appendChild(dialog);
  193. storage.element.dialog = dialog;
  194. };
  195.  
  196. main.list = (urls, option, index = false, start = false) => {
  197. // urls: string[], option: object
  198. // urls: object[], option: undefined
  199. for (const url of urls) {
  200. const optionThis = Object.assign({}, option);
  201. let request = typeof url === 'string' ? { url: url } : Object.assign({}, url);
  202. if (!request.url) {
  203. console.error('user-download: 缺少参数url');
  204. continue;
  205. }
  206. request = Object.assign(optionThis, request);
  207. request.raw = url;
  208. request.index = storage.list.length;
  209. if (typeof index === 'number') {
  210. storage.list.splice(index, 0, { request });
  211. index++;
  212. } else {
  213. storage.list.push({ request });
  214. }
  215. }
  216. storage.element.dialog.querySelector('[name="total-progress"]').setAttribute('max', storage.list.length);
  217. if (start && !storage.downloading) main.start();
  218. };
  219. main.add = (url, option, index, start) => main.list([url], option, index, start);
  220. main.start = () => {
  221. const startTask = (task) => {
  222. task.status = 'downloading';
  223. updateProgress(task);
  224.  
  225. const request = Object.assign({}, task.request);
  226. const tryCallFailed = (res, type) => {
  227. delete task.abort;
  228. if (!navigator.onLine) {
  229. main.pause();
  230. storage.element.dialog.querySelector('.nav-bar>[name="pause"]').value = 'resume';
  231. }
  232. task.retry = typeof task.retry === 'number' && !isNaN(task.retry) ? task.retry + 1 : 1;
  233.  
  234. if (typeof task.request.onfailedEvery === 'function') {
  235. task.request.onfailedEvery(res, task.request, type);
  236. } else if (typeof storage.config.onfailedEvery === 'function') {
  237. storage.config.onfailedEvery(res, task.request, type);
  238. }
  239. if (task.retry >= storage.config.retry) {
  240. if (typeof task.request.onfailed === 'function') {
  241. task.request.onfailed(res, task.request);
  242. } else if (typeof storage.config.onfailed === 'function') {
  243. storage.config.onfailed(res, task.request);
  244. }
  245. }
  246. };
  247. request.onabort = (res) => {
  248. task.status = 'abort';
  249. if (typeof task.request.onabort === 'function') {
  250. task.request.onabort(res, task.request);
  251. } else if (typeof storage.config.onabort === 'function') {
  252. storage.config.onabort(res, task.request);
  253. }
  254. tryCallFailed(res, 'abort');
  255. updateProgress(task, res);
  256. };
  257. request.onerror = (res) => {
  258. task.status = 'error';
  259. if (typeof task.request.onerror === 'function') {
  260. task.request.onerror(res, task.request);
  261. } else if (typeof storage.config.onerror === 'function') {
  262. storage.config.onerror(res, task.request);
  263. }
  264. tryCallFailed(res, 'error');
  265. updateProgress(task, res);
  266. };
  267. request.onload = async (res) => {
  268. let success;
  269. if (typeof task.request.checkLoad === 'function') {
  270. success = await task.request.checkLoad(res);
  271. } else if (typeof storage.config.checkLoad === 'function') {
  272. success = await storage.config.checkLoad(res);
  273. }
  274. if (success === false) {
  275. request.onerror(res);
  276. return;
  277. }
  278.  
  279. task.status = 'load';
  280. task.response = res;
  281. delete task.abort;
  282. delete task.retry;
  283. if (typeof task.request.onload === 'function') {
  284. task.request.onload(res, task.request);
  285. } else if (typeof storage.config.onload === 'function') {
  286. storage.config.onload(res, task.request);
  287. }
  288. updateProgress(task, res);
  289. };
  290. request.onprogress = (res) => {
  291. if (typeof task.request.onprogress === 'function') {
  292. task.request.onprogress(res, task.request);
  293. } else if (typeof storage.config.onprogress === 'function') {
  294. storage.config.onprogress(res, task.request);
  295. }
  296. updateProgress(task, res);
  297. };
  298. request.onreadystatechange = (res) => {
  299. if (typeof task.request.onreadystatechange === 'function') {
  300. task.request.onreadystatechange(res, task.request);
  301. } else if (typeof storage.config.onreadystatechange === 'function') {
  302. storage.config.onreadystatechange(res, task.request);
  303. }
  304. updateProgress(task, res);
  305. };
  306. request.ontimeout = (res) => {
  307. task.status = 'timeout';
  308. if (typeof task.request.ontimeout === 'function') {
  309. task.request.ontimeout(res, task.request);
  310. } else if (typeof storage.config.ontimeout === 'function') {
  311. storage.config.ontimeout(res, task.request);
  312. }
  313. tryCallFailed(res, 'timeout');
  314. updateProgress(task, res);
  315. };
  316. task.abort = xhr(request).abort;
  317. };
  318. const checkDownload = () => {
  319. if (storage.pause) {
  320. storage.downloading = false;
  321. return;
  322. }
  323. while (storage.list.filter(i => i.status === 'downloading').length < storage.config.thread && storage.list.findIndex(i => i.status === undefined) >= 0) {
  324. startTask(storage.list.find(i => i.status === undefined));
  325. }
  326. if (storage.list.findIndex(i => i.status === undefined) === -1) {
  327. while (storage.list.filter(i => i.status === 'downloading').length < storage.config.thread && storage.list.findIndex(i => (i.retry || 0) < storage.config.retry && !(['downloading', 'load'].includes(i.status))) >= 0) {
  328. startTask(storage.list.find(i => (i.retry || 0) < storage.config.retry && !(['downloading', 'load'].includes(i.status))));
  329. }
  330. if (storage.list.findIndex(i => i.status !== 'load' && (i.retry || 0) < storage.config.retry) === -1) {
  331. storage.config.onComplete(storage.list);
  332. storage.downloading = false;
  333. } else {
  334. setTimeout(checkDownload, 200);
  335. }
  336. } else {
  337. setTimeout(checkDownload, 200);
  338. }
  339. };
  340. storage.downloading = true;
  341. checkDownload();
  342. };
  343. main.stop = () => {
  344. storage.pause = true;
  345. for (let i = 0; i < storage.list.length; i++) {
  346. storage.list.retry = Infinity;
  347. if (storage.list.abort) storage.list.abort();
  348. }
  349. storage.list = [];
  350. storage.pause = false;
  351. };
  352.  
  353. main.pause = () => {
  354. storage.pause = true;
  355. for (const i of storage.list.filter(i => 'abort' in i)) i.abort();
  356. };
  357. main.resume = () => {
  358. storage.pause = false;
  359. if (!storage.downloading) main.start();
  360. };
  361. main.retry = () => {
  362. for (const i of storage.list.filter(i => 'retry' in i)) storage.list[storage.list.indexOf(i)].retry = 0;
  363. if (!storage.downloading) main.start();
  364. };
  365. main.showDialog = () => {
  366. storage.element.dialog.style.display = 'block';
  367. };
  368. main.hideDialog = () => {
  369. storage.element.dialog.style.display = 'none';
  370. };
  371. main.emptyDialog = () => {
  372. storage.element.dialog.querySelectorAll('.task').innerHTML = '';
  373. };
  374. main.console = () => console.log(storage);
  375. main.storage = {
  376. get: (name, value) => name in storage ? storage[name] : value,
  377. set: (name, value) => (storage[name] = value),
  378. config: {
  379. get: (name, value) => name in storage.config ? storage.config[name] : value,
  380. set: (name, value) => (storage.config[name] = value)
  381. },
  382. getSelf: () => storage
  383. };
  384.  
  385. function xhr (url, onload, data = null, opt = {}) {
  386. if (storage.config.debug) console.log({ url, data });
  387. if (typeof url === 'object') {
  388. opt = url;
  389. url = opt.url;
  390. data = opt.data;
  391. }
  392. opt.onload = onload || opt.onload;
  393. if (opt.cache) {
  394. const str = JSON.stringify({ url, data, opt });
  395. const find = storage.cache.find(i => i[0] === str);
  396. if (find) return find[1];
  397. }
  398. if ((storage.config.mode === 'gm_xhr' || !['gm_xhr', 'fetch', 'xhr'].includes(storage.config.mode)) && typeof GM_xmlhttpRequest === 'function') {
  399. return GM_xmlhttpRequest({
  400. url: url,
  401. data: data,
  402.  
  403. method: opt.method || (data ? 'POST' : storage.config.method || 'GET'),
  404. user: opt.user || storage.config.user,
  405. password: opt.password || storage.config.password,
  406. overrideMimeType: opt.overrideMimeType || storage.config.overrideMimeType || `text/html; charset=${document.characterSet}`,
  407. headers: opt.headers || storage.config.headers,
  408. responseType: ['text', 'json', 'blob', 'arraybuffer', 'document'].includes(opt.responseType) ? opt.responseType : storage.config.responseType,
  409. timeout: opt.timeout || storage.config.timeout,
  410. anonymous: opt.anonymous || storage.config.anonymous,
  411. onabort (res) {
  412. (opt.onabort || storage.config.onabort)(res);
  413. },
  414. onerror (res) {
  415. (opt.onerror || storage.config.onerror)(res);
  416. },
  417. onload (res) {
  418. if (opt.cache) {
  419. const str = JSON.stringify({ url, data, opt });
  420. storage.cache.push([str, res]);
  421. }
  422. (opt.onload || storage.config.onload)(res);
  423. },
  424. onprogress (res) {
  425. (opt.onprogress || storage.config.onprogress)(res);
  426. },
  427. onreadystatechange (res) {
  428. (opt.onreadystatechange || storage.config.onreadystatechange)(res);
  429. },
  430. ontimeout (res) {
  431. (opt.ontimeout || storage.config.ontimeout)(res);
  432. }
  433. });
  434. }
  435. if ((storage.config.mode === 'fetch' || !['gm_xhr', 'fetch', 'xhr'].includes(storage.config.mode)) && typeof window.fetch === 'function') { // TODO
  436. // https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/fetch
  437. const controller = new window.AbortController();
  438. const signal = controller.signal;
  439. window.fetch(url, {
  440. body: data,
  441.  
  442. method: opt.method || (data ? 'POST' : storage.config.method || 'GET'),
  443. // user: opt.user || storage.config.user,
  444. // password: opt.password || storage.config.password,
  445. // overrideMimeType: opt.overrideMimeType || storage.config.overrideMimeType || `text/html; charset=${document.characterSet}`,
  446. // headers: opt.headers || storage.config.headers,
  447. // responseType: ['text', 'json', 'blob', 'arraybuffer', 'document'].includes(opt.responseType) ? opt.responseType : storage.config.responseType,
  448. // timeout: opt.timeout || storage.config.timeout,
  449. // anonymous: opt.anonymous || storage.config.anonymous,
  450.  
  451. signal
  452. }).then(function (res) {
  453. if (opt.cache) {
  454. const str = JSON.stringify({ url, data, opt });
  455. storage.cache.push([str, res]);
  456. }
  457. (opt.onload || storage.config.onload)(res);
  458. }).catch(function (res) {
  459. (opt.onerror || storage.config.onerror)(res);
  460. });
  461. return controller;
  462. }
  463. if ((storage.config.mode === 'xhr' || !['gm_xhr', 'fetch', 'xhr'].includes(storage.config.mode)) && typeof window.fetch === 'function') { // TODO
  464. // https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
  465. }
  466. }
  467. function xhrSync (url, data = null, opt = {}) {
  468. return new Promise((resolve, reject) => {
  469. const optRaw = Object.assign({}, opt);
  470. opt.onload = (res) => {
  471. (optRaw.onload || storage.config.onload)(res);
  472. resolve(res);
  473. };
  474. for (const event of ['onload', 'onabort', 'onerror', 'ontimeout']) {
  475. opt[event] = (res) => {
  476. (optRaw[event] && typeof optRaw[event] === 'function' ? optRaw[event] : storage.config[event])(res);
  477. if (['onload'].includes(event)) {
  478. resolve(res);
  479. } else {
  480. reject(res);
  481. }
  482. };
  483. }
  484. xhr(url, opt.onload, data, opt);
  485. });
  486. }
  487.  
  488. window.xhr = main;
  489. main.init();
  490. })(typeof window !== 'undefined' ? window : document);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址