🏠 Home 

KindleMangaDownloader

Manga downloader for read.amazon.co.jp


安装此脚本?
  1. // ==UserScript==
  2. // @name KindleMangaDownloader
  3. // @namespace https://github.com/Timesient/manga-download-scripts
  4. // @version 0.6
  5. // @license GPL-3.0
  6. // @author Timesient
  7. // @description Manga downloader for read.amazon.co.jp
  8. // @icon https://m.media-amazon.com/images/G/01/kfw/mobile/kindle_favicon.png
  9. // @homepageURL https://greasyfork.org/scripts/451870-kindlemangadownloader
  10. // @supportURL https://github.com/Timesient/manga-download-scripts/issues
  11. // @match https://read.amazon.co.jp/manga/*
  12. // @require https://unpkg.com/axios@0.27.2/dist/axios.min.js
  13. // @require https://unpkg.com/jszip@3.7.1/dist/jszip.min.js
  14. // @require https://unpkg.com/file-saver@2.0.5/dist/FileSaver.min.js
  15. // @require https://update.greasyfork.org/scripts/451810/1398192/ImageDownloaderLib.js
  16. // @grant GM_info
  17. // @grant GM_xmlhttpRequest
  18. // ==/UserScript==
  19. (async function(axios, JSZip, saveAs, ImageDownloader) {
  20. 'use strict';
  21. // collect essential data
  22. const { asin, revision, pageAmount } = await new Promise(resolve => {
  23. const timer = setInterval(() => {
  24. const target1 = document.querySelector('#requestData');
  25. const target2 = document.querySelector('#bookInfo');
  26. const target3 = document.querySelector('#pageInfoCurrentPage');
  27. const target4 = document.querySelector('#pageInfoTotalPage');
  28. if (target1 && target2 && target3 && target4 && parseInt(target3.textContent) !== 0 && parseInt(target4.textContent) !== 1) {
  29. clearInterval(timer);
  30. resolve({
  31. asin: JSON.parse(target1.textContent).asin,
  32. revision: JSON.parse(target2.textContent).contentGuid,
  33. pageAmount: parseInt(target4.textContent)
  34. });
  35. }
  36. }, 200);
  37. });
  38. // get book info and build parameters
  39. const bookInfo = await axios.get(`https://read.amazon.co.jp/api/manga/open-next-book/${asin}`).then(res => res.data);
  40. const params = {
  41. version: '3.0',
  42. asin,
  43. contentType: 'FullBook',
  44. revision,
  45. fontFamily: 'Bookerly',
  46. fontSize: 4.95,
  47. lineHeight: 1.4,
  48. dpi: 160,
  49. height: 923,
  50. width: 400,
  51. maxNumberColumns: 2,
  52. theme: 'dark',
  53. packageType: 'TAR',
  54. numPage: -1 * pageAmount,
  55. skipPageCount: pageAmount,
  56. startingPosition: 0,
  57. token: bookInfo.karamelToken.token
  58. }
  59. // get pages
  60. const pages = [].concat(
  61. await getPages(params),
  62. await getPages({ ...params, numPage: 1 }),
  63. );
  64. // setup ImageDownloader
  65. ImageDownloader.init({
  66. maxImageAmount: pages.length,
  67. getImagePromises,
  68. title: bookInfo.title
  69. });
  70. // collect promises of image
  71. function getImagePromises(startNum, endNum) {
  72. return pages
  73. .slice(startNum - 1, endNum)
  74. .map(page => getDecryptedImage(page)
  75. .then(ImageDownloader.fulfillHandler)
  76. .catch(ImageDownloader.rejectHandler)
  77. )
  78. }
  79. // get decrypted image
  80. async function getDecryptedImage(page) {
  81. const src = `${page.baseUrl}/${page.url}?${page.authParameter}&token=${encodeURIComponent(bookInfo.karamelToken.token)}&expiration=${encodeURIComponent(bookInfo.karamelToken.expiresAt)}`;
  82. const encryptedBuffer = await fetch(src).then(res => res.arrayBuffer());
  83. const decryptedBuffer = await getDecryptedBuffer(encryptedBuffer, bookInfo.karamelToken);
  84. return new Blob([decryptedBuffer]);
  85. }
  86. async function getDecryptedBuffer(t, e) {
  87. const n = new TextDecoder('utf-8');
  88. const o = new TextEncoder;
  89. const r = n.decode(t);
  90. const a = r.slice(0, 24);
  91. const c = r.slice(24, 48);
  92. const h = r.slice(48, r.length);
  93. const l = base64StringToArrayBuffer(a);
  94. const d = base64StringToArrayBuffer(c);
  95. const u = base64StringToArrayBuffer(h);
  96. const g = getKey(e);
  97. const v = await window.crypto.subtle.importKey("raw", o.encode(g), { name: "PBKDF2" }, false, ["deriveBits", "deriveKey"]);
  98. const p = await window.crypto.subtle.deriveKey({ name: "PBKDF2", salt: l, iterations: 1e3, hash: "SHA-256" }, v, { name: "AES-GCM", length: 128 }, false, ["decrypt"]);
  99. return await window.crypto.subtle.decrypt({
  100. name: "AES-GCM",
  101. iv: d,
  102. additionalData: o.encode(g.slice(0, 9)),
  103. tagLength: 128
  104. }, p, u);
  105. }
  106. function base64StringToArrayBuffer(base64) {
  107. const origin = window.atob(base64);
  108. const r###lt = new Uint8Array(origin.length);
  109. for (let i = 0; i < origin.length; i++) {
  110. r###lt[i] = origin.charCodeAt(i);
  111. }
  112. return r###lt.buffer;
  113. }
  114. function getKey(t) {
  115. if (t.token.length < 100) throw new Error('error in getKey');
  116. const i = t.expiresAt % 60;
  117. return t.token.substring(i, i + 40);
  118. }
  119. // request pages from API
  120. async function getPages(params) {
  121. const apiURL = `https://read.amazon.co.jp/renderer/render?${new URLSearchParams(params)}`;
  122. const tarString = await axios.get(apiURL).then(res => res.data.replaceAll('\u0000', ''));
  123. const manifest = JSON.parse(`{` + tarString.match(/"cdnResources".*"acr"/)[0] + `: "acr content" }`);
  124. return manifest.cdnResources.map(page => {
  125. page.baseUrl = manifest.cdn.baseUrl;
  126. page.authParameter = manifest.cdn.authParameter;
  127. page.order = parseInt(page.url.replace('resource/rsrc', ''), 36);
  128. return page;
  129. }).sort((a, b) => a.order - b.order);
  130. }
  131. })(axios, JSZip, saveAs, ImageDownloader);