🏠 Home 

PixivComicDownloader

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);