Manga downloader for comic.pixiv.net
// ==UserScript== // @name PixivComicDownloader // @namespace https://github.com/Timesient/manga-download-scripts // @version 1.1 // @license GPL-3.0 // @author Timesient // @description Manga downloader for comic.pixiv.net // @icon https://comic.pixiv.net/static/images/icons/icon-192x192.png // @homepageURL https://greasyfork.org/scripts/451877-pixivcomicdownloader // @supportURL https://github.com/Timesient/manga-download-scripts/issues // @match https://comic.pixiv.net/* // @require https://unpkg.com/[email protected]/dist/axios.min.js // @require https://unpkg.com/[email protected]/dist/jszip.min.js // @require https://unpkg.com/[email protected]/dist/FileSaver.min.js // @require https://unpkg.com/[email protected]/crypto-js.js // @require https://update.greasyfork.org/scripts/451810/1398192/ImageDownloaderLib.js // @grant GM_info // @grant GM_xmlhttpRequest // ==/UserScript== (async function(axios, JSZip, saveAs, CryptoJS, ImageDownloader) { 'use strict'; // reload page when enter or leave chapter const re = /https:\/\/comic\.pixiv\.net\/viewer\/stories\/.*/; const oldHref = window.location.href; const timer = setInterval(() => { const newHref = window.location.href; if (newHref === oldHref) return; if (re.test(newHref) || re.test(oldHref)) { clearInterval(timer); window.location.reload(); } }, 200); // return if not reading chapter now if (!re.test(oldHref)) return; // get salt const salt = await new Promise(resolve => { const timer = setInterval(() => { const salt = unsafeWindow?.__NEXT_DATA__?.props?.pageProps?.salt; if (salt) { clearInterval(timer); resolve(salt); } }, 200); }); // generate time string and hash const now = new Date(); const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, '0'); const date = now.getDate().toString().padStart(2, '0'); const hour = now.getHours().toString().padStart(2, '0'); const minute = now.getMinutes().toString().padStart(2, '0'); const second = now.getSeconds().toString().padStart(2, '0'); const time = `${year}-${month}-${date}T${hour}:${minute}:${second}+08:00`; const hash = CryptoJS.SHA256(`${time}${salt}`).toString(); // get title and pages const { title, pages } = await axios({ method: 'GET', url: `https://comic.pixiv.net/api/app/episodes/${window.location.pathname.split('/').pop()}/read_v4`, headers: { 'x-client-time': time, 'x-client-hash': hash, 'x-requested-with': 'pixivcomic' } }).then(res => res.data.data.reading_episode); // setup ImageDownloader ImageDownloader.init({ maxImageAmount: pages.length, getImagePromises, title }); // collect promises of image function getImagePromises(startNum, endNum) { return pages .slice(startNum - 1, endNum) .map(page => getDecryptedImage(page) .then(ImageDownloader.fulfillHandler) .catch(ImageDownloader.rejectHandler) ); } // get promise of decrypted image async function getDecryptedImage(page) { return new Promise(async resolve => { // get image in arraybuffer const imageArrayBuffer = await axios.get(page.url, { headers: { 'X-Cobalt-Thumber-Parameter-Gridshuffle-Key': page.key }, responseType: 'arraybuffer' }).then(res => res.data); // load image on canvas const image = document.createElement('img'); image.src = 'data:image/jpg;base64,' + window.btoa(new Uint8Array(imageArrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '')); image.onload = async function () { // create canvas const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = page.width; canvas.height = page.height; ctx.drawImage(image, 0, 0); // process image data const imageData = ctx.getImageData(0, 0, page.width, page.height); const decryptedImageData = await decryptImage(imageData.data, 4, page.width, page.height, page.gridsize, page.gridsize, "4wXCKprMMoxnyJ3PocJFs4CYbfnbazNe", page.key, true); ctx.putImageData(new ImageData(decryptedImageData, page.width, page.height), 0, 0); canvas.toBlob(resolve); } }); } // scramble code extracted from scripts loaded by page async function decryptImage(e, t, r, i, s, n, a, l, o) { function tE(e, t) { return (e << (t %= 32) >>> 0 | e >>> 32 - t) >>> 0 } class tS { next() { let e = 9 * tE(5 * this.s[1] >>> 0, 7) >>> 0, t = this.s[1] << 9 >>> 0; return this.s[2] = (this.s[2] ^ this.s[0]) >>> 0, this.s[3] = (this.s[3] ^ this.s[1]) >>> 0, this.s[1] = (this.s[1] ^ this.s[2]) >>> 0, this.s[0] = (this.s[0] ^ this.s[3]) >>> 0, this.s[2] = (this.s[2] ^ t) >>> 0, this.s[3] = tE(this.s[3], 11), e } constructor(e) { if (4 !== e.length) throw Error("seed.length !== 4 (seed.length: ".concat(e.length, ")")); this.s = new Uint32Array(e), 0 === this.s[0] && 0 === this.s[1] && 0 === this.s[2] && 0 === this.s[3] && (this.s[0] = 1) } } if (t <= 0 || r <= 0 || i <= 0 || s <= 0 || n <= 0) throw Error("bytesPerElement <= 0 || width <= 0 || height <= 0 || blockSizeH <= 0 || blockSizeV <= 0 (bytesPerElement: ".concat(t, ", width: ").concat(r, ", height: ").concat(i, ", blockSizeH: ").concat(s, ", blockSizeV: ").concat(n, ")")); if (!Number.isSafeInteger(t) || !Number.isSafeInteger(r) || !Number.isSafeInteger(i) || !Number.isSafeInteger(s) || !Number.isSafeInteger(n)) throw Error("!Number.isSafeInteger(bytesPerElement) || !Number.isSafeInteger(width) || !Number.isSafeInteger(height) || !Number.isSafeInteger(blockSizeH) || !Number.isSafeInteger(blockSizeV) (bytesPerElement: ".concat(t, ", width: ").concat(r, ", height: ").concat(i, ", blockSizeH: ").concat(s, ", blockSizeV: ").concat(n, ")")); if (e.length !== r * i * t) throw Error("data.length !== width * height * bytesPerElement (data.length: ".concat(e.length, ", width: ").concat(r, ", height: ").concat(i, ", bytesPerElement: ").concat(t, ")")); let d = Math.ceil(i / n), c = Math.floor(r / s), u = Array(d).fill(null).map(() => Array.from(Array(c).keys())); { let e = new TextEncoder().encode(a + l), t = await crypto.subtle.digest("SHA-256", e), r = new Uint32Array(t, 0, 4), i = new tS(r); for (let e = 0; e < 100; e++) i.next(); for (let e = 0; e < d; e++) { let t = u[e]; for (let e = c - 1; e >= 1; e--) { let r = i.next() % (e + 1), s = t[e]; t[e] = t[r], t[r] = s } } } if (o) for (let e = 0; e < d; e++) { let t = u[e], r = t.map((e, r) => t.indexOf(r)); if (r.some(e => e < 0)) throw Error("Failed to reverse shuffle table"); u[e] = r } let h = new Uint8ClampedArray(e.length); for (let a = 0; a < i; a++) { let i = Math.floor(a / n), l = u[i]; for (let i = 0; i < c; i++) { let n = l[i], o = i * s, d = (a * r + o) * t, c = n * s, u = (a * r + c) * t, p = s * t; for (let t = 0; t < p; t++) h[d + t] = e[u + t] } { let i = c * s, n = (a * r + i) * t, l = (a * r + r) * t; for (let t = n; t < l; t++) h[t] = e[t] } } return h } })(axios, JSZip, saveAs, CryptoJS, ImageDownloader);