Pixiv Downloader

按画师名划分文件夹下载Pixiv原图,支持多图Zip/Gif。

Versión del día 9/9/2021. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Pixiv Downloader
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  按画师名划分文件夹下载Pixiv原图,支持多图Zip/Gif。
// @author       ruaruarua
// @match        https://www.pixiv.net/*
// @icon         https://www.google.com/s2/favicons?domain=pixiv.net
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @connect      i.pximg.net
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js
// ==/UserScript==

(async function () {
  const workerStr = await fetch('https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js')
  .then(res=>res.blob());
  const gifConfig = {
    workers: 3,
    quality: 5,
    workerScript: URL.createObjectURL(workerStr)
  };
  const GIF_FLAG = false;

  class PixivDownloader {
    constructor(pixivID) {
      this.pixivID = pixivID;
      this.artworksURL = 'https://www.pixiv.net/artworks/' + pixivID;
      this.data = {};
      this.author = '';
      this.illustTitle = '';
      this.illustType = -1;  
      this.authorFormatted = '';
      this.illustTitleFormatted = '';
      this.pageCount = 0;
      this.imgList = [];
      this.gifSrc = '';
      this.gifSrcInfo = {};
    }

    _toGIF(blob, self, path, resolve, reject) {
      let gifPromiseList = [];
      let imgList = [];
      zip.folder(`${self.pixivID}`)
        .loadAsync(blob)
        .then((zip) => {
          let gif = new GIF(gifConfig);
          let i = 0;
          zip.forEach((relativePath, file) => {
            gifPromiseList[i] = new Promise((loadResolve, loadReject) => {
              i += 1;
              const image = new Image();
              imgList.push(image);
              image.onload = () => {
                loadResolve();
              };
              file.async('blob')
                .then((blob) => {
                  const objectURL = URL.createObjectURL(blob);
                  image.src = objectURL;
                });
            });
          });
          Promise.all(gifPromiseList)
            .then(() => {
              zip.remove(`${self.pixivID}`);
              imgList.forEach((e, i) => {
                gif.addFrame(e, { delay: self.gifSrcInfo.frames[i].delay });
              });
              gif.on('finished', (blob) => {
                self._save(blob, path, resolve, reject);
              });
              gif.render();
            });
        })
    }

    _save(blob, path, resolve, reject) {
      const imgUrl = URL.createObjectURL(blob);
      GM_download({
        url: imgUrl,
        name: path,
        onerror: (error) => {
          console.log(error);
          if (reject) {
            reject();
          }
        },
        onload: () => {
          console.log('Download complete: ' + path);
          URL.revokeObjectURL(imgUrl);
          if (resolve) {
            resolve();
          }
        }
      });
    }

    startDownload(path = '', dlSrc = '', onprogressCallback = null, resolve = null, reject = null) {
      let self = this;
      GM_xmlhttpRequest({
        url: dlSrc,
        method: 'GET',
        headers: {
          referer: 'https://www.pixiv.net'
        },
        responseType: 'blob',
        onprogress: (e) => {
          if (typeof onprogressCallback == 'function') {
            onprogressCallback(e);
          }
        },
        onload: function (res) {
          if (path.slice(-3) != 'gif') {
            self._save(res.response, path, resolve, reject);
          } else {
            if (typeof onprogressCallback == 'function') {
              onprogressCallback(res);
            }
            self._toGIF(res.response, self, path, resolve, reject);
          }
        },
        onerror: function () {
          console.log('XmlhttpRequest failed: ' + dlSrc);
          if (reject) {
            reject();
          }
        }
      });
    }

    _replaceInvalidChar(picInfo = '') {
      if (picInfo) {
        let newpicInfo = '';
        for (let i = 0; i < picInfo.length; i++) {
          const char = picInfo.charAt(i);
          switch (char) {
            case '\\':
            case '/':
            case ':':
            case '?':
            case '|':
              newpicInfo += '-';
              break;
            case '"':
              newpicInfo += "'";
              break;
            case '<':
              newpicInfo += "[";
              break;
            case '>':
              newpicInfo += "]";
              break;
            default:
              newpicInfo += char;
          }
        }
        return newpicInfo;
      }
    }

    async _initProps(htmlText) {
      this.data = JSON.parse(htmlText.match(/"meta-preload-data" content='(.*)'>/)[1]);
      if (this.data.illust) {
        this.author = this.data.illust[this.pixivID].userName;
        this.illustTitle = this.data.illust[this.pixivID].illustTitle;
        this.authorFormatted = this._replaceInvalidChar(this.author);
        this.illustTitleFormatted = this._replaceInvalidChar(this.illustTitle);
        this.illustType = this.data.illust[this.pixivID].illustType;

        if (this.illustType == 0 || this.illustType == 1) {
          this.pageCount = this.data.illust[this.pixivID].pageCount;
          let firstimgSrc = this.data.illust[this.pixivID].urls.original;
          let baseSrc = {
            srcPrefix: firstimgSrc.slice(0, firstimgSrc.indexOf('_') + 2),
            srcSuffix: firstimgSrc.slice(-4)
          };
          for (let i = 0; i < this.pageCount; i++) {
            const imgSrc = baseSrc.srcPrefix + i + baseSrc.srcSuffix;
            this.imgList.push(imgSrc);
          }
          return this;
        }
        if (this.illustType == 2) {
          const metaURL = `https://www.pixiv.net/ajax/illust/${this.pixivID}/ugoira_meta?lang=zh`;
          this.gifSrcInfo = await fetch(metaURL)
            .then(res => res.json())
            .then(res => res.body)
            .catch(() => '');
          if (/img-zip-ugoira/.test(this.gifSrcInfo.originalSrc)) {
            return this;
          } else {
            throw new Error('Fail to parse gif src.');
          }
        }
      }
      throw new Error('Fail to parse html.');
    }

    fetchData() {
      return fetch(this.artworksURL)
        .then(res => res.text())
        .then(htmlText => this._initProps(htmlText))
    }

    download(onprogressCallback) {
      if (this.illustType == 0 || this.illustType == 1) {
        let promiseList = [];
        if (this.imgList.length > 0) {
          this.imgList.forEach((imgSrc, i) => {
            promiseList[i] = new Promise((resolve, reject) => {
              const path = this.authorFormatted + '/' + this.authorFormatted + '_' + this.illustTitleFormatted + '_' + this.pixivID + '_p' + i + imgSrc.slice(-4);
              this.startDownload(path, imgSrc, onprogressCallback, resolve, reject);
            });
          })
        }
        return Promise.all(promiseList);
      }

      if (this.illustType == 2) {
        return new Promise((resolve, reject) => {
          let path = this.authorFormatted + '/' + this.authorFormatted + '_' + this.illustTitleFormatted + '_' + this.pixivID;
          if (GIF_FLAG) {
            path += '.gif';
          } else {
            path += '.zip';
          }
          this.startDownload(path, this.gifSrcInfo.originalSrc, onprogressCallback, resolve, reject);
        });
      }
    }
  }

  function isLinkMatch(link = null) {
    const artworkReg = /artworks\/([0-9]+)$/;
    const isHrefMatch = artworkReg.exec(link.href);
    if (isHrefMatch) {
      if (link.getAttribute('data-gtm-value') || link.classList.contains('gtm-illust-recommend-thumbnail-link') || link.classList.contains('gtm-discover-user-recommend-thumbnail') || link.classList.contains('work')) {
        return isHrefMatch[1];
      }
      if (window.location.href.indexOf('bookmark_new_illust') != -1) {
        return isHrefMatch[1];
      }
    } else {
      const activityReg = /illust_id=([0-9]+)/;
      const isActivityMatch = activityReg.exec(link.href);
      if (isActivityMatch && link.classList.contains('work')) {
        return isActivityMatch[1];
      }
    }
    return false;
  }

  function createDlBtn(isMain = false, pixivID = 0) {
    const dlBtn = document.createElement('a');
    dlBtn.setAttribute('href', 'javascript:void(0)');
    dlBtn.setAttribute('pixivID', pixivID);
    dlBtn.classList.add('dl-btn');
    if (!isMain) {
      dlBtn.classList.add('dl-sub-pic-btn');
      dlBtn.setAttribute('style', 'position:absolute; left:0; bottom:0; z-index:1; display:inline-block; width:32px; height:32px; margin:0; padding:0; cursor:pointer; background-color: rgba(255,255,255,0.5);');
    } else {
      dlBtn.classList.add('dl-pic-btn');
      dlBtn.setAttribute('style', 'display:inline-block; width:32px; height:32px; margin:0; padding:0; margin-left:10px; cursor:pointer; background-color: transparent;');
    }
    if (pixivStorage.has(pixivID)) {
      dlBtn.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1024 1024\"><path fill=\"green\" d=\"M406.656 706.944 195.84 496.256a32 32 0 1 0-45.248 45.248l256 256 512-512a32 32 0 0 0-45.248-45.248L406.592 706.944z\"></path></svg>"
    } else {
      dlBtn.innerHTML = '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1024 1024\"><path d=\"M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64zm384-253.696 236.288-236.352 45.248 45.248L508.8 704 192 387.2l45.248-45.248L480 584.704V128h64v450.304z\"></path></svg>'
    }
    return dlBtn;
  }

  function keyboardHandler(e) {
    if (e.ctrlKey && e.key == 'd') {
      const dlBtn = rootNode.querySelector('.dl-pic-btn');
      if (dlBtn) {
        e.preventDefault();
        if (!e.repeat) {
          dlBtn.firstElementChild.dispatchEvent(new MouseEvent('click', { "bubbles": true }));
        }
      }
    }
  }

  function handleDownload(dlBtn = null, pixivID = '') {
    function onprogressCallback(e) {
      if (e.loaded) {
        dlBtn.innerHTML = '<span style="line-height: 32px; font-size: 15px; color: green;">' + Math.round((e.loaded / e.total) * 100) + '</span>';
      } else {
        dlBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="green" d="M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64H224zm0-64h576a128 128 0 0 1 128 128v576a128 128 0 0 1-128 128H224A128 128 0 0 1 96 800V224A128 128 0 0 1 224 96z"></path><path fill="green" d="M384 416a64 64 0 1 0 0-128 64 64 0 0 0 0 128zm0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256z"></path><path fill="green" d="M480 320h256q32 0 32 32t-32 32H480q-32 0-32-32t32-32zm160 416a64 64 0 1 0 0-128 64 64 0 0 0 0 128zm0 64a128 128 0 1 1 0-256 128 128 0 0 1 0 256z"></path><path fill="green" d="M288 640h256q32 0 32 32t-32 32H288q-32 0-32-32t32-32z"></path></svg>';
      }
    }
    new PixivDownloader(pixivID)
      .fetchData()
      .then(downloader => downloader.download(onprogressCallback))
      .then(() => {
        dlBtn.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1024 1024\"><path fill=\"green\" d=\"M406.656 706.944 195.84 496.256a32 32 0 1 0-45.248 45.248l256 256 512-512a32 32 0 0 0-45.248-45.248L406.592 706.944z\"></path></svg>";
        pixivStorage.add(pixivID);
        localStorage.pixivDownloader = JSON.stringify([...pixivStorage]);
      })
      .catch((err) => {
        dlBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="orange" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 832a384 384 0 0 0 0-768 384 384 0 0 0 0 768zm48-176a48 48 0 1 1-96 0 48 48 0 0 1 96 0zm-48-464a32 32 0 0 1 32 32v288a32 32 0 0 1-64 0V288a32 32 0 0 1 32-32z"></path></svg>';
        if (err) {
          console.log(err);
        }
      });
  }

  function observerCallback() {
    const regMatch = /artworks\/([0-9]+)$/;
    const isArtworksPage = regMatch.exec(window.location.href);

    if (isArtworksPage && !document.body.querySelector('.dl-pic-btn')) {
      const addFavousBtn = document.body.querySelector('.gtm-main-bookmark') || null;
      if (addFavousBtn) {
        const handleBar = addFavousBtn.parentElement.parentElement;
        const dlBtnWrap = addFavousBtn.parentElement.cloneNode();
        dlBtnWrap.appendChild(createDlBtn(true, isArtworksPage[1]));
        handleBar.appendChild(dlBtnWrap);
        document.addEventListener('keydown', keyboardHandler);
      }
    }

    const picLists = rootNode.querySelectorAll('a');
    picLists.forEach((link) => {
      const pixivID = isLinkMatch(link);
      if (pixivID && !link.querySelector('.dl-sub-pic-btn')) {
        link.appendChild(createDlBtn(false, pixivID));
      }
    })

  }

  localStorage.pixivDownloader = localStorage.pixivDownloader || JSON.stringify([]);
  let pixivStorage = new Set(JSON.parse(localStorage.pixivDownloader));
  const rootNode = document.body;
  const config = { attributes: false, childList: true, subtree: true };
  const rootObserver = new MutationObserver(observerCallback);
  rootObserver.observe(rootNode, config);

  rootNode.addEventListener('click', (e) => {
    if (/svg|path/i.test(e.target.tagName)) {
      let findDlBtn = e.target.closest('.dl-btn');
      if (findDlBtn) {
        const pixivID = findDlBtn.getAttribute('PixivID');
        e.stopPropagation();
        handleDownload(findDlBtn, pixivID);
      }
    }
  });

  let zip = new JSZip();

})();