Comic Fuz Downloader

Userscript for download comics on Comic Fuz

目前为 2021-11-19 提交的版本。查看 最新版本

// ==UserScript==
// @name              Comic Fuz Downloader
// @namespace         http://circleliu.cn
// @version           0.3.2
// @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/[email protected]/crypto-js.js
// @require           https://unpkg.com/axios/dist/axios.min.js
// @require           https://unpkg.com/[email protected]/dist/jszip.min.js
// @require           https://unpkg.com/[email protected]/dist/jszip-utils.min.js
// @require           https://unpkg.com/[email protected]/vendor/FileSaver.js
// @require           https://unpkg.com/[email protected]/dist/jquery.min.js
// @require           https://cdn.jsdelivr.net/npm/[email protected]/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
// ==/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 = $(`
      <div id="downloader">
        <img id="downloaderIcon" src="${downloadIcon}">
        <img id="downloadingIcon" src="${loadingIcon}">
        <span id="downloaderText">Initializing</span>
      </div>
    `)
    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 ($('.ViewerFooter_footer__3E55F').length) {
          $('.ViewerFooter_footer__3E55F: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 = image.imageUrl.slice(image.imageUrl.lastIndexOf('/') + 1, image.imageUrl.indexOf('?')).replace('.enc', '')
      const fileName = `${index}.jpeg`
      const imageData = await decryptImage(image)
      addImageToZip(fileName, imageData, zip)
      if (progress) {
        progress.done++
        updateDownloadProgress(progress)
      }
    }
  
    function addImageToZip(name, base64Data, zip) {
      zip.file(name, base64Data, {
        base64: true,
      })
    }
  })
})()