// ==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) {
}
})();