// ==UserScript==
// @name Pixiv Downloader
// @namespace https://greasyfork.org/zh-CN/scripts/432150
// @version 0.2.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 GIF_FLAG = false; //需要保存GIF请设置为true
const GIF_QUALITY = 5; //数值越小,GIF质量越好
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: GIF_QUALITY,
workerScript: URL.createObjectURL(workerStr)
};
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: ',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) {
let basePath = this.authorFormatted + '/' + this.authorFormatted + '_' + this.illustTitleFormatted + '_' + this.pixivID;
basePath = basePath.replace('./','/');
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 = basePath + '_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) => {
if (GIF_FLAG) {
basePath += '.gif';
} else {
basePath += '.zip';
}
this.startDownload(basePath, 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 && link.getAttribute('class')) {
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) {
// ctrl + d
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 && e.total != -1) {
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();
})();