Add `download all chapter` button to syosetu.com (you need login to download chapters )
// ==UserScript== // @name 小説家になろう book downloader // @namespace https://github.com/NateScarlet/Scripts/tree/master/user-script // @description Add `download all chapter` button to syosetu.com (you need login to download chapters ) // @grant GM.xmlHttpRequest // @include /^https?://ncode\.syosetu\.com/\w+/$/ // @include /^https?://novel18\.syosetu\.com/\w+/$/ // @run-at document-end // @version 2023.12.05+2c14f8f3 // ==/UserScript== "use strict"; (() => { var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/utils/urlLastPart.ts function urlLastPart(url) { return url.split("/").filter((i) => i).slice(-1)[0]; } // src/utils/downloadFile.ts function downloadFile(file, filename = `${urlLastPart(location.pathname)} ${document.title}.md`) { const anchor = document.createElement("a"); anchor.href = URL.createObjectURL(file); anchor.download = filename; anchor.style["display"] = "none"; document.body.append(anchor); anchor.click(); setTimeout(() => { document.body.removeChild(anchor); URL.revokeObjectURL(anchor.href); }, 0); } // src/utils/canvasToMarkdown.ts function canvasToMarkdown(canvas, alt = "", title = "") { return `} "${title}")`; } // src/utils/isCanvasTainted.ts function isCanvasTainted(canvas) { try { canvas.getContext("2d").getImageData(0, 0, 1, 1); return false; } catch (err) { return err instanceof DOMException && err.name === "SecurityError"; } } // src/utils/imageToCanvas.ts function imageToCanvas(_0) { return __async(this, arguments, function* (img, { background } = {}) { const canvas = document.createElement("canvas"); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext("2d"); if (background) { ctx.fillStyle = background; ctx.fillRect(0, 0, canvas.width, canvas.height); } ctx.drawImage(img, 0, 0); if (img.src && img.crossOrigin !== "anonymous" && isCanvasTainted(canvas)) { const corsImage = new Image(); corsImage.crossOrigin = "anonymous"; corsImage.src = img.src; yield corsImage.decode(); return imageToCanvas(corsImage, { background }); } return canvas; }); } // src/utils/imageToMarkdown.ts function imageToMarkdown(_0) { return __async(this, arguments, function* (img, { background } = {}) { return canvasToMarkdown( yield imageToCanvas(img, { background }), img.alt, img.title ); }); } // src/utils/loadImage.ts function loadImage(url) { return __async(this, null, function* () { const img = new Image(); img.src = url; img.alt = url; yield img.decode(); return img; }); } // src/utils/parseHeader.ts function parseHeader(headers) { const ret = /* @__PURE__ */ new Map(); for (const line of headers.split("\r\n")) { if (!line) { continue; } const match = /^(.+?): ?(.+)$/.exec(line); if (!match) { throw new Error(`malformed header: ${line}`); } const [_, key, value] = match; if (!ret.has(key)) { ret.set(key, []); } ret.get(key).push(value); } return ret; } // src/utils/loadImageCORS.ts function loadImageCORS(url) { return __async(this, null, function* () { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: "GET", url, // https://github.com/greasemonkey/greasemonkey/issues/1834#issuecomment-37084558 overrideMimeType: "text/plain; charset=x-user-defined", onload: (_0) => __async(this, [_0], function* ({ responseText, responseHeaders }) { var _a, _b; const headers = parseHeader(responseHeaders); const data = new Blob( [Uint8Array.from(responseText.split("").map((i) => i.charCodeAt(0)))], { type: (_b = (_a = headers.get("content-type")) == null ? void 0 : _a[0]) != null ? _b : "image/jpeg" } ); const src = URL.createObjectURL(data); const image = yield loadImage(src); image.alt = url; resolve(image); URL.revokeObjectURL(src); }), onerror: ({ context }) => { reject({ context }); } }); }); }); } // src/utils/sleep.ts function sleep(duration) { return __async(this, null, function* () { return new Promise((resolve) => { setTimeout(resolve, duration); }); }); } // src/syosetu.com/download.user.ts var __name__ = "小説家になろう book downloader"; var statusIndicator = document.createElement("span"); var finishedCount = 0; var totalCount = 0; function log(...v) { console.log(`${__name__}:`, ...v); } var messageNodes = []; function addMessage(text, title, color = "red") { const div = document.createElement("div"); div.className = `ui ${color} message`; statusIndicator.after(div); messageNodes.push(div); if (title) { const header = document.createElement("div"); header.innerText = title; header.className = "header"; div.appendChild(header); } const lines = (typeof text === "string" ? [text] : text) || []; for (const i of lines) { const p = document.createElement("p"); p.innerText = i; div.appendChild(p); } } function chapterImageToMarkdown(line) { return __async(this, null, function* () { const match = line.match(/^<(.+)\|(.+)>$/); if (match) { const url = `https://${match[2]}.mitemin.net/userpageimage/viewimagebig/icode/${match[1]}/`; try { return imageToMarkdown(yield loadImageCORS(url)); } catch (err) { addMessage([url, JSON.stringify(err)], "Image download failed", "orange"); return ``; } } return line; }); } function updateStatus() { statusIndicator.innerText = `(${finishedCount}/${totalCount})`; } function downloadChapter(ncode, chapter) { return __async(this, null, function* () { const url = `https://${location.host}/txtdownload/dlstart/ncode/${ncode}/?no=${chapter}&hankaku=0&code=utf-8&kaigyo=lf`; log(`fetch chapter: ${chapter}: ${url}`); const resp = yield fetch(url); if (resp.status !== 200) { addMessage( [`${resp.status} ${resp.statusText}`, url], "Fetch chapter failed" ); throw new Error( `Fetch chapter failed: ${resp.status} ${resp.statusText} : ${url}` ); } return yield resp.text(); }); } function clearMessage() { while (messageNodes.length) { messageNodes.pop().remove(); } } function getMetaData() { const data = { link: document.location.href }; const authorContainer = document.querySelector(".novel_writername"); const authorAnchor = document.querySelector( ".novel_writername > a:nth-child(1)" ); if (authorAnchor instanceof HTMLAnchorElement) { data["author"] = authorAnchor.innerText; data["author_link"] = authorAnchor.href; } else if (authorContainer instanceof HTMLDivElement) { data["author"] = authorContainer.innerText.replace(/^作者:/, ""); } return [ "---", ...Object.entries(data).map(([k, v]) => `${k}: ${v}`), "---" ].join("\n"); } function downloadChapterChunk(ncode, chapters) { return __async(this, null, function* () { return Promise.all( chapters.map( (i) => function() { return __async(this, null, function* () { const ret = yield Promise.all( (yield downloadChapter(ncode, i.chapter)).split("\n").map((i2) => i2.trim()).filter((i2) => i2.length > 0).map(chapterImageToMarkdown) ); ret.splice(0, 0, `# ${i.title}`); finishedCount += 1; updateStatus(); return ret; }); }() ) ).then((i) => { const ret = []; i.map((j) => { ret.push(...j); }); return ret; }); }); } function main(button) { return __async(this, null, function* () { clearMessage(); const ncode = urlLastPart( document.querySelector( "#novel_footer > ul:nth-child(1) > li:nth-child(3) > a:nth-child(1)" ).href ); log(`start downloading: ${ncode}`); const chapters = []; for (const i of document.querySelectorAll( "dl.novel_sublist2 > dd:nth-child(1) > a:nth-child(1)" )) { chapters.push({ chapter: urlLastPart(i.href), title: i.innerText }); } finishedCount = 0; totalCount = chapters.length; updateStatus(); const lines = []; const chunkSize = 10; for (let i = 0; i < chapters.length; i += chunkSize) { lines.push( ...yield downloadChapterChunk(ncode, chapters.slice(i, i + chunkSize)) ); yield sleep(5e3); } log(`got ${lines.length} lines`); function download() { downloadFile( new Blob([getMetaData(), "\n\n", lines.join("\n\n")], { type: "text/markdown" }) ); } button.onclick = download; download(); }); } (function() { return __async(this, null, function* () { const button = document.createElement("button"); button.innerText = "Download all chapters"; button.className = "button"; button.onclick = () => __async(this, null, function* () { try { button.disabled = true; button.style.opacity = "50%"; yield main(button); } catch (err) { console.error(err); } finally { button.disabled = false; button.style.opacity = ""; } }); document.querySelector("#novel_ex").after(button, statusIndicator); log("activated"); }); })(); })();