download

download.lib

Bu script direkt olarak kurulamaz. Başka scriptler için bir kütüphanedir ve meta yönergeleri içerir // @require https://update.greasyfork.org/scripts/398502/980667/download.js

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

You will need to install an extension such as Tampermonkey to install this script.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

/* eslint-env browser */
// ==UserScript==
// @name        download
// @version     1.2.7
// @include     *
// ==/UserScript==
// TODO: 支持fetch,xhr
/* global GM_xmlhttpRequest */
(function (window) {
  const storageInit = {
    default: {
      debug: false,
      mode: 'gm_xhr', // one of gm_xhr,fetch,xhr
      retry: 5,
      css: [
        '#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;}',

        '#gmDownloadDialog>.nav-bar>button{width:24px;height:24px;z-index:1000001;padding:0;margin:0;}',
        '#gmDownloadDialog>.nav-bar>[name="pause"]{float:left;}',
        '#gmDownloadDialog>.nav-bar>[name="pause"][value="pause"]::before{content:"⏸️"}',
        '#gmDownloadDialog>.nav-bar>[name="pause"][value="resume"]::before{content:"▶"}',
        '#gmDownloadDialog>.nav-bar>[name="hide"]{float:right;}',
        '#gmDownloadDialog>.nav-bar>[name="hide"]::before{content:"×";color:red;}',
        '#gmDownloadDialog>.nav-bar>[name="total-progress"]{cursor:pointer;width:calc(100% - 65px);margin:4px;}',
        '#gmDownloadDialog>.nav-bar>[name="total-progress"]::before{content:attr(value)" / "attr(max);}',

        '#gmDownloadDialog>.task{overflow-x:hidden;overflow-y:auto;width:300px;height:40vh;}', // display:flex;flex-direction:column;
        '#gmDownloadDialog>.task>div{display:flex;}',
        '#gmDownloadDialog>.task>div>*{margin:0 2px;white-space:nowrap;display:inline-block;}',

        '#gmDownloadDialog>.task>div>a[name="title"]{width:206px;overflow:hidden;text-overflow:ellipsis;text-align:justify;}',
        '#gmDownloadDialog>.task>div>a[name="title"]:empty::before{content:attr(href)}',

        '#gmDownloadDialog>.task>div[status="downloading"]>progress{width:120px;display:inline-block!important;}',
        '#gmDownloadDialog>.task>div[status="downloading"]>progress::before{content:attr(value)" / "attr(max);}',

        '#gmDownloadDialog>.task>div>[name="status"]{width:32px;}',
        '#gmDownloadDialog>.task>div[status="downloading"]>[name="status"]{width:48px;}',
        '#gmDownloadDialog>.task>div[status="downloading"]>[name="status"]::before{content:"下载中";color:#00f;}',
        '#gmDownloadDialog>.task>div[status="error"]>[name="status"]::before{content:"错误";color:#f00;}',
        '#gmDownloadDialog>.task>div[status="timeout"]>[name="status"]::before{content:"超时";color:#f00;}',
        '#gmDownloadDialog>.task>div[status="abort"]>[name="status"]::before{content:"取消";color:#f00;}',
        '#gmDownloadDialog>.task>div[status="load"]>[name="status"]::before{content:"完成";color:#0f0;}',

        '#gmDownloadDialog>.task>div[status="downloading"]>[name="abort"]{width:32px;cursor:pointer;}',
        '#gmDownloadDialog>.task>div[status="downloading"]>[name="abort"]::before{content:"abort";color:#f00;}',
      ].join(''),
      progress: '{order}{title}{progress}{status}{abort}',
      thread: 5,
      onComplete(list) { }, // 当list任务全部完成时(不管是否有下载错误)
      onfailed(res, request) { }, // 当某次请求失败(error/timeout)超过重复次数(之后不再尝试请求)
      onfailedEvery(res, request, type) { }, // 当某次请求失败(error/timeout)
      async checkLoad(res) {}, // 返回布尔,当false时,执行onerror并再次请求

      method: 'GET',
      user: null,
      password: null,
      overrideMimeType: null,
      headers: {
        // 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      },
      responseType: 'text',
      timeout: null,
      anonymous: false,
      onabort(res, request) { },
      onerror(res, request) { },
      onload(res, request) { },
      onprogress(res, request) { },
      onreadystatechange(res, request) { },
      ontimeout(res, request) { },
    },
    list: [
      // request 请求信息
      // status 状态 undefined,downloading,error,timeout,abort,load
      // retry 重复请求次数
      // abort 终止请求
      // response
    ],
    pause: false,
    downloading: false,
    element: {},
    cache: [],
  };

  let storage = { ...JSON.parse(JSON.stringify(storageInit)) };

  const updateProgress = (task, res = {}) => {
    let elem;
    let max = res.lengthComputable ? res.total : 1;
    let value = res.statusText === 'OK' ? max : res.lengthComputable ? res.loaded : 0;
    if (max !== 1 && value !== 0) {
      value = Math.floor(value / max * 100);
      max = 100;
    }
    if (storage.element.dialog.querySelector(`.task>[index="${task.request.index}"]`)) {
      elem = storage.element.dialog.querySelector(`.task>[index="${task.request.index}"]`);
      if (res.lengthComputable) {
        elem.querySelector('progress').setAttribute('value', value);
        elem.querySelector('progress').setAttribute('max', max);
      }
      if (task.request.title) {
        elem.querySelector('[name="title"]').textContent = task.request.title;
      } else if (res.statusText === 'OK' && !elem.querySelector('[name="title"]').textContent) {
        let dom;
        if (typeof res.response === 'string') {
          dom = new window.DOMParser().parseFromString(res.response, 'text/html');
        } else if (res.response instanceof window.Document) {
          dom = res.response;
        }
        if (dom instanceof window.Document) elem.querySelector('[name="title"]').textContent = dom.title;
      }
    } else {
      elem = document.createElement('div');
      elem.setAttribute('index', task.request.index);
      elem.innerHTML = storage.config.progress.replace(/\{(.*?)\}/g, (all, $1) => {
        if ($1 === 'order') {
          return `<span>${task.request.index + 1}</span>`;
        } if ($1 === 'title') {
          const title = task.request.title || '';
          return `<a name="title" href="${task.request.url}" target="_blank">${title}</a>`;
        } if ($1 === 'progress') {
          return `<progress value="${value}" max="${max}" style="display:none;"></progress>`;
        } if ($1 === 'status') {
          return '<span name="status"></span>';
        } if ($1 === 'abort') {
          return '<a name="abort"></a>';
        }
        return '';
      });
      storage.element.dialog.querySelector('.task').appendChild(elem);
    }
    elem.setAttribute('status', task.status);
    elem.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
    storage.element.dialog.querySelector('[name="total-progress"]').setAttribute('value', storage.list.filter((i) => i.status && i.status !== 'downloading').length);
  };

  const main = xhr;
  main.sync = xhrSync;
  main.init = (option) => {
    main.stop();
    for (const elem of Object.values(storage.element)) if (elem.parentNode) elem.parentNode.removeChild(elem);
    storage = { ...JSON.parse(JSON.stringify(storageInit)) };
    storage.config = Object.assign(storage.default, option);
    for (const listener of ['onComplete', 'onfailed', 'onfailedEvery', 'checkLoad',
      'onabort', 'onerror', 'onload', 'onprogress', 'onreadystatechange', 'ontimeout']) {
      if (typeof storage.config[listener] !== 'function') storage.config[listener] = function () {};
    }

    const style = document.createElement('style');
    style.id = 'gmDownloadStyle';
    style.textContent = storage.config.css;
    document.head.appendChild(style);
    storage.element.style = style;

    if (document.getElementById('gmDownloadDialog')) document.getElementById('gmDownloadDialog').parentElement.removeChild(document.getElementById('gmDownloadDialog'));
    const dialog = document.createElement('div');
    dialog.id = 'gmDownloadDialog';
    dialog.innerHTML = [
      '<div class="nav-bar">',
      '  <button name="pause" value="pause"></button>',
      '  <progress name="total-progress" value="0" max="1" title="点击清除已完成"></progress>',
      '  <button name="hide"></button>',
      '</div>',
      '<div class="task"></div>',
      '<div class="bottom-bar"></div>',
    ].join('');
    dialog.addEventListener('click', (e) => {
      // TODO
      const name = e.target.getAttribute('name');
      if (name === 'pause') {
        let value = e.target.getAttribute('value');
        if (value === 'pause') {
          main.pause();
          value = 'resume';
        } else {
          main.resume();
          value = 'pause';
        }
        e.target.setAttribute('value', value);
      } else if (name === 'hide') {
        main.hideDialog();
      } else if (name === 'total-progress') {
        for (const i of storage.element.dialog.querySelectorAll('.task>[status="load"]')) {
          i.style.display = 'none';
        }
      } else if (name === 'abort') {
        const index = e.target.parentNode.getAttribute('index') * 1;
        const task = storage.list.find((i) => i.request.index === index);
        if (task && task.abort && typeof task.abort === 'function') task.abort();
      } else {
        // console.log(e.target, name);
      }
    });
    storage.element.dialog = dialog;
  };

  main.list = (urls, option, index = false, start = false) => {
    // urls: string[], option: object
    // urls: object[], option: undefined
    for (const url of urls) {
      const optionThis = { ...option };
      let request = typeof url === 'string' ? { url } : ({ ...url });
      if (!request.url) {
        console.error('user-download: 缺少参数url');
        continue;
      }
      request = Object.assign(optionThis, request);
      request.raw = url;
      request.index = storage.list.length;
      if (typeof index === 'number') {
        storage.list.splice(index, 0, { request });
        index++;
      } else {
        storage.list.push({ request });
      }
    }
    storage.element.dialog.querySelector('[name="total-progress"]').setAttribute('max', storage.list.length);
    if (start && !storage.downloading) main.start();
  };
  main.add = (url, option, index, start) => main.list([url], option, index, start);
  main.start = () => {
    const startTask = (task) => {
      task.status = 'downloading';
      updateProgress(task);

      const request = { ...task.request };
      const tryCallFailed = (res, type) => {
        delete task.abort;
        if (!navigator.onLine) {
          main.pause();
          storage.element.dialog.querySelector('.nav-bar>[name="pause"]').value = 'resume';
        }
        task.retry = typeof task.retry === 'number' && !isNaN(task.retry) ? task.retry + 1 : 1;

        if (typeof task.request.onfailedEvery === 'function') {
          task.request.onfailedEvery(res, task.request, type);
        } else if (typeof storage.config.onfailedEvery === 'function') {
          storage.config.onfailedEvery(res, task.request, type);
        }
        if (task.retry >= storage.config.retry) {
          if (typeof task.request.onfailed === 'function') {
            task.request.onfailed(res, task.request);
          } else if (typeof storage.config.onfailed === 'function') {
            storage.config.onfailed(res, task.request);
          }
        }
      };
      request.onabort = (res) => {
        task.status = 'abort';
        if (typeof task.request.onabort === 'function') {
          task.request.onabort(res, task.request);
        } else if (typeof storage.config.onabort === 'function') {
          storage.config.onabort(res, task.request);
        }
        tryCallFailed(res, 'abort');
        updateProgress(task, res);
      };
      request.onerror = (res) => {
        task.status = 'error';
        if (typeof task.request.onerror === 'function') {
          task.request.onerror(res, task.request);
        } else if (typeof storage.config.onerror === 'function') {
          storage.config.onerror(res, task.request);
        }
        tryCallFailed(res, 'error');
        updateProgress(task, res);
      };
      request.onload = async (res) => {
        let success;
        if (typeof task.request.checkLoad === 'function') {
          success = await task.request.checkLoad(res);
        } else if (typeof storage.config.checkLoad === 'function') {
          success = await storage.config.checkLoad(res);
        }
        if (success === false) {
          request.onerror(res);
          return;
        }

        task.status = 'load';
        task.response = res;
        delete task.abort;
        delete task.retry;
        const resNew = { ...res }; // FIX Violentmonkey
        for (const i of ['response', 'responseText', 'responseXML']) { // FIX Tamermonkey
          try {
            resNew[i] = Object.getOwnPropertyDescriptor(res, i).value || Object.getOwnPropertyDescriptor(res, i).get();
          } catch (error) {
            console.log(error);
          }
        }
        res = resNew;
        if (!request.responseType || request.responseType === 'text') {
          res.response = res.responseText = res.responseText || res.response;
        } else if (request.responseType === 'document') {
          res.response = res.responseXML = res.responseXML || res.response;
        } else if (request.responseType === 'json') {
          try {
            res.response = res.json;
          } catch (error) {}
        }
        if (typeof task.request.onload === 'function') {
          task.request.onload(res, task.request);
        } else if (typeof storage.config.onload === 'function') {
          storage.config.onload(res, task.request);
        }
        updateProgress(task, res);
      };
      request.onprogress = (res) => {
        if (typeof task.request.onprogress === 'function') {
          task.request.onprogress(res, task.request);
        } else if (typeof storage.config.onprogress === 'function') {
          storage.config.onprogress(res, task.request);
        }
        updateProgress(task, res);
      };
      request.onreadystatechange = (res) => {
        if (typeof task.request.onreadystatechange === 'function') {
          task.request.onreadystatechange(res, task.request);
        } else if (typeof storage.config.onreadystatechange === 'function') {
          storage.config.onreadystatechange(res, task.request);
        }
        updateProgress(task, res);
      };
      request.ontimeout = (res) => {
        task.status = 'timeout';
        if (typeof task.request.ontimeout === 'function') {
          task.request.ontimeout(res, task.request);
        } else if (typeof storage.config.ontimeout === 'function') {
          storage.config.ontimeout(res, task.request);
        }
        tryCallFailed(res, 'timeout');
        updateProgress(task, res);
      };
      task.abort = xhr(request).abort;
    };
    const checkDownload = () => {
      if (storage.pause) {
        storage.downloading = false;
        return;
      }
      while (storage.list.filter((i) => i.status === 'downloading').length < storage.config.thread && storage.list.findIndex((i) => i.status === undefined) >= 0) {
        startTask(storage.list.find((i) => i.status === undefined));
      }
      if (storage.list.findIndex((i) => i.status === undefined) === -1) {
        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) {
          startTask(storage.list.find((i) => (i.retry || 0) < storage.config.retry && !(['downloading', 'load'].includes(i.status))));
        }
        if (storage.list.findIndex((i) => i.status !== 'load' && (i.retry || 0) < storage.config.retry) === -1) {
          storage.config.onComplete(storage.list);
          storage.downloading = false;
        } else {
          setTimeout(checkDownload, 200);
        }
      } else {
        setTimeout(checkDownload, 200);
      }
    };
    storage.downloading = true;
    checkDownload();

    if (!document.getElementById('gmDownloadDialog')) document.body.appendChild(storage.element.dialog);
  };
  main.stop = () => {
    storage.pause = true;
    for (let i = 0; i < storage.list.length; i++) {
      storage.list.retry = Infinity;
      if (storage.list.abort) storage.list.abort();
    }
    storage.list = [];
    storage.pause = false;
  };

  main.pause = () => {
    storage.pause = true;
    for (const i of storage.list.filter((i) => 'abort' in i)) i.abort();
  };
  main.resume = () => {
    storage.pause = false;
    if (!storage.downloading) main.start();
  };
  main.retry = () => {
    for (const i of storage.list.filter((i) => 'retry' in i)) storage.list[storage.list.indexOf(i)].retry = 0;
    if (!storage.downloading) main.start();
  };
  main.showDialog = () => {
    storage.element.dialog.style.display = 'block';
  };
  main.hideDialog = () => {
    storage.element.dialog.style.display = 'none';
  };
  main.emptyDialog = () => {
    storage.element.dialog.querySelectorAll('.task').innerHTML = '';
  };
  main.console = () => console.log(storage);
  main.storage = {
    get: (name, value) => (name in storage ? storage[name] : value),
    set: (name, value) => (storage[name] = value),
    config: {
      get: (name, value) => (name in storage.config ? storage.config[name] : value),
      set: (name, value) => (storage.config[name] = value),
    },
    getSelf: () => storage,
  };

  function xhr(url, onload, data = null, opt = {}) {
    if (storage.config.debug) console.log({ url, data });
    if (typeof url === 'object') {
      opt = url;
      url = opt.url;
      data = opt.data;
    }
    opt.onload = onload || opt.onload;
    if (opt.cache) {
      const str = JSON.stringify({ url, data, opt });
      const find = storage.cache.find((i) => i[0] === str);
      if (find) return find[1];
    }
    if ((storage.config.mode === 'gm_xhr' || !['gm_xhr', 'fetch', 'xhr'].includes(storage.config.mode)) && typeof GM_xmlhttpRequest === 'function') { // eslint-disable-line camelcase
      return GM_xmlhttpRequest({
        url,
        data,

        method: opt.method || (data ? 'POST' : storage.config.method || 'GET'),
        user: opt.user || storage.config.user,
        password: opt.password || storage.config.password,
        overrideMimeType: opt.overrideMimeType || storage.config.overrideMimeType || `text/html; charset=${document.characterSet}`,
        headers: opt.headers || storage.config.headers,
        responseType: ['text', 'json', 'blob', 'arraybuffer', 'document'].includes(opt.responseType) ? opt.responseType : storage.config.responseType,
        timeout: opt.timeout || storage.config.timeout,
        anonymous: opt.anonymous || storage.config.anonymous,
        onabort(res) {
          (opt.onabort || storage.config.onabort)(res);
        },
        onerror(res) {
          (opt.onerror || storage.config.onerror)(res);
        },
        onload(res) {
          if (opt.cache) {
            const str = JSON.stringify({ url, data, opt });
            storage.cache.push([str, res]);
          }
          (opt.onload || storage.config.onload)(res);
        },
        onprogress(res) {
          (opt.onprogress || storage.config.onprogress)(res);
        },
        onreadystatechange(res) {
          (opt.onreadystatechange || storage.config.onreadystatechange)(res);
        },
        ontimeout(res) {
          (opt.ontimeout || storage.config.ontimeout)(res);
        },
      });
    }
    if ((storage.config.mode === 'fetch' || !['gm_xhr', 'fetch', 'xhr'].includes(storage.config.mode)) && typeof window.fetch === 'function') { // TODO
      // https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/fetch
      const controller = new window.AbortController();
      const { signal } = controller;
      window.fetch(url, {
        body: data,

        method: opt.method || (data ? 'POST' : storage.config.method || 'GET'),
        // user: opt.user || storage.config.user,
        // password: opt.password || storage.config.password,
        // overrideMimeType: opt.overrideMimeType || storage.config.overrideMimeType || `text/html; charset=${document.characterSet}`,
        // headers: opt.headers || storage.config.headers,
        // responseType: ['text', 'json', 'blob', 'arraybuffer', 'document'].includes(opt.responseType) ? opt.responseType : storage.config.responseType,
        // timeout: opt.timeout || storage.config.timeout,
        // anonymous: opt.anonymous || storage.config.anonymous,

        signal,
      }).then((res) => {
        if (opt.cache) {
          const str = JSON.stringify({ url, data, opt });
          storage.cache.push([str, res]);
        }
        (opt.onload || storage.config.onload)(res);
      }).catch((res) => {
        (opt.onerror || storage.config.onerror)(res);
      });
      return controller;
    }
    if ((storage.config.mode === 'xhr' || !['gm_xhr', 'fetch', 'xhr'].includes(storage.config.mode)) && typeof window.fetch === 'function') { // TODO
      // https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
    }
  }
  function xhrSync(url, data = null, opt = {}) {
    return new Promise((resolve, reject) => {
      const optRaw = { ...opt };
      opt.onload = (res) => {
        (optRaw.onload || storage.config.onload)(res);
        resolve(res);
      };
      for (const event of ['onload', 'onabort', 'onerror', 'ontimeout']) {
        opt[event] = (res) => {
          (optRaw[event] && typeof optRaw[event] === 'function' ? optRaw[event] : storage.config[event])(res);
          if (['onload'].includes(event)) {
            resolve(res);
          } else {
            reject(res);
          }
        };
      }
      xhr(url, opt.onload, data, opt);
    });
  }

  window.xhr = main;
  main.init();
}(typeof window !== 'undefined' ? window : document));