您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a download button to rip chapters from Magapoke (bypasses image scrambling protection)
// ==UserScript== // @name Magapoke Ripper // @version 1.0.0 // @description Adds a download button to rip chapters from Magapoke (bypasses image scrambling protection) // @author /a/non // @namespace https://greasyfork.org/en/users/1090983-anon // @license MIT // @match https://pocket.shonenmagazine.com/* // @icon https://pocket.shonenmagazine.com/img/favicon.ico // @grant GM.xmlHttpRequest // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function () { "use strict"; let selectedChapter; const canvas = new OffscreenCanvas(0, 0); const ctx = canvas.getContext("2d", { alpha: false }); // Hook fetch method to intercept request to selected chapter's info const ogFetch = unsafeWindow.fetch; unsafeWindow.fetch = async (url, options) => { const response = await ogFetch(url, options); if (url.includes("/web/episode/viewer")) { selectedChapter = await response.clone().json(); } return response; }; // Descrambling logic and tile dimension calculations from chunk-SYAGRXIy.js (simplified) 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]); return sortedVal.map((e, i) => ({ source: { x: e % 4, y: Math.floor(e / 4), }, dest: { x: i % 4, y: Math.floor(i / 4), }, })); }; // Rearrange the tiles based on the unscrambled coordinates const drawPage = async (page, coords) => { const getTileDimension = (size) => Math.floor(size / 32) * 8; const pageWidth = page.width; const pageHeight = page.height; const tileWidth = getTileDimension(pageWidth); const tileHeight = getTileDimension(pageHeight); canvas.width = pageWidth; canvas.height = pageHeight; 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 ); } return await canvas.convertToBlob({ type: "image/jpeg", quality: 0.8, // Approximate original file size }); }; const getTitle = () => { const seriesName = document.querySelector( ".p-episode__comic-ttl" ).innerText; const chapterName = document.querySelector( ".p-episode__header-ttl" ).innerText; const title = `${seriesName} ー ${chapterName}`; return title.replace(/[<>:"\/\|?*]/g, ""); }; const downloadChapter = async (progressBar) => { if (!selectedChapter) { throw new Error("No chapter found"); } const scrambleSeed = selectedChapter.scramble_seed; const pageList = selectedChapter.page_list; if (!scrambleSeed) { throw new Error("No scramble seed found for selected chapter"); } if (!pageList || !pageList.length) { throw new Error("No page found in selected chapter"); } const pageCount = pageList.length; const title = getTitle(); let pageCountProgress = 0; const updateDlProgress = () => { const percentage = Math.round( (++pageCountProgress / (pageCount * 3)) * 100 ); progressBar.style.width = percentage + "%"; }; let pageBitmaps; try { const pageBlobs = await Promise.all( pageList.map(async (pageUrl) => { // GM.xmlHttpRequest needed instead of native fetch to bypass same-origin policy const response = await GM.xmlHttpRequest({ url: pageUrl, responseType: "blob", }); updateDlProgress(); return response.response; }) ); // Convert blobs to image bitmaps to use with canvas pageBitmaps = await Promise.all( pageBlobs.map(async (blob) => { const pageBitmap = await createImageBitmap(blob); updateDlProgress(); return pageBitmap; }) ); } catch (error) { console.error(error); throw new Error("Couldn't retrieve chapter pages"); } const unscrambledPageBlobs = []; const unscrambledCoords = getUnscrambledCoords(scrambleSeed); try { for (const page of pageBitmaps) { const blob = await drawPage(page, unscrambledCoords); updateDlProgress(); unscrambledPageBlobs.push(blob); } } catch (error) { console.error(error); throw new Error("Couldn't unscramble chapter pages"); } try { await unsafeWindow.processZip( title, pageCount, unscrambledPageBlobs ); } catch (error) { console.error(error); throw new Error("Couldn't process zip file"); } }; const insertDlButton = (target) => { const dlButtonWrapper = document.createElement("div"); const progressBar = document.createElement("div"); const dlButton = document.createElement("button"); dlButtonWrapper.id = "dl-button-wrapper"; progressBar.id = "progress-bar"; dlButton.id = "dl-button"; dlButton.innerText = "Download Chapter"; dlButton.onclick = async () => { dlButton.disabled = true; dlButton.innerText = "Downloading..."; dlButton.classList = ["loading"]; const resetButton = (isSuccess) => { dlButton.disabled = false; dlButton.classList = isSuccess ? ["completed"] : []; dlButton.innerText = "Download Chapter"; progressBar.style.width = "0"; }; try { if (!selectedChapter) { await new Promise((resolve) => setTimeout(resolve, 2000)); } await downloadChapter(progressBar); setTimeout(() => resetButton(true), 500); } catch (error) { dlButton.innerText = "Download Failed"; dlButton.classList = ["fail"]; setTimeout(() => resetButton(false), 2000); setTimeout(() => { alert( `Error downloading chapter: ${error.message}\nReload the page and try again\nIf the issue persists, please report it in the feedback section of the script's homepage` ); }, 100); } }; target.appendChild(dlButtonWrapper); dlButtonWrapper.appendChild(progressBar); dlButtonWrapper.appendChild(dlButton); }; // The userscript runs in a sandbox since @grant is not "none", which breaks the behaviors of the required JSZip and FileSaver libraries // As a workaround, we handle the zip creation and downloading process in the page context directly and then access it through unsafeWindow const injectDomScripts = () => { const jszip = document.createElement("script"); const fileSaver = document.createElement("script"); jszip.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; fileSaver.src = "https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"; const processZipScript = document.createElement("script"); processZipScript.textContent = ` window.processZip = async (title, pageCount, unscrambledPageBlobs) => { const zip = new JSZip(); const padStartLength = pageCount.toString().length; unscrambledPageBlobs.forEach((page, index) => { const paddedFileName = (index + 1).toString().padStart(padStartLength, "0") + ".jpg"; zip.file(paddedFileName, page, { binary: true }); }); const zipBlob = await zip.generateAsync({ type: "blob" }); await saveAs(zipBlob, title + ".zip"); } `; document.head.appendChild(jszip); document.head.appendChild(fileSaver); document.head.appendChild(processZipScript); }; const injectDlButtonStyle = () => { const dlButtonStyle = document.createElement("style"); dlButtonStyle.textContent = ` @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } :root { --download-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3,14 v5 q0,2 2,2 h14 q2,0 2,-2 v-5 M7,10 l4,4 q1,1 2,0 l4,-4 M12,3 v11' fill='none' stroke='%230d3594' stroke-width='2' stroke-linecap='round'%3E%3C/path%3E%3C/svg%3E"); --loading-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='%231DA1F2' stroke-width='4' opacity='0.4'%3E%3C/circle%3E%3Cpath d='M12,2 a10,10 0 0 1 10,10' fill='none' stroke='%230d3594' stroke-width='4' stroke-linecap='round'%3E%3C/path%3E%3C/svg%3E"); --completed-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3,14 v5 q0,2 2,2 h14 q2,0 2,-2 v-5 M7,10 l3,4 q1,1 2,0 l8,-11' fill='none' stroke='%230d3594' stroke-width='2' stroke-linecap='round'%3E%3C/path%3E%3C/svg%3E"); --fail-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='11' fill='red' stroke='black' stroke-width='2' opacity='0.8'%3E%3C/circle%3E%3Cpath d='M14,5 a1,1 0 0 0 -4,0 l0.5,9.5 a1.5,1.5 0 0 0 3,0 z M12,17 a2,2 0 0 0 0,4 a2,2 0 0 0 0,-4' fill='%23fff' stroke='none'%3E%3C/path%3E%3C/svg%3E"); --locked-svg: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18 10.5C19.6569 10.5 21 11.8431 21 13.5V19.5C21 21.1569 19.6569 22.5 18 22.5H6C4.34315 22.5 3 21.1569 3 19.5V13.5C3 11.8431 4.34315 10.5 6 10.5V7.5C6 4.18629 8.68629 1.5 12 1.5C15.3137 1.5 18 4.18629 18 7.5V10.5ZM12 3.5C14.2091 3.5 16 5.29086 16 7.5V10.5H8V7.5C8 5.29086 9.79086 3.5 12 3.5ZM18 12.5H6C5.44772 12.5 5 12.9477 5 13.5V19.5C5 20.0523 5.44772 20.5 6 20.5H18C18.5523 20.5 19 20.0523 19 19.5V13.5C19 12.9477 18.5523 12.5 18 12.5Z' fill='%235e5e5e'/%3E%3C/svg%3E"); } .p-episode__sns { margin-top: 0.7rem; } .p-episode__rss-btn { margin-right: 0.8rem; } #dl-button-wrapper { position: relative; width: 180px; height: 35px; } #progress-bar { position: absolute; z-index: 1; top: 2px; bottom: 0; left: 2px; right: 0; width: 0; max-width: 176px; height: 31px; border-radius: 4px; background-color: #d4d4d4; } #dl-button { position: absolute; display: flex; align-items: center; justify-content: center; 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.5rem; font-weight: bold; color: #0d3594; border: 2px solid #0d3594; border-radius: 7px; cursor: pointer; transition: opacity 0.2s; } #dl-button:not(.loading):not(.locked):not(.fail):hover { opacity: 0.8; } #dl-button.loading { opacity: 0.8; cursor: default; } #dl-button.fail { color: red; border-color: red; cursor: default; } #dl-button.locked { color: #5e5e5e; border-color: #5e5e5e; cursor: default; } #dl-button::before { content: ""; display: inline-block; background-image: var(--download-svg); background-size: contain; background-repeat: no-repeat; width: 20px; height: 20px; margin-left: -5px; margin-right: 5px; } #dl-button.loading::before { background-image: var(--loading-svg); animation: spin 1s linear infinite; } #dl-button.completed::before { background-image: var(--completed-svg); } #dl-button.fail::before { background-image: var(--fail-svg); margin-right: 8px; } #dl-button.locked::before { background-image: var(--locked-svg); margin-bottom: 2px; } `; document.head.appendChild(dlButtonStyle); }; const checkChapter = (mutationList, observer) => { if (!location.pathname.match(/^\/title\/.*\/episode\/.*$/)) { return; } const target = document.querySelector(".p-episode__sns-btn-list"); if (!target) return; let dlButton = document.querySelector("#dl-button"); if (!dlButton) { insertDlButton(target); dlButton = document.querySelector("#dl-button"); } const isNotBought = document.querySelector(".p-episode-purchase"); 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"); } }; document.addEventListener("DOMContentLoaded", () => { injectDomScripts(); injectDlButtonStyle(); const observer = new MutationObserver(checkChapter); observer.observe(document.body, { childList: true, subtree: true }); }); })();