// ==UserScript==
// @name Pixiv Downloader
// @namespace https://greasyfork.org/zh-CN/scripts/432150
// @version 0.3.1
// @description 一键(快捷键)下载Pixiv各页面的原图,支持多图下载,动图下载支持Zip压缩包/Gif动图格式切换。下载的图片将保存到以画师名命名的单独文件夹(需要调整tampermonkey“下载”设置为“浏览器API”)。保留已下载图片的记录。
// @author ruaruarua
// @match https://www.pixiv.net/*
// @icon https://www.google.com/s2/favicons?domain=pixiv.net
// @grant GM_xmlhttpRequest
// @grant GM_download
// @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 () {
const GIF_QUALITY = 10; //0-10。数值越小,GIF质量越高
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.pageCount = 1;
this.pageDownloaded = 0;
this.imgList = [];
this.gifSrcInfo = {};
}
saveAsGif_(blob, self, savePath, resolve, reject) {
const gifPromiseList = [];
const 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.saveImg_(blob, savePath, resolve, reject);
});
gif.render();
});
})
}
saveImg_(blob, savePath, resolve, reject) {
const imgUrl = URL.createObjectURL(blob);
GM_download({
url: imgUrl,
name: savePath,
onerror: (error) => {
console.log('Error: ', error);
if (reject) {
reject();
}
},
onload: () => {
URL.revokeObjectURL(imgUrl);
if (resolve) {
resolve();
}
}
});
}
startDownload_(savePath, imgSrc, onProgressCallback, onLoadCallback, resolve, reject) {
let self = this;
GM_xmlhttpRequest({
url: imgSrc,
method: 'GET',
headers: {
referer: 'https://www.pixiv.net'
},
responseType: 'blob',
onprogress: (e) => {
if (typeof onProgressCallback == 'function') {
onProgressCallback(self.pageCount, e);
}
},
onload: function (res) {
self.pageDownloaded++;
if (typeof onLoadCallback == 'function') {
onLoadCallback(self.pageDownloaded, self.pageCount);
}
if (savePath.slice(-3) != 'gif') {
self.saveImg_(res.response, savePath, resolve, reject);
} else {
self.saveAsGif_(res.response, self, savePath, resolve, reject);
}
},
onerror: function () {
console.log('XmlhttpRequest failed: ' + imgSrc);
if (reject) {
reject();
}
}
});
}
replaceInvalidChar_(oriString = '') {
if (oriString) {
let newString = '';
for (let i = 0; i < oriString.length; i++) {
const char = oriString.charAt(i);
switch (char) {
case '\\':
case '/':
case ':':
case '?':
case '|':
newString += '-';
break;
case '"':
newString += "'";
break;
case '<':
newString += "[";
break;
case '>':
newString += "]";
break;
case '\u200B':
break;
default:
newString += char;
}
}
return newString;
}
}
async initProps_(htmlText) {
this.data = JSON.parse(htmlText.match(/"meta-preload-data" content='(.*)'>/)[1]);
if (this.data.illust) {
const illustInfo = this.data.illust[this.pixivId];
this.author = this.replaceInvalidChar_(illustInfo.userName) || 'userId-' + illustInfo.userId;
this.illustTitle = this.replaceInvalidChar_(illustInfo.illustTitle) || 'illustId-' + illustInfo.illustId;
this.illustType = illustInfo.illustType;
if (this.illustType == 0 || this.illustType == 1) {
this.pageCount = illustInfo.pageCount;
const firstImgSrc = illustInfo.urls.original;
const srcPrefix = firstImgSrc.slice(0, firstImgSrc.indexOf('_') + 2);
const srcSuffix = firstImgSrc.slice(-4);
for (let i = 0; i < this.pageCount; i++) {
const imgSrc = srcPrefix + i + 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, onLoadCallback) {
let baseSavePath = this.author + '/' + this.author + '_' + this.illustTitle + '_' + this.pixivId;
baseSavePath = baseSavePath.replace('./', '/');
if (this.illustType == 0 || this.illustType == 1) {
const promiseList = [];
if (this.imgList.length > 0) {
this.imgList.forEach((imgSrc, i) => {
promiseList[i] = new Promise((resolve, reject) => {
const savePath = baseSavePath + '_p' + i + imgSrc.slice(-4);
this.startDownload_(savePath, imgSrc, onProgressCallback, onLoadCallback, resolve, reject);
});
})
}
return Promise.all(promiseList);
}
if (this.illustType == 2) {
return new Promise((resolve, reject) => {
if (GIF_FLAG) {
baseSavePath += '.gif';
} else {
baseSavePath += '.zip';
}
this.startDownload_(baseSavePath, this.gifSrcInfo.originalSrc, onProgressCallback, onLoadCallback, resolve, reject);
});
}
}
}
const getPixivId = function (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;
}
const createPdlBtn = function (isMain = false, pixivId = '') {
const pdlBtn = document.createElement('a');
pdlBtn.setAttribute('href', 'javascript:void(0)');
pdlBtn.setAttribute('pdl-id', pixivId);
pdlBtn.classList.add('pdl-btn');
if (!isMain) {
pdlBtn.classList.add('pdl-btn-sub');
} else {
pdlBtn.classList.add('pdl-btn-main');
}
if (pixivStorage.has(pixivId)) {
pdlBtn.classList.add('pdl-complete');
}
return pdlBtn;
}
const handleDownload = function (pdlBtn, pixivId) {
const onProgressCallback = function (pageCount = 1, e) {
if (pageCount == 1) {
if (e.loaded && e.total != -1) {
pdlBtn.innerHTML = Math.round((e.loaded / e.total) * 100);
} else {
pdlBtn.innerHTML = '';
}
}
};
const onLoadCallback = function (pageDownloaded = 0, pageCount = 1) {
if (pageCount != 1) {
pdlBtn.innerHTML = Math.round((pageDownloaded / pageCount) * 100);
} else {
pdlBtn.innerHTML = '';
}
}
pdlBtn.classList.add('pdl-progress');
new PixivDownloader(pixivId)
.fetchData()
.then(downloader => downloader.download(onProgressCallback, onLoadCallback))
.then(() => {
pixivStorage.add(pixivId);
localStorage.setItem(`pdlTemp-${pixivId}`, '')
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.classList.remove('pdl-progress');
});
}
const shortKeyHandler = function (e) {
if (e.ctrlKey && e.key == 'q') {
const pdlMainBtn = this.querySelector('.pdl-btn-main');
if (pdlMainBtn) {
e.preventDefault();
if (!e.repeat) {
pdlMainBtn.dispatchEvent(new MouseEvent('click', { "bubbles": true }));
}
}
}
}
const observerCallback = function () {
const reg = /artworks\/([0-9]+)$/;
const isArtworksPage = reg.exec(window.location.href);
if (isArtworksPage && !bodyNode.querySelector('.pdl-btn-main')) {
const addFavousBtn = bodyNode.querySelector('.gtm-main-bookmark');
if (addFavousBtn){
const handleBar = addFavousBtn.parentElement.parentElement;
const pdlBtnWrap = addFavousBtn.parentElement.cloneNode();
pdlBtnWrap.appendChild(createPdlBtn(true, isArtworksPage[1]));
handleBar.appendChild(pdlBtnWrap);
bodyNode.addEventListener('keydown', shortKeyHandler);
}
}
const picLists = bodyNode.querySelectorAll('a');
picLists.forEach((e) => {
if (!e.querySelector('.pdl-btn-sub')) {
const pixivId = getPixivId(e);
if (pixivId) {
e.appendChild(createPdlBtn(false, pixivId));
}
}
});
}
localStorage.isPdlGif = localStorage.isPdlGif || 'false';
localStorage.pixivDownloader = localStorage.pixivDownloader || '[]';
let GIF_FLAG = JSON.parse(localStorage.isPdlGif);
let pixivStorage = new Set(JSON.parse(localStorage.pixivDownloader));
const updateStorage = function(){
Object.keys(localStorage).forEach((string) => {
const matchResult = /pdlTemp-(\d+)/.exec(string);
if (matchResult) {
pixivStorage.add(matchResult[1]);
localStorage.removeItem(matchResult[0]);
}
});
};
updateStorage();
localStorage.pixivDownloader = JSON.stringify([...pixivStorage]);
GM_registerMenuCommand('Gif', () => { GIF_FLAG = true; localStorage.isPdlGif = GIF_FLAG; }, 'g');
GM_registerMenuCommand('Zip', () => { GIF_FLAG = false; localStorage.isPdlGif = GIF_FLAG; }, 'z');
GM_registerMenuCommand('Clear history', () => { updateStorage(); localStorage.pixivDownloader = '[]'; }, 'c');
const pdlStyle = document.createElement('style');
pdlStyle.innerHTML = ` .pdl-btn {
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: 16px;
font-weight: bold;
height: 32px;
line-height: 32px;
margin: 0;
overflow: hidden;
padding: 0;
text-decoration: none!important;
text-align: center;
text-overflow: ellipsis;
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-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:empty {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E %3Cpath fill='%2301B468' d='M368 48h4c6.627 0 12-5.373 12-12V12c0-6.627-5.373-12-12-12H12C5.373 0 0 5.373 0 12v24c0 6.627 5.373 12 12 12h4c0 80.564 32.188 165.807 97.18 208C47.899 298.381 16 383.9 16 464h-4c-6.627 0-12 5.373-12 12v24c0 6.627 5.373 12 12 12h360c6.627 0 12-5.373 12-12v-24c0-6.627-5.373-12-12-12h-4c0-80.564-32.188-165.807-97.18-208C336.102 213.619 368 128.1 368 48zM64 48h256c0 101.62-57.307 184-128 184S64 149.621 64 48zm256 416H64c0-101.62 57.308-184 128-184s128 82.38 128 184z'%3E%3C/path%3E %3C/svg%3E");
background-size: 60%;
}`;
document.head.appendChild(pdlStyle);
const bodyNode = document.body;
const config = { attributes: false, childList: true, subtree: true };
const rootObserver = new MutationObserver(observerCallback);
rootObserver.observe(bodyNode, config);
let zip = new JSZip();
const workerStr = await fetch('https://raw.githubusercontent.com/jnordberg/gif.js/master/dist/gif.worker.js')
.then(res => res.blob());
const gifConfig = {
workers: 3,
quality: GIF_QUALITY,
workerScript: URL.createObjectURL(workerStr)
};
bodyNode.addEventListener('click', (e) => {
if (e.target.hasAttribute('pdl-id')) {
e.stopPropagation();
if (!e.target.classList.contains('pdl-progress')) {
const pixivId = e.target.getAttribute('pdl-id');
handleDownload(e.target, pixivId);
}
}
});
})();