Pixiv Downloader

一键(快捷键)下载Pixiv各页面原图。支持多图下载,动图下载,画师作品批量下载。动图支持格式转换:Gif | Apng | Webm。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。

Version vom 14.06.2022. Aktuellste Version

// ==UserScript==
// @name         Pixiv Downloader
// @namespace    https://greasyfork.org/zh-CN/scripts/432150
// @version      0.4.2
// @description  一键(快捷键)下载Pixiv各页面原图。支持多图下载,动图下载,画师作品批量下载。动图支持格式转换:Gif | Apng | Webm。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。
// @author       ruaruarua
// @match        https://www.pixiv.net/*
// @icon         https://www.pixiv.net/favicon.ico
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @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 () {
  let convertFormat = localStorage.pdlFormat || (localStorage.pdlFormat = 'zip');
  let filenamePattern = localStorage.pdlFilename || (localStorage.pdlFilename = '{author}_{title}_{id}_p{page}');
  let gifWS, apngWS;
  if (!(gifWS = await GM_getValue('gifWS'))) {
    gifWS = await fetch('https://raw.githubusercontent.com/jnordberg/gif.js/master/dist/gif.worker.js').then((res) => res.blob()).then((blob) => blob.text());
    GM_setValue('gifWS', gifWS);
  }
  if (!(apngWS = await GM_getValue('apngWS'))) {
    const pako = await fetch('https://cdnjs.cloudflare.com/ajax/libs/pako/2.0.4/pako.min.js').then((res) => res.text());
    const upng = await fetch('https://cdnjs.cloudflare.com/ajax/libs/upng-js/2.1.0/UPNG.min.js').then((res) => res.text()).then((js) => js.replace('window.UPNG', 'UPNG').replace('window.pako', 'pako'));
    const workerEvt = `onmessage = (evt)=>{
      const {data, width, height, delay } = evt.data;
      const png = UPNG.encode(data, width, height, 0, delay, {loop: 0});
      if (!png) console.log('Convert Apng failed.');
      postMessage(png);
    };`;
    apngWS = workerEvt + pako + upng;
    GM_setValue('apngWS', apngWS);
  }
  gifWS = URL.createObjectURL(new Blob([gifWS], { type: 'text/javascript' }));
  apngWS = URL.createObjectURL(new Blob([apngWS], { type: 'text/javascript' }));
  const converter = (() => {
    const zip = new JSZip();
    const gifConfig = {
      workers: 2,
      quality: 10,
      workerScript: gifWS,
    };
    const freeApngWorkers = [];
    const apngWorkers = [];
    const MAX_CONVERT = 2;
    let queue = [];
    let active = [];
    let isStop = false;
    const convert = (convertMeta) => {
      const { blob, meta, callBack } = convertMeta;
      meta.abort = () => void (meta.state = 0);
      active.push(convertMeta);
      meta.onProgress && meta.onProgress(0, 'zip');
      zip.folder(meta.id)
        .loadAsync(blob)
        .then((zip) => {
          const promises = [];
          zip.forEach((relativePath, file) => {
            promises.push(new Promise((resolve) => {
              const image = new Image();
              image.onload = () => {
                resolve(image);
              };
              file.async('blob')
                .then((blob) => void (image.src = URL.createObjectURL(blob)));
            }));
          });
          return Promise.all(promises);
        })
        .then((imgList) => {
          zip.remove(meta.id);
          if (!meta.state) throw 'Convert stop manually, reject when unzip. ' + meta.id;
          if (convertFormat === 'zip') throw 'no need to convert';
          return convert2[convertFormat](imgList, meta, callBack);
        })
        .catch((err) => {
          meta.reject('Error when converting. ' + err);
        })
        .finally(() => {
          active.splice(active.indexOf(convertMeta), 1);
          if (queue.length) convert(queue.shift());
        });
    };
    const toGif = (frames, meta, callBack) => {
      return new Promise((resolve, reject) => {
        let gif = new GIF(gifConfig);
        meta.abort = () => {
          meta.state = 0;
          gif.abort();
          reject('Convert stop manually, reject when convert gif. ' + meta.id);
        };
        debugLog('Convert:', meta.path);
        frames.forEach((frame, i) => {
          gif.addFrame(frame, { delay: meta.ugoiraMeta.frames[i].delay });
        });
        gif.on('progress', (() => {
          const type = 'gif';
          return (progress) => {
            debugLog('Convert progress:', meta.path);
            meta.onProgress && meta.onProgress(progress, type);
          };
        })());
        gif.on('finished', (gifBlob) => {
          if (typeof callBack === 'function') callBack(gifBlob, meta);
          frames.forEach((frame) => {
            URL.revokeObjectURL(frame.src);
          });
          gif = null;
          resolve();
        });
        gif.on('abort', () => {
          gif = null;
        });
        gif.render();
      });
    };
    const toApng = (frames, meta, callBack) => {
      return new Promise((resolve, reject) => {
        let canvas = document.createElement('canvas');
        const width = canvas.width = frames[0].naturalWidth;
        const height = canvas.height = frames[0].naturalHeight;
        const context = canvas.getContext('2d');
        const data = [];
        const delay = meta.ugoiraMeta.frames.map((frame) => {
          return Number(frame.delay);
        });
        frames.forEach((frame) => {
          if (!meta.state) throw 'Convert stop manually, reject when drawImage. ' + meta.id;
          context.clearRect(0, 0, width, height);
          context.drawImage(frame, 0, 0, width, height);
          data.push(context.getImageData(0, 0, width, height).data);
        });
        canvas = null;
        debugLog('Convert:', meta.path);
        let worker;
        if (apngWorkers.length === MAX_CONVERT) {
          worker = freeApngWorkers.shift();
        } else {
          worker = new Worker(apngWS);
          apngWorkers.push(worker);
        }
        meta.abort = () => {
          meta.state = 0;
          reject('Convert stop manually, reject when convert apng. ' + meta.id);
          worker.terminate();
          apngWorkers.splice(apngWorkers.indexOf(worker), 1);
        };
        worker.onmessage = function (e) {
          if (queue.length) {
            freeApngWorkers.push(worker);
          } else {
            worker.terminate();
            apngWorkers.splice(apngWorkers.indexOf(worker), 1);
          }
          if (!e.data) {
            return reject('apng data is null. ' + meta.id);
          }
          const pngBlob = new Blob([e.data], { type: 'image/png' });
          if (typeof callBack === 'function') callBack(pngBlob, meta);
          resolve();
        };
        const cfg = { data, width, height, delay };
        worker.postMessage(cfg);
      })
    };
    const toWebm = (frames, meta, callBack) => {
      return new Promise((resolve, reject) => {
        let canvas = document.createElement('canvas');
        const width = canvas.width = frames[0].naturalWidth;
        const height = canvas.height = frames[0].naturalHeight;
        const context = canvas.getContext('2d');
        const stream = canvas.captureStream();
        const recorder = new MediaRecorder(stream, { mimeType: 'video/webm', videoBitsPerSecond: 80000000 });
        const delay = meta.ugoiraMeta.frames.map((frame) => {
          return Number(frame.delay);
        });
        let data = [];
        let frame = 0;
        const displayFrame = () => {
          context.clearRect(0, 0, width, height);
          context.drawImage(frames[frame], 0, 0);
          if (!meta.state) {
            return recorder.stop();
          }
          setTimeout(() => {
            meta.onProgress && meta.onProgress((frame + 1) / frames.length, 'webm');
            if (frame === frames.length - 1) {
              return recorder.stop();
            } else {
              frame++;
            }
            displayFrame();
          }, delay[frame]);
        };
        recorder.ondataavailable = (event) => {
          if (event.data && event.data.size) {
            data.push(event.data);
          }
        };
        recorder.onstop = () => {
          if (!meta.state) {
            return reject('Convert stop manually, reject when convert webm.' + meta.id);
          }
          callBack(new Blob(data, { type: 'video/webm' }), meta);
          canvas = null;
          resolve();
        };
        displayFrame();
        recorder.start();
      });
    };
    const convert2 = {
      'gif': toGif,
      'png': toApng,
      'webm': toWebm,
    };
    return {
      add: (blob, meta, callBack) => {
        debugLog('Convert add', meta.path);
        queue.push({ blob, meta, callBack });
        while (active.length < MAX_CONVERT && queue.length && !isStop) {
          convert(queue.shift());
        }
      },
      del: (metas) => {
        if (!metas.length) return;
        isStop = true;
        active = active.filter((meta) => {
          if (metas.includes(meta.meta)) {
            meta.meta.abort();
          } else {
            return true;
          }
        });
        queue = queue.filter((meta) => !metas.includes(meta.meta));
        isStop = false;
        while (active.length < MAX_CONVERT && queue.length) {
          convert(queue.shift());
        }
      },
    };
  })();
  const parser = (() => {
    const TYPE_ILLUSTS = 0;
    const TYPE_MANGA = 1;
    const TYPE_UGOIRA = 2;
    const replaceInvalidChar = (string) => {
      if (!string) return;
      const temp = document.createElement('div');
      temp.innerHTML = string;
      string = temp.textContent;
      return string.trim()
        .replace(/^\.|\.$/g, '')
        .replace(/[\u200b-\u200f\uFEFF\u202a-\u202e\\/:*?|]/g, "")
        .replace(/"/g, "'")
        .replace(/</g, '﹤')
        .replace(/>/g, '﹥');
    };
    const parseByIllust = async (illustId) => {
      const res = await fetch('https://www.pixiv.net/artworks/' + illustId);
      if (!res.ok) throw new Error('fetch artworksURL failed: ' + res.status);
      const htmlText = await res.text();
      const preloadData = JSON.parse(htmlText.match(/"meta-preload-data" content='(.*)'>/)[1]);
      if (!preloadData.illust) throw new Error('Fail to parse meta preload data.');
      const illustInfo = preloadData.illust[illustId];
      const user = replaceInvalidChar(illustInfo.userName) || 'userId-' + illustInfo.userId;
      const title = replaceInvalidChar(illustInfo.illustTitle) || 'illustId-' + illustInfo.illustId;
      const illustType = illustInfo.illustType;
      const filename = filenamePattern.replace('{author}', user).replace('{title}', title).replace('{id}', illustId);
      let metas = [];
      if (illustType === TYPE_ILLUSTS || illustType === TYPE_MANGA) {
        const firstImgSrc = illustInfo.urls.original;
        const srcPrefix = firstImgSrc.slice(0, firstImgSrc.indexOf('_') + 2);
        const srcSuffix = firstImgSrc.slice(-4);
        for (let i = 0; i < illustInfo.pageCount; i++) {
          metas.push({
            id: illustId,
            illustType: illustType,
            path: user + '/' + filename.replace('{page}', i) + srcSuffix,
            src: srcPrefix + i + srcSuffix,
          });
        }
      }
      if (illustType === TYPE_UGOIRA) {
        const ugoiraRes = await fetch('https://www.pixiv.net/ajax/illust/' + illustId + '/ugoira_meta');
        if (!ugoiraRes.ok) throw new Error('fetch ugoira meta failed: ' + res.status);
        const ugoira = await ugoiraRes.json();
        metas.push({
          id: illustId,
          illustType: illustType,
          path: user + '/' + filename.replace('{page}', '0') + '.' + convertFormat,
          src: ugoira.body.originalSrc,
          ugoiraMeta: ugoira.body,
        });
      }
      return metas;
    };
    const parseByUser = async (userId, type) => {
      const res = await fetch('https://www.pixiv.net/ajax/user/' + userId + '/profile/all');
      if (!res.ok) throw new Error('fetch user profile failed: ' + res.status);
      const profile = await res.json();
      let illustIds;
      if (type) {
        illustIds = Object.keys(profile.body[type]);
      } else {
        illustIds = Object.keys(profile.body.illusts).concat(Object.keys(profile.body.manga));
      }
      return illustIds;
    };
    return {
      id: parseByIllust,
      user: parseByUser,
    };
  })();
  const downloader = (() => {
    const MAX_DOWNLOAD = 5;
    const MAX_RETRY = 3;
    let isStop = false;
    let queue = [];
    let active = [];
    const errHandler = (meta) => {
      if (!meta.retries) {
        meta.retries = 1;
      } else {
        meta.retries++;
      }
      if (meta.retries > MAX_RETRY) {
        meta.reject('xmlhttpRequest failed: ' + meta.src);
        console.log('[pixiv downloader]Xml request fail:', meta.path, meta.src);
        active.splice(active.indexOf(meta), 1);
        if (queue.length && !isStop) download(queue.shift());
      } else {
        debugLog('retry xhr', meta.src);
        download(meta);
      }
    };
    const save = (blob, meta) => {
      const imgUrl = URL.createObjectURL(blob);
      const request = {
        url: imgUrl,
        name: meta.path,
        onerror: (error) => {
          console.log('[pixiv downloader]Error when saving.', meta.path);
          meta.reject && meta.reject(error);
        },
        onload: () => {
          if (typeof meta.onLoad === 'function') meta.onLoad();
          URL.revokeObjectURL(imgUrl);
          meta.resolve(meta);
        },
      };
      meta.abort = GM_download(request).abort;
    };
    const download = (meta) => {
      debugLog('Download:', meta.path);
      active.push(meta);
      const request = {
        url: meta.src,
        timeout: 20000,
        method: 'GET',
        headers: {
          referer: 'https://www.pixiv.net'
        },
        responseType: 'blob',
        ontimeout: () => {
          debugLog('xmlhttpRequest timeout:', meta.src);
          errHandler(meta);
        },
        onprogress: (e) => {
          if (e.lengthComputable && typeof meta.onProgress === 'function') {
            meta.onProgress(e.loaded / e.total);
          }
        },
        onload: (e) => {
          debugLog('Download complete', meta.path);
          if (!meta.state) return debugLog();
          if (meta.illustType === 2 && convertFormat !== 'zip') {
            converter.add(e.response, meta, save);
          } else {
            save(e.response, meta);
          }
          active.splice(active.indexOf(meta), 1);
          if (queue.length && !isStop) download(queue.shift());
        },
        onerror: () => {
          debugLog('xmlhttpRequest failed:', meta.src);
          errHandler(meta);
        },
      };
      const abortObj = GM_xmlhttpRequest(request);
      meta.abort = () => {
        meta.state = 0;
        abortObj.abort();
        meta.reject('xhr abort manually. ' + meta.src);
        debugLog('xhr abort:', meta.path);
      };
    };
    const add = (metas) => {
      if (metas.length < 1) return;
      const promises = [];
      metas.forEach((meta) => {
        promises.push(new Promise((resolve, reject) => {
          meta.state = 1;
          meta.resolve = resolve;
          meta.reject = reject;
        }));
      });
      queue = queue.concat(metas);
      while (active.length < MAX_DOWNLOAD && queue.length && !isStop) {
        download(queue.shift());
      }
      return Promise.all(promises);
    };
    const del = (metas) => {
      if (!metas.length) return;
      isStop = true;
      active = active.filter((meta) => {
        if (metas.includes(meta)) {
          meta.abort();
        } else {
          return true;
        }
      });
      queue = queue.filter((meta) => !metas.includes(meta));
      isStop = false;
      while (active.length < MAX_DOWNLOAD && queue.length) {
        download(queue.shift());
      }
    };
    return {
      add: add,
      del: del,
    };
  })();
  const getIllustId = (thumbnail) => {
    if (thumbnail.childElementCount === 0) return false;
    const isHrefMatch = /artworks\/(\d+)$/.exec(thumbnail.href);
    if (isHrefMatch) {
      if (thumbnail.getAttribute('data-gtm-value') || thumbnail.classList.contains('gtm-illust-recommend-thumbnail-thumbnail') || thumbnail.classList.contains('gtm-discover-user-recommend-thumbnail') || thumbnail.classList.contains('work')) {
        return isHrefMatch[1];
      }
      if (location.href.indexOf('bookmark_new_illust') !== -1 && thumbnail.getAttribute('class')) {
        return isHrefMatch[1];
      }
    } else {
      const isActivityMatch = /illust_id=(\d+)/.exec(thumbnail.href);
      if (isActivityMatch && thumbnail.classList.contains('work')) {
        return isActivityMatch[1];
      }
    }
    return '';
  };
  const createPdlBtn = (ele, attributes, textContent = '') => {
    if (!ele) ele = document.createElement('a');
    ele.href = 'javascript:void(0)';
    ele.textContent = textContent;
    if (!attributes) return ele;
    const { attrs, classList } = attributes;
    if (classList && classList.length > 0) {
      for (const cla of classList) {
        ele.classList.add(cla);
      }
    }
    if (attrs) {
      for (const key in attrs) {
        ele.setAttribute(key, attrs[key]);
      }
    }
    return ele;
  };
  const handleDownload = (pdlBtn, illustId) => {
    let pageCount, pageComplete = 0;
    const onProgress = (progress = 0, type = null) => {
      if (pageCount > 1) return;
      progress = Math.floor(progress * 100);
      switch (type) {
        case null:
          pdlBtn.style.setProperty('--pdl-progress', progress + '%');
        case 'gif':
        case 'webm':
          pdlBtn.textContent = progress;
          break;
        case 'zip':
          pdlBtn.textContent = '';
          break;
      }
    };
    const onLoad = function () {
      if (pageCount < 2) return;
      const progress = Math.floor((++pageComplete / pageCount) * 100);
      pdlBtn.textContent = progress;
      pdlBtn.style.setProperty('--pdl-progress', progress + '%');
    };
    pdlBtn.classList.add('pdl-progress');
    parser.id(illustId)
      .then((metas) => {
        pageCount = metas.length;
        metas.forEach((meta) => {
          meta.onProgress = onProgress;
          meta.onLoad = onLoad;
        });
        return downloader.add(metas);
      })
      .then(() => {
        pixivStorage.add(illustId);
        localStorage.setItem(`pdlTemp-${illustId}`, '');
        pdlBtn.classList.remove('pdl-error');
        pdlBtn.classList.add('pdl-complete');
      })
      .catch((err) => {
        if (err) console.log(err);
        pdlBtn.classList.remove('pdl-complete');
        pdlBtn.classList.add('pdl-error');
      })
      .finally(() => {
        pdlBtn.innerHTML = '';
        pdlBtn.style.removeProperty('--pdl-progress');
        pdlBtn.classList.remove('pdl-progress');
      });
  };
  const handleDownloadAll = (userId, type = '') => {
    let worksCount = 0, worksComplete = 0, failed = 0;
    let isCanceled = false;
    let metasRecord = [];
    const timers = [];
    const placeholder = body.querySelector('.pdl-nav-placeholder');
    const control = body.querySelector('.pdl-btn-control');
    const isExcludeDled = body.querySelector('#pdl-filter').checked;
    return new Promise((resolve, reject) => {
      control.onclick = () => {
        isCanceled = true;
        for (const timer of timers) {
          if (timer) clearTimeout(timer);
        }
        if (metasRecord.length) {
          downloader.del(metasRecord);
          converter.del(metasRecord);
          metasRecord = [];
        }
        control.onclick = null;
        reject('Download stopped');
      };
      const onProgressCB = (illustId) => {
        placeholder.textContent = `Downloading: ${++worksComplete} / ${worksCount}`;
        if (worksComplete === worksCount - failed) {
          placeholder.textContent = worksComplete === worksCount ? 'Complete' : 'Incomplete, see console.';
          resolve();
        }
      };
      placeholder.textContent = 'Download...';
      parser.user(userId, type)
        .then((illustIds) => {
          if (isCanceled) throw 'Download stopped';
          if (isExcludeDled) {
            updateHistory();
            debugLog('Before filter', illustIds.length);
            illustIds = illustIds.filter((illustId) => !pixivStorage.has(illustId));
            debugLog('After filter', illustIds.length);
          }
          if (!illustIds.length) throw 'Exclude';
          worksCount = illustIds.length;
          illustIds.forEach((illustId, idx) => {
            if (isCanceled) throw 'Download stopped';
            let timer = setTimeout(() => {
              timer = null;
              parser.id(illustId)
                .then(metas => {
                  if (isCanceled) throw 'Download stop manually: ' + metas[0].id;
                  metasRecord = metasRecord.concat(metas);
                  return downloader.add(metas);
                })
                .then((metas) => {
                  pixivStorage.add(illustId);
                  localStorage.setItem(`pdlTemp-${illustId}`, '');
                  if (isCanceled) throw 'download stopped already, will not update progress.' + illustId;
                  metasRecord = metasRecord.filter((meta) => !metas.includes(meta));
                  onProgressCB();
                })
                .catch((err) => {
                  failed++;
                });
            }, idx * 300);
            timers.push(timer);
          });
        })
        .catch((err) => {
          reject(err);
        });
    });
  };
  const toggleDlAll = (evt) => {
    const target = evt.target;
    if (target.classList.contains('pdl-btn-all')) {
      evt.stopPropagation();
      const dlBars = target.parentElement.querySelectorAll('[pdl-userid]');
      const placeholder = body.querySelector('.pdl-nav-placeholder');
      const userId = target.getAttribute('pdl-userid');
      dlBars.forEach((ele) => { ele.classList.toggle('pdl-hide'); });
      handleDownloadAll(userId, target.getAttribute('pdl-type'))
        .catch((err) => {
          placeholder.textContent = err;
        })
        .finally(() => {
          dlBars.forEach((ele) => { ele.classList.toggle('pdl-hide'); });
        });
    }
  };
  const editFilename = () => {
    const newPattern = prompt(`Default: {author}_{title}_{id}_p{page}`, filenamePattern);
    if (!newPattern) return;
    localStorage.pdlFilename = filenamePattern = newPattern.trim()
      .replace(/^\.|[\u200b-\u200f\uFEFF\u202a-\u202e\\/:*?"|<>]/g, "");
  };
  const observerCallback = () => {
    const isArtworksPage = /artworks\/(\d+)$/.exec(location.href);
    const isUserPage = /users\/(\d+)/.exec(location.pathname);
    const isSelfBookmarks = /users\/\d+\/bookmarks\/artworks/.exec(location.pathname) && body.querySelector('a[href="/setting_profile.php"]');
    if (isArtworksPage && !body.querySelector('.pdl-btn-main')) {
      const handleBar = body.querySelector('main section section');
      if (handleBar) {
        const pdlBtnWrap = handleBar.lastElementChild.cloneNode();
        const attrs = { attrs: { 'pdl-id': isArtworksPage[1] }, classList: ['pdl-btn', 'pdl-btn-main'] };
        if (pixivStorage.has(isArtworksPage[1])) attrs.classList.push('pdl-complete');
        pdlBtnWrap.appendChild(createPdlBtn(null, attrs));
        handleBar.appendChild(pdlBtnWrap);
        body.addEventListener('keydown', (e) => {
          if (e.ctrlKey && e.key === 'q') {
            const pdlMainBtn = body.querySelector('.pdl-btn-main');
            if (pdlMainBtn) {
              e.preventDefault();
              if (!e.repeat) {
                pdlMainBtn.dispatchEvent(new MouseEvent('click', { "bubbles": true }));
              }
            }
          }
        });
      }
    }
    body.querySelectorAll('a').forEach((e) => {
      if (!e.querySelector('.pdl-btn-sub')) {
        const illustId = getIllustId(e);
        if (illustId) {
          const attrs = { attrs: { 'pdl-id': illustId }, classList: ['pdl-btn', 'pdl-btn-sub'] };
          if (pixivStorage.has(illustId)) attrs.classList.push('pdl-complete');
          if (isSelfBookmarks) attrs.classList.push('pdl-btn-sub-self');
          e.appendChild(createPdlBtn(null, attrs));
        }
      }
    });
    if (isUserPage) {
      const nav = body.querySelector('nav');
      if (!nav || body.querySelector('.pdl-nav-placeholder')) return;
      const fragment = document.createDocumentFragment();
      const placeholder = document.createElement('div');
      placeholder.classList.add('pdl-nav-placeholder');
      fragment.appendChild(placeholder);
      const baseEle = nav.querySelector('a:not([aria-current])').cloneNode();
      fragment.appendChild(createPdlBtn(baseEle.cloneNode(), { attrs: { 'pdl-userId': isUserPage[1] }, classList: ['pdl-btn-control', 'pdl-stop', 'pdl-hide'] }, 'Stop'));
      fragment.appendChild(createPdlBtn(baseEle.cloneNode(), { attrs: { 'pdl-userId': isUserPage[1] }, classList: ['pdl-btn-all'] }, 'All'));
      if (nav.querySelector('a[href$=illustrations]') && nav.querySelector('a[href$=manga]')) {
        fragment.appendChild(createPdlBtn(baseEle.cloneNode(), { attrs: { 'pdl-userid': isUserPage[1], 'pdl-type': 'illusts' }, classList: ['pdl-btn-all'] }, 'Illusts'));
        fragment.appendChild(createPdlBtn(baseEle.cloneNode(), { attrs: { 'pdl-userid': isUserPage[1], 'pdl-type': 'manga' }, classList: ['pdl-btn-all'] }, 'Manga'));
      }
      const wrapper = document.createElement('div');
      const checkbox = document.createElement('input');
      const label = document.createElement('label');
      wrapper.classList.add('pdl-wrap');
      checkbox.id = 'pdl-filter';
      checkbox.type = 'checkbox';
      label.setAttribute('for', 'pdl-filter');
      label.textContent = 'Exclude downloaded';
      wrapper.appendChild(checkbox);
      wrapper.appendChild(label);
      nav.parentElement.insertBefore(wrapper, nav);
      nav.appendChild(fragment);
      nav.addEventListener('click', toggleDlAll);
    }
  };
  const updateHistory = () => {
    Object.keys(localStorage).forEach((key) => {
      const matchResult = /pdlTemp-(\d+)/.exec(key);
      if (matchResult) {
        pixivStorage.add(matchResult[1]);
        localStorage.removeItem(matchResult[0]);
      }
    });
    localStorage.pixivDownloader = JSON.stringify([...pixivStorage]);
  };
  localStorage.pixivDownloader = localStorage.pixivDownloader || '[]';
  let pixivStorage = new Set(JSON.parse(localStorage.pixivDownloader));
  updateHistory();
  GM_registerMenuCommand('Apng', () => { convertFormat = localStorage.pdlFormat = 'png'; }, 'a');
  GM_registerMenuCommand('Gif', () => { convertFormat = localStorage.pdlFormat = 'gif'; }, 'g');
  GM_registerMenuCommand('Zip', () => { convertFormat = localStorage.pdlFormat = 'zip'; }, 'z');
  GM_registerMenuCommand('Webm', () => { convertFormat = localStorage.pdlFormat = 'webm'; }, 'w');
  GM_registerMenuCommand('Clear history', () => { updateHistory(); pixivStorage = new Set(); localStorage.pixivDownloader = '[]'; }, 'c');
  GM_registerMenuCommand('Edit filename', () => { editFilename(); }, 'e');
  const pdlStyle = document.createElement('style');
  pdlStyle.innerHTML = `
  @property --pdl-progress {
    syntax: '<percentage>';
    inherits: true;
    initial-value: 0%;
  }
  @keyframes pdl_loading {
    100% {
      transform: translate(-50%, -50%) rotate(360deg);
    }
  }
  .pdl-btn {
    position: relative;
    border-top-right-radius: 8px;
    background: no-repeat center/85%;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%233C3C3C' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E");
    color: #01b468;
    display: inline-block;
    font-size: 13px;
    font-weight: bold;
    height: 32px;
    line-height: 32px;
    margin: 0;
    overflow: hidden;
    padding: 0;
    text-decoration: none!important;
    text-align: center;
    text-overflow: ellipsis;
    user-select: none;
    white-space: nowrap;
    width: 32px;
    z-index: 1;
  }
  .pdl-btn-main {
    margin: 0 0 0 10px;
  }
  .pdl-btn-sub {
    bottom: 0;
    background-color: rgba(255, 255, 255, .5);
    left: 0;
    position: absolute;
  }
  .pdl-btn-sub-self.pdl-btn-sub-self {
    left: auto;
    right: 0;
    bottom: 34px;
    border-radius: 8px;
    border-top-right-radius: 0px;
    border-bottom-right-radius: 0px;
  }
  .pdl-error {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23EA0000' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E");
  }
  .pdl-complete {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%2301B468' d='M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z'%3E%3C/path%3E %3C/svg%3E");
  }
  .pdl-progress {
    background-image: none;
    cursor: default;
  }
  .pdl-progress:after{
    content: '';
    display: inline-block;
    position: absolute;
    top: 50%;
    left: 50%;
    width: 27px;
    height: 27px;
    transform: translate(-50%, -50%);
    -webkit-mask: radial-gradient(transparent, transparent 54%, #000 57%, #000);
    mask: radial-gradient(transparent, transparent 54%, #000 57%, #000);
    border-radius: 50%;
  }
  .pdl-progress:not(:empty):after {
    background: conic-gradient(#01B468 0, #01B468 var(--pdl-progress), transparent var(--pdl-progress), transparent);
    transition: --pdl-progress .2s ease;
  }
  .pdl-progress:empty:after {
    background: conic-gradient(#01B468 0, #01B468 25%, #01B46833 25%, #01B46833);
    animation: 1.5s infinite linear pdl_loading;
  }
  .pdl-nav-placeholder {
    flex-grow: 1;
    height: 42px;
    line-height: 42px;
    text-align: right;
    font-weight: bold;
    font-size: 16px;
    color: rgb(133, 133, 133);
    border-top: 4px solid transparent;
    cursor: default;
    white-space: nowrap;
  }
  .pdl-btn-all::before,
  .pdl-stop::before {
    content: '';
    height: 24px;
    width: 24px;
    transition: background-image 0.2s ease 0s;
    background: no-repeat center/85%;
  }
  .pdl-btn-all::before {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E");
  }
  .pdl-stop::before {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%23858585' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E");
  }
  .pdl-btn-all:hover::before{
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%231F1F1F' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm-32-316v116h-67c-10.7 0-16 12.9-8.5 20.5l99 99c4.7 4.7 12.3 4.7 17 0l99-99c7.6-7.6 2.2-20.5-8.5-20.5h-67V140c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12z'%3E%3C/path%3E %3C/svg%3E");
  }
  .pdl-stop:hover::before {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath fill='%231F1F1F' d='M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z'%3E%3C/path%3E %3C/svg%3E");
  }
  .pdl-hide {
    display: none!important;
  }
  .pdl-wrap {
    text-align: right;
    padding-right: 24px;
    font-weight: bold;
    font-size: 14px;
    line-height: 14px;
    color: rgb(133, 133, 133);
    transition: color 0.2s ease 0s;
}
.pdl-wrap:hover {
    color: rgb(31, 31, 31);
}
.pdl-wrap label {
    padding-left: 8px;
    cursor: pointer;
}
.pdl-wrap input {
    vertical-align: top;
    appearance: none;
    position: relative;
    box-sizing: border-box;
    width: 28px;
    border: 2px solid transparent;
    cursor: pointer;
    border-radius: 14px;
    height: 14px;
    background-color: rgba(133, 133, 133);
    transition: background-color 0.2s ease 0s, box-shadow 0.2s ease 0s;
}
.pdl-wrap input:hover {
    background-color: rgba(31, 31, 31);
}
.pdl-wrap input::after {
    content: "";
    position: absolute;
    display: block;
    top: 0px;
    left: 0px;
    width: 10px;
    height: 10px;
    transform: translateX(0px);
    background-color: rgb(255, 255, 255);
    border-radius: 10px;
    transition: transform 0.2s ease 0s;
}
.pdl-wrap input:checked {
    background-color: rgb(0, 150, 250);
}
.pdl-wrap input:checked::after {
    transform: translateX(14px);
}`;
  document.head.appendChild(pdlStyle);
  const body = document.body;
  new MutationObserver(observerCallback).observe(body, { attributes: false, childList: true, subtree: true, });
  body.addEventListener('click', (event) => {
    const pdlNode = event.target;
    if (pdlNode.hasAttribute('pdl-id')) {
      event.stopPropagation();
      if (!pdlNode.classList.contains('pdl-progress')) {
        handleDownload(pdlNode, pdlNode.getAttribute('pdl-id'));
      }
    }
  });
  function debugLog(...msgs) {
  }
})();