Greasy Fork 支持简体中文。

download

download.lib

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/398502/980667/download.js

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