Tora EBook Downloader

A userscript that allow you download all images in Tora EBook Viewer

// ==UserScript==
// @name         Tora EBook Downloader
// @namespace    http://tampermonkey.net/
// @version      0.12
// @description  A userscript that allow you download all images in Tora EBook Viewer
// @author       https://github.com/Amarillys
// @match        http://viewer.toraebook.com/imageviewer/index.php*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.9/dat.gui.min.js
// @icon         https://www.toranoana.jp/favicon.ico
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  const bookInfoApi = `http://viewer.toraebook.com/bookinfo_php73.php`
  const imageInfoApi = `http://viewer.toraebook.com/image_php73.php`
  
  window = unsafeWindow
  const bookCode = window.location.search.slice(1)

  const gui = new dat.GUI({
    autoPlace: false,
    useLocalStorage: false
  })
  
  let options = {
    progress: 0
  }

  const clickHandler = {
    text() {},
    download: () => {
      console.log('start downloading')
      main()
    }
  }
  gui.add(clickHandler, 'text').name('Tora-Ebook-Downloader')
  const progressCtl = gui.add(options, 'progress', 0, 100, 0.01).name('Progress')
  gui.add(clickHandler, 'download').name('download')
  gui.open()

  setTimeout(() => {
    gui.domElement.style['z-index'] = 10
    gui.domElement.style.position = 'fixed'
    gui.domElement.style.left = '100px'
    gui.domElement.style.top = '100px'
    window.document.body.appendChild(gui.domElement)
    window.document.getElementById('page0-screen').remove()
  }, 1500)

  async function main() {
    const bookInfoXMLStr = await gmRequireText(`${bookInfoApi}?${bookCode}`)
    const parser = new DOMParser()
    const bookInfo = parser.parseFromString(bookInfoXMLStr, `application/xml`)
    const imageList = Array.from(bookInfo.documentElement.children[1].children[2].children)
    const zip = new JSZip()
    let total = 0
    
    for (let i = 0; i < imageList.length; ++i) {
      const imageInfoURL = `${imageInfoApi}?sp=${bookCode}&x1=${imageList[i].children[7].innerHTML}&x2=${imageList[i].children[8].innerHTML}&t=${9999999999 * Math.random()}`
      const imageInfoText = await gmRequireText(imageInfoURL)
      const imageInfo = JSON.parse(imageInfoText)
      const imageURL = imageInfo.image
      let imageBlockInfo = imageInfo.x
      const image = await gmRequire(imageURL)
      const imageBlobURL = URL.createObjectURL(image)

      const imageEl = await loadImage(imageBlobURL)
      const canvasEl = window.document.createElement('canvas')
      canvasEl.style.position = 'fixed'
      canvasEl.style.left = 0
      canvasEl.width = imageEl.width
      canvasEl.height = imageEl.height
      window.document.body.appendChild(canvasEl)
      const ctx = canvasEl.getContext('2d')
      
      imageBlockInfo = imageBlockInfo.slice(1)
      for (let j = 0; j < imageBlockInfo.length - 1; ++j) {
        ctx.drawImage(imageEl, imageBlockInfo[j][2], imageBlockInfo[j][3], imageBlockInfo[j][4], imageBlockInfo[j][5],
            imageBlockInfo[j][0], imageBlockInfo[j][1], imageBlockInfo[j][4], imageBlockInfo[j][5])
      }
      canvasEl.toBlob(blob => {
        zip.folder(purifyName(window.document.title)).file(`${purifyName(i)}.jpg`, blob, {
          compression: 'STORE'
        })
        total++
        setTimeout(() => canvasEl.remove(), 1200)
        progressCtl.setValue(total / imageList.length * 100)
        if (total === imageList.length) {
          zip.generateAsync({
            type: 'blob',
            compression: 'STORE'
          }).then(zipBlob => saveBlob(zipBlob, `${purifyName(window.document.title)}.zip`))
        }
      }, "image/jpeg", 0.95)
    }
  }
  
  function gmRequire(url, index, overrideMimeType, responseType) {
    return new Promise((resolve) =>
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        overrideMimeType: overrideMimeType || 'image/jpeg',
        responseType: responseType || 'blob',
        asynchrouns: true,
        credentials: "include",
        onload: res => {
          if (responseType === 'json') resolve(res.responseText)
          else resolve(res.response)
        },
        onerror: console.error
      })
    )
  }

  function gmRequireText(url, index) {
    return gmRequire(url, index, 'application/json', 'json')
  }
  
  function loadImage(url) {
    return new Promise(r => { let i = new Image(); i.onload = (() => r(i)); i.src = url; });
  }

  function purifyName(filename) {
    return (filename + '').replaceAll(':', '').replaceAll('/', '').replaceAll('\\', '').replaceAll('>', '').replaceAll('<', '')
        .replaceAll('*:', '').replaceAll('|', '').replaceAll('?', '').replaceAll('"', '')
  }

  function saveBlob(blob, fileName) {
    let downloadDom = document.createElement('a')
    document.body.appendChild(downloadDom)
    downloadDom.style = `display: none`
    let url = window.URL.createObjectURL(blob)
    downloadDom.href = url
    downloadDom.download = fileName
    downloadDom.click()
    window.URL.revokeObjectURL(url)
  }
})();