// ==UserScript==
// @name Biliplus Chrome 整本下载
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @description Biliplus Chrome 整本下载,需要在https://www.biliplus.com/manga/?act=detail&mangaid=类似详情页下载
// @author Aoba Xu
// @match https://www.biliplus.com/manga/?act=detail&mangaid=*
// @icon https://www.google.com/s2/favicons?sz=64&domain=biliplus.com
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const createStyles = () => {
const style = document.createElement("style");
style.innerText += `.b-toolbox-d-flex { display: flex } .b-toolbox-d-none { display: none } .b-toolbox-flex-column { flex-direction: column }`;
document.head.append(style);
return {
element: style,
addStyle: (newStyle) => {
style.innerText += newStyle;
},
};
};
const styles = createStyles();
const createPopupPanel = (styles) => {
const panel = document.createElement("div");
styles.addStyle(
`.b-toolbox-popup { top:70px; right: 1rem; position: fixed; border-radius: 6px; max-height: 50% }`
);
panel.className = "b-toolbox-popup b-toolbox-d-flex";
document.body.append(panel);
return panel;
};
const popupPanel = createPopupPanel(styles);
const createToolboxPanel = (parentPanel, styles) => {
const panel = document.createElement("div");
styles.addStyle(
`.b-toolbox-panel { margin-right: 1.5rem; background: rgba(255, 255, 255, 0.8); padding: 1rem; gap: 1rem }`
);
panel.className = "b-toolbox-panel b-toolbox-d-none b-toolbox-flex-column";
parentPanel.append(panel);
return panel;
};
const toolboxPanel = createToolboxPanel(popupPanel, styles);
const createToolboxShowBtn = (parentPanel, showablePanel, styles) => {
const container = document.createElement("div");
container.className = "b-toolbox-d-flex b-toolbox-flex-column";
parentPanel.append(container);
const btn = document.createElement("button");
btn.role = "button";
btn.insertAdjacentHTML("beforeEnd", "<div>工具箱</div>");
styles.addStyle(
`.b-toolbox-toolbox-btn { align-items: center; background-color: #32aaff; border: none; border-radius: 6px; color: #fff; cursor: pointer; display: flex; justify-content: center; padding: 1rem 0.5rem }`
);
btn.className += "b-toolbox-toolbox-btn";
container.append(btn);
btn.onclick = () => {
showablePanel.classList.toggle("b-toolbox-d-none");
showablePanel.classList.toggle("b-toolbox-d-flex");
};
};
createToolboxShowBtn(popupPanel, toolboxPanel, styles);
const createStatusDisplay = (parentPanel) => {
const panel = document.createElement("div");
panel.className = "b-toolbox-d-flex b-toolbox-flex-column";
panel.style.overflow = "auto";
panel.insertAdjacentHTML("beforeEnd", "<div>等待任务</div>");
parentPanel.append(panel);
let timer = 0;
const complete = () => {
panel.insertAdjacentHTML("beforeEnd", "<div>已完成</div>");
timer = setTimeout(() => {
panel.innerHTML = "";
panel.insertAdjacentHTML("beforeEnd", "<div>等待任务</div>");
}, 2000);
};
return {
element: panel,
complete,
clear: () => {
clearTimeout(timer);
panel.innerHTML = "";
panel.insertAdjacentHTML("beforeEnd", "<div>等待任务</div>");
},
addStatus: (status) => {
panel.insertAdjacentHTML("beforeEnd", `<div>${status}</div>`);
},
};
};
const statusDisplay = createStatusDisplay(toolboxPanel);
const safeFileName = (name) => {
return name.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\.$/, "_");
};
const createDownloadBtn = (parentPanel, statusDisplay) => {
const btn = document.createElement("button");
btn.type = "button";
btn.insertAdjacentHTML("beforeEnd", "<div>下载本书已购内容</div>");
btn.className += "b-toolbox-toolbox-btn";
parentPanel.append(btn);
btn.addEventListener("click", async () => {
btn.disabled = true;
const { storage, needExport } = (() => {
if (window.showDirectoryPicker) {
return {
storage: window.showDirectoryPicker({
id: "b-toolbox-download-folder",
startIn: "desktop",
mode: "readwrite",
}),
needExport: false,
};
} else {
return {
storage: navigator.storage.getDirectory(),
needExport: true,
};
}
})();
if (needExport) {
statusDisplay.addStatus(`暂不支持导出`);
statusDisplay.complete();
}
const epList = Array.from(
document.querySelectorAll(".contents-full>a.episode:not(.locked)")
).map((x) => ({
url: x.href,
title: x.innerText,
}));
statusDisplay.addStatus(`共${epList.length}个章节`);
if (epList.length === 0) {
statusDisplay.addStatus(`无需下载`);
statusDisplay.complete();
btn.disabled = false;
return;
}
const dir = await storage;
const comicFolder = await dir.getDirectoryHandle(
safeFileName(document.querySelector("h3").innerText),
{
create: true,
}
);
const docParser = new DOMParser();
for (let i = 0; i < epList.length; i++) {
const ep = epList[i];
statusDisplay.addStatus(`正在下载第${i + 1}话 ${ep.title}`);
const res = await fetch(ep.url, {
method: "GET",
credentials: "include",
});
const text = await res.text();
const subDoc = docParser.parseFromString(text, "text/html");
const downloadUrls = Array.from(
subDoc.querySelectorAll("img.comic-single")
).map((x) => x.attributes._src.nodeValue);
console.log(subDoc.querySelectorAll("img.comic-single"));
console.log(downloadUrls);
const epFolder = await comicFolder.getDirectoryHandle(
safeFileName(ep.title),
{
create: true,
}
);
const padding = Math.ceil(Math.log10(downloadUrls.length));
const tasks = downloadUrls.map(async (url, j) => {
const file = await epFolder.getFileHandle(
`${(j + 1).toString().padStart(padding, "0")}.jpg`,
{ create: true }
);
const writable = await file.createWritable();
const res = await fetch(url);
await res.body.pipeTo(writable);
});
await Promise.all(tasks);
const delay = Math.floor(Math.random() * 1000) + 500;
statusDisplay.addStatus(`等待${delay}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
if (needExport) {
statusDisplay.addStatus(`导出下载文件`);
}
statusDisplay.complete();
btn.disabled = false;
});
};
createDownloadBtn(toolboxPanel, statusDisplay);
})();