/** The MIT License (MIT) Copyright (c) 2021 shadows Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. **/ // ==UserScript== // @name comic-walker 漫画下载 // @namespace shadows // @version 0.1.1 // @description 下载comic-walker网站的免费漫画 // @author shadows // @license MIT License // @copyright Copyright (c) 2021 shadows // @icon https://dimg04.c-ctrip.com/images/0391j120008r0n8a84D94.png // @icon64 https://static.yximgs.com/bs2/adInnovationResource/367c797d005b4b1ab180f0a361a7ef43.png // @match https://comic-walker.com/contents/detail/* // @require https://cdn.jsdelivr.net/npm/jszip@3.6.0/dist/jszip.min.js // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.2/dist/FileSaver.min.js // @downloadURL none // ==/UserScript== "use strict"; addButton() function addButton() { let targets = document.querySelectorAll(".acBacknumber-item-leftbox"); console.log(targets); for (let elem of targets) { if (elem.parentNode.querySelector(".download-button")) continue; let name = elem.querySelector(".acBacknumber-title").textContent; let href = new URL(elem.parentNode.querySelector('a').href); let params = href.searchParams; let cid = params.get('cid'); elem.after(creatButton(name, cid)); } } function creatButton(name, cid) { let button = document.createElement('button'); button.classList.add("download-button"); button.textContent = "Download"; button.style.cssText = `z-index: 2; background: linear-gradient(135deg, #6e8efb, #a777e3); color: white; padding: 3px 3px; margin: 4px 0px; text-align: center; border-radius: 3px; border-width: 0px;`; button.dataset.cid = cid; button.dataset.name = name; button.onclick = download; return button; } async function download(event) { event.stopPropagation(); event.preventDefault(); let elem = event.target; let name = elem.dataset.name; let cid = elem.dataset.cid; let imagesData = await getImageSData(cid); let images = await downloadImages(imagesData, name); let zip = new JSZip(); for (let image of images) { let decrypted = decryptImage(image); zip.file(`${image.id}.jpg`, decrypted); } zip.generateAsync({ type: "blob", base64: true }).then(content => saveAs(content, `${name}.zip`)); } async function getImageSData(cid) { return fetch(`https://comicwalker-api.nicomanga.jp/api/v1/comicwalker/episodes/${cid}/frames`) .then(resp => resp.json()) .then(json => { let dataArray = json.data.result; return dataArray.map((item, index) => ({ drm_hash: item.meta.drm_hash, source_url: item.meta.source_url, id: index + 1 })) }); } function downloadImages(imagesData, name) { async function downloadSingleImage(item) { return fetch(item.source_url).then(resp => resp.arrayBuffer()).then(arrayBuffer => { console.log(`${name}-${item.id} have downloaded.`); //返回包含id drm_hash与图片数据的对象 return { id: item.id, drm_hash: item.drm_hash, content: arrayBuffer }; }); } let images = asyncPool(10, imagesData, downloadSingleImage); return images; } /** * 解密图片 * @param {Object} image * @param {String} image.drm_hash 解密密钥 * @param {ArrayBuffer} image.content 图片数据 * @returns {Blob} 已解密的图片Blob对象 */ function decryptImage({ drm_hash, content }) { if (drm_hash == null) return new Blob([content], { type: "image/jpeg" }); let key = generateKey(drm_hash); let contentUint8Array = arrayBufferToUint8(content); let decryptedUin8Array = xor(contentUint8Array, key); return new Blob([decryptedUin8Array], { type: "image/jpeg" }); } function generateKey(drm_hash) { drm_hash = drm_hash.slice(0, 16).match(/[\da-f]{2}/gi); return new Uint8Array(drm_hash.map(i => parseInt(i, 16))); } /** * xor解密 * @param {Uint8Array} content 待解密的内容 * @param {Uint8Array} key 密钥 * @returns Uint8Array 结果 */ function xor(content, key) { let result = new Uint8Array(content.length); let keyLength = key.length; for (let i = 0; i < content.length; i++) { result[i] = content[i] ^ key[i % keyLength]; } return result; } function arrayBufferToUint8(arrayBuffer) { return new Uint8Array(arrayBuffer); } /** * @param poolLimit 并发控制数 (>= 1) * @param array 参数数组 * @param iteratorFn 异步任务,返回 promise 或是 async 方法 * https://www.luanzhuxian.com/post/60c2c548.html */ function asyncPool(poolLimit, array, iteratorFn) { let i = 0 const ret = [] // Promise.all(ret) 的数组 const executing = [] const enqueue = function() { // array 遍历完,进入 Promise.all 流程 if (i === array.length) { return Promise.resolve() } // 每调用一次 enqueue,就初始化一个 promise,并放入 ret 队列 const item = array[i++] const p = Promise.resolve().then(() => iteratorFn(item, array)) ret.push(p) // 插入 executing 队列,即正在执行的 promise 队列,并且 promise 执行完毕后,会从 executing 队列中移除 const e = p.then(() => executing.splice(executing.indexOf(e), 1)) executing.push(e) // 每当 executing 数组中 promise 数量达到 poolLimit 时,就利用 Promise.race 控制并发数,完成的 promise 会从 executing 队列中移除,并触发 Promise.race 也就是 r 的回调,继续递归调用 enqueue,继续 加入新的 promise 任务至 executing 队列 let r = Promise.resolve() if (executing.length >= poolLimit) { r = Promise.race(executing) } // 递归,链式调用,直到遍历完 array return r.then(() => enqueue()) } return enqueue().then(() => Promise.all(ret)) }