// ==UserScript== // @name Comic Fuz Downloader // @namespace http://circleliu.cn // @version 0.3.4 // @description Userscript for download comics on Comic Fuz // @author Circle // @license MIT // @match https://comic-fuz.com/book/viewer* // @match https://comic-fuz.com/magazine/viewer* // @match https://comic-fuz.com/manga/viewer* // @run-at document-start // @grant none // @require https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.js // @require https://unpkg.com/axios/dist/axios.min.js // @require https://unpkg.com/jszip@3.6.0/dist/jszip.min.js // @require https://unpkg.com/jszip-utils@0.1.0/dist/jszip-utils.min.js // @require https://unpkg.com/jszip@3.6.0/vendor/FileSaver.js // @require https://unpkg.com/jquery@3.6.0/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/protobufjs@6.11.2/dist/protobuf.min.js // @require https://greasyfork.org/scripts/435461-comic-fuz-downloader-protobuf-message/code/Comic%20Fuz%20Downloader%20Protobuf%20Message.js?version=987894 // @homepageURL https://circleliu.github.io/Comic-Fuz-Downloader/ // @supportURL https://github.com/CircleLiu/Comic-Fuz-Downloader // @downloadURL none // ==/UserScript== ;(function () { 'use strict' const api = getApi() const imgBaseUrl = 'https://img.comic-fuz.com' const responseDecoder = { 'book_viewer_2': api.v1.BookViewer2Response, 'book_viewer': api.v1.BookViewer2Response, 'magazine_viewer_2': api.v1.MagazineViewer2Response, 'magazine_viewer': api.v1.MagazineViewerResponse, 'manga_viewer': api.v1.MangaViewerResponse, } const oldFetch = window.fetch window.fetch = async (input, options) => { const response = await oldFetch(input, options) for (const i in responseDecoder) { if (input.indexOf(i) > -1) { const resClone = response.clone() decodeResponse(resClone, responseDecoder[i]) break; } } return response } let metadata async function decodeResponse(response, decoder) { const data = await response.arrayBuffer() const res = decoder.decode(new Uint8Array(data)) metadata = res } async function decryptImage({imageUrl, encryptionKey, iv}) { const res = await axios.get(imgBaseUrl + imageUrl, { responseType: 'arraybuffer', }) const cipherParams = CryptoJS.lib.CipherParams.create({ ciphertext: CryptoJS.lib.WordArray.create(res.data) }) const key = CryptoJS.enc.Hex.parse(encryptionKey) const _iv = CryptoJS.enc.Hex.parse(iv) const dcWordArray = CryptoJS.AES.decrypt(cipherParams, key, { iv: _iv, mode: CryptoJS.mode.CBC, }) return dcWordArray.toString(CryptoJS.enc.Base64) } $(document).ready($ => { const downloadIcon = 'https://circleliu.github.io/Comic-Fuz-Downloader/icons/download.png' const loadingIcon = 'https://circleliu.github.io/Comic-Fuz-Downloader/icons/loading.gif' // const downloadIcon = 'http://localhost:5000/icons/download.png' // const loadingIcon = 'http://localhost:5000/icons/loading.gif' const divDownload = $(`
Initializing
`) divDownload.css({ 'grid-area': 'hoge', color: '#2c3438', width: 'fit-content', cursor: 'pointer', }) divDownload.on('click', async () => { setDownloaderBusy() try { await downloadAsZip() setDownloaderReady() } catch (error) { console.error(error) setDownloaderReady() setText(error.message) } }) function setDownloaderReady() { $('#downloaderIcon').show() $('#downloadingIcon').hide() setText('Download') } function setDownloaderBusy() { $('#downloaderIcon').hide() $('#downloadingIcon').show() } function setText(text) { $('#downloaderText').text(text) } function updateDownloadProgress(progress) { setText(`Loading: ${progress.done}/${progress.total}`) } const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) const maxRetry = 10 ;(async () => { for (let i = 0; i < maxRetry; ++i) { if ($('div[class^="ViewerFooter_footer__"]').length) { $('div[class^="ViewerFooter_footer__"]:first').append(divDownload) setDownloaderReady() break } else { await delay(500) } } })() async function downloadAsZip() { if (!metadata) { throw new Error('Failed to load data!') } const zipName = getNameFromMetadata() const zip = new JSZip() if (metadata.tableOfContents){ zip.file('TableOfContents.txt', JSON.stringify(metadata.tableOfContents, null, ' ')) } const progress = { total: 0, done: 0, } const promises = metadata.pages.map(({image}, i) => { if (image){ progress.total++ return getImageToZip(image, zip, progress, i) } }) await Promise.all(promises) const content = await zip.generateAsync({ type: 'blob' }, ({ percent }) => { setText(`Packaging: ${percent.toFixed(2)}%`) }) saveAs(content, `${zipName}.zip`) } function getNameFromMetadata() { if (metadata.bookIssue) { return metadata.bookIssue.bookIssueName.trim() } else if (metadata.viewerTitle) { return metadata.viewerTitle.trim() } else if (metadata.magazineIssue) { return metadata.magazineIssue.magazineName.trim() + ' ' + metadata.magazineIssue.magazineIssueName.trim() } } async function getImageToZip(image, zip, progress, index) { const fileName = `${index}.jpeg` try { const imageData = await decryptImage(image) addImageToZip(fileName, imageData, zip) } catch (err) { console.error(err) } if (progress) { progress.done++ updateDownloadProgress(progress) } } function addImageToZip(name, base64Data, zip) { zip.file(name, base64Data, { base64: true, }) } }) })()