// ==UserScript==
// @name Pixiv Downloader
// @namespace https://greasyfork.org/zh-CN/scripts/432150
// @version 0.4.3
// @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 isBookmarkPage = /users\/\d+\/bookmarks\/artworks/.test(
location.pathname
);
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 (isBookmarkPage) 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) {}
})();