您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a download button to rip chapters from K MANGA (bypasses image scrambling protection)
当前为
// ==UserScript== // @name K MANGA Ripper // @version 2.2.0 // @description Adds a download button to rip chapters from K MANGA (bypasses image scrambling protection) // @author /a/non // @namespace K MANGA Ripper // @match https://kmanga.kodansha.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=kodansha.com // @grant none // @run-at document-start // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // ==/UserScript== (function() { 'use strict'; let currentChapterJson; const ogXhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (...args) { if (args[1].startsWith("https://api.kmanga.kodansha.com/web/episode/viewer")) { this.addEventListener("load", () => {currentChapterJson = JSON.parse(this.responseText)}); } return ogXhrOpen.apply(this, args); } const getUnscrambledCoords = (seed) => { const seed32 = new Uint32Array(1); seed32[0] = seed; const pairs = []; for (let i = 0; i < 16; i++) { seed32[0] ^= seed32[0] << 13; seed32[0] ^= seed32[0] >>> 17; seed32[0] ^= seed32[0] << 5; pairs.push([seed32[0], i]); } pairs.sort((a, b) => a[0] - b[0]); const sortedVal = pairs.map(e => e[1]); const coords = sortedVal.map((e, i) => ({ source: { x: e % 4, y: Math.floor(e / 4) }, dest: { x: i % 4, y: Math.floor(i / 4) } })); return coords; } const canvas = new OffscreenCanvas(0, 0); const ctx = canvas.getContext("2d"); const drawPage = (page, coords, tileWidth, tileHeight) => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(page, 0, 0); for (const c of coords) { ctx.drawImage(page, c.source.x * tileWidth, c.source.y * tileHeight, tileWidth, tileHeight, c.dest.x * tileWidth, c.dest.y * tileHeight, tileWidth, tileHeight); } } const getTitle = () => { const seriesName = document.querySelector(".p-episode__comic-ttl").innerText; const chapterName = document.querySelector(".p-episode__header-ttl").innerText; const words = chapterName.split(" "); const fixedChapterName = words.map(word => word[0] + word.substring(1).toLowerCase()).join(" "); const title = seriesName + " - " + fixedChapterName; const invalidChar = /[<>:"\/\|?*]/g; const fixedTitle = title.replace(invalidChar, ""); return fixedTitle; } const downloadChapter = async (dlButton, progressBar) => { if (!currentChapterJson) throw new Error("No chapter found"); const scrambleSeed = currentChapterJson.scramble_seed; const pageList = currentChapterJson.page_list; if (!scrambleSeed) throw new Error("Invalid scramble seed"); if (!pageList || pageList.length === 0) throw new Error("No pages found"); const pageCount = pageList.length; let title; try {title = getTitle()} catch(e) { console.error(e); title = "Undefined"; } let pageCountProgress = 0; const updateDlProgress = () => { const percentage = Math.round((++pageCountProgress / (pageCount * 5)) * 100); progressBar.style.width = percentage + "%"; dlButton.innerText = `Downloading ${percentage}%`; } let pageBitmaps; try { const responses = await Promise.all(pageList.map(async (pageUrl) => { const response = await fetch(pageUrl); updateDlProgress(); return response; })); const blobs = await Promise.all(responses.map(async (response) => { const blob = await response.blob(); updateDlProgress(); return blob; })); pageBitmaps = await Promise.all(blobs.map(async (blob) => { const pageBitmap = await createImageBitmap(blob); updateDlProgress(); return pageBitmap; })); } catch(e) { console.error(e); throw new Error("Couldn't retrieve chapter pages"); } const unscrambledPageBlobs = []; try { const unscrambledCoords = getUnscrambledCoords(scrambleSeed); const pageWidth = pageBitmaps[0].width; const pageHeight = pageBitmaps[0].height; canvas.width = pageWidth; canvas.height = pageHeight; const getTileDimension = (size) => (Math.floor(size / 8) * 8) / 4; const tileWidth = getTileDimension(pageWidth); const tileHeight = getTileDimension(pageHeight); for (const page of pageBitmaps) { updateDlProgress(); drawPage(page, unscrambledCoords, tileWidth, tileHeight); const blob = await canvas.convertToBlob({type: "image/jpeg", quality: 0.8}); updateDlProgress(); unscrambledPageBlobs.push(blob); } } catch(e) { console.error(e); throw new Error("Couldn't unscramble pages"); } try { const zip = new JSZip(); unscrambledPageBlobs.forEach((page, index) => { const paddedFileName = (index + 1).toString().padStart(pageCount.toString().length, "0") + ".jpg"; zip.file(paddedFileName, page, {binary: true}); }); zip.generateAsync({type: "blob"}).then(blob => saveAs(blob, title + ".zip")); } catch (e) { console.error(e); throw new Error("Couldn't process zip file"); } } const insertDlButton = (target) => { const dlButtonContainer = document.createElement("div"); const progressBar = document.createElement("div"); const dlButton = document.createElement("button"); dlButtonContainer.id = "dl-button-container"; progressBar.id = "progress-bar"; dlButton.id = "dl-button"; dlButton.innerText = "Download Chapter"; dlButton.onclick = async () => { dlButton.disabled = true; dlButton.innerText = "Downloading..."; dlButton.classList.add("loading"); const resetButton = () => { dlButton.disabled = false; dlButton.classList = []; dlButton.innerText = "Download Chapter"; progressBar.style.width = "0"; } try { if (currentChapterJson) { await downloadChapter(dlButton, progressBar); setTimeout(() => resetButton(), 500); } else { setTimeout(async () => { await downloadChapter(dlButton, progressBar); setTimeout(() => resetButton(), 500); }, 2000); } } catch(e) { dlButton.innerText = "Download Failed"; dlButton.classList.add("fail"); setTimeout(() => resetButton(), 2000); setTimeout(() => alert(`Error downloading chapter: ${e.message}\nReload the page and try again`), 100); } } target.appendChild(dlButtonContainer); dlButtonContainer.appendChild(progressBar); dlButtonContainer.appendChild(dlButton); } const dlButtonStyle = document.createElement("style"); dlButtonStyle.textContent = ` #dl-button-container { position: relative; top: 6px; width: 180px; height: 35px; } #progress-bar { position: absolute; z-index: 1; top: 3px; bottom: 0; left: 3px; right: 0; width: 0; max-width: 177px; height: 29px; border-radius: 0 3px 3px 0; background-color: #d6d6d6; } #dl-button { position: absolute; z-index: 2; top: 0; bottom: 0; left: 0; right: 0; width: 180px; height: 35px; font-family: "Helvetica Neue", "Helvetica", "Aral", "sans-serif"; font-size: 1.7rem; font-weight: bold; color: #2a93d9; border: 3px solid #2a93d9; border-radius: 7px; cursor: pointer; } #dl-button:not(.loading):not(.locked):not(.fail):hover { color: #2287c9; border-color: #2287c9; background-color: #f0f0f0; } #dl-button.loading { color: #2287c9; border-color: #2287c9; cursor: default; } #dl-button.fail { color: red; border-color: red; background-color: #f0f0f0; } #dl-button.locked { color: gray; border-color: gray; cursor: not-allowed; } `; document.head.appendChild(dlButtonStyle); let currentLocation = location.pathname; const chapterPathTemplate = /^\/title\/.*\/episode\/.*$/; const checkChapter = (mutationList, observer) => { if (location.pathname.match(chapterPathTemplate)) { const dlButton = document.querySelector("#dl-button"); const target = document.querySelector(".p-episode__header-inner"); const isNotBought = document.querySelector(".p-episode-purchase"); if (!dlButton) { if (target) insertDlButton(target); } else { if (location.pathname !== currentLocation && currentChapterJson) currentChapterJson = null; if (isNotBought) { dlButton.disabled = true; dlButton.setAttribute("title", "Cannot download a chapter you don't have access to"); dlButton.classList.add("locked"); } else if (dlButton.classList.contains("locked")) { dlButton.disabled = false; dlButton.removeAttribute("title"); dlButton.classList.remove("locked"); } } currentLocation = location.pathname; } } document.addEventListener(("DOMContentLoaded"), () => { const observer = new MutationObserver(checkChapter); observer.observe(document.body, {childList: true, subtree: true}); }); })();