Manga downloader for read.amazon.co.jp
- // ==UserScript==
- // @name KindleMangaDownloader
- // @namespace https://github.com/Timesient/manga-download-scripts
- // @version 0.6
- // @license GPL-3.0
- // @author Timesient
- // @description Manga downloader for read.amazon.co.jp
- // @icon https://m.media-amazon.com/images/G/01/kfw/mobile/kindle_favicon.png
- // @homepageURL https://greasyfork.org/scripts/451870-kindlemangadownloader
- // @supportURL https://github.com/Timesient/manga-download-scripts/issues
- // @match https://read.amazon.co.jp/manga/*
- // @require https://unpkg.com/axios@0.27.2/dist/axios.min.js
- // @require https://unpkg.com/jszip@3.7.1/dist/jszip.min.js
- // @require https://unpkg.com/file-saver@2.0.5/dist/FileSaver.min.js
- // @require https://update.greasyfork.org/scripts/451810/1398192/ImageDownloaderLib.js
- // @grant GM_info
- // @grant GM_xmlhttpRequest
- // ==/UserScript==
- (async function(axios, JSZip, saveAs, ImageDownloader) {
- 'use strict';
- // collect essential data
- const { asin, revision, pageAmount } = await new Promise(resolve => {
- const timer = setInterval(() => {
- const target1 = document.querySelector('#requestData');
- const target2 = document.querySelector('#bookInfo');
- const target3 = document.querySelector('#pageInfoCurrentPage');
- const target4 = document.querySelector('#pageInfoTotalPage');
- if (target1 && target2 && target3 && target4 && parseInt(target3.textContent) !== 0 && parseInt(target4.textContent) !== 1) {
- clearInterval(timer);
- resolve({
- asin: JSON.parse(target1.textContent).asin,
- revision: JSON.parse(target2.textContent).contentGuid,
- pageAmount: parseInt(target4.textContent)
- });
- }
- }, 200);
- });
- // get book info and build parameters
- const bookInfo = await axios.get(`https://read.amazon.co.jp/api/manga/open-next-book/${asin}`).then(res => res.data);
- const params = {
- version: '3.0',
- asin,
- contentType: 'FullBook',
- revision,
- fontFamily: 'Bookerly',
- fontSize: 4.95,
- lineHeight: 1.4,
- dpi: 160,
- height: 923,
- width: 400,
- maxNumberColumns: 2,
- theme: 'dark',
- packageType: 'TAR',
- numPage: -1 * pageAmount,
- skipPageCount: pageAmount,
- startingPosition: 0,
- token: bookInfo.karamelToken.token
- }
- // get pages
- const pages = [].concat(
- await getPages(params),
- await getPages({ ...params, numPage: 1 }),
- );
- // setup ImageDownloader
- ImageDownloader.init({
- maxImageAmount: pages.length,
- getImagePromises,
- title: bookInfo.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 decrypted image
- async function getDecryptedImage(page) {
- const src = `${page.baseUrl}/${page.url}?${page.authParameter}&token=${encodeURIComponent(bookInfo.karamelToken.token)}&expiration=${encodeURIComponent(bookInfo.karamelToken.expiresAt)}`;
- const encryptedBuffer = await fetch(src).then(res => res.arrayBuffer());
- const decryptedBuffer = await getDecryptedBuffer(encryptedBuffer, bookInfo.karamelToken);
- return new Blob([decryptedBuffer]);
- }
- async function getDecryptedBuffer(t, e) {
- const n = new TextDecoder('utf-8');
- const o = new TextEncoder;
- const r = n.decode(t);
- const a = r.slice(0, 24);
- const c = r.slice(24, 48);
- const h = r.slice(48, r.length);
- const l = base64StringToArrayBuffer(a);
- const d = base64StringToArrayBuffer(c);
- const u = base64StringToArrayBuffer(h);
- const g = getKey(e);
- const v = await window.crypto.subtle.importKey("raw", o.encode(g), { name: "PBKDF2" }, false, ["deriveBits", "deriveKey"]);
- const p = await window.crypto.subtle.deriveKey({ name: "PBKDF2", salt: l, iterations: 1e3, hash: "SHA-256" }, v, { name: "AES-GCM", length: 128 }, false, ["decrypt"]);
- return await window.crypto.subtle.decrypt({
- name: "AES-GCM",
- iv: d,
- additionalData: o.encode(g.slice(0, 9)),
- tagLength: 128
- }, p, u);
- }
- function base64StringToArrayBuffer(base64) {
- const origin = window.atob(base64);
- const r###lt = new Uint8Array(origin.length);
- for (let i = 0; i < origin.length; i++) {
- r###lt[i] = origin.charCodeAt(i);
- }
- return r###lt.buffer;
- }
- function getKey(t) {
- if (t.token.length < 100) throw new Error('error in getKey');
- const i = t.expiresAt % 60;
- return t.token.substring(i, i + 40);
- }
- // request pages from API
- async function getPages(params) {
- const apiURL = `https://read.amazon.co.jp/renderer/render?${new URLSearchParams(params)}`;
- const tarString = await axios.get(apiURL).then(res => res.data.replaceAll('\u0000', ''));
- const manifest = JSON.parse(`{` + tarString.match(/"cdnResources".*"acr"/)[0] + `: "acr content" }`);
- return manifest.cdnResources.map(page => {
- page.baseUrl = manifest.cdn.baseUrl;
- page.authParameter = manifest.cdn.authParameter;
- page.order = parseInt(page.url.replace('resource/rsrc', ''), 36);
- return page;
- }).sort((a, b) => a.order - b.order);
- }
- })(axios, JSZip, saveAs, ImageDownloader);