BookliveDownloader

Manga downloader for booklive.jp

// ==UserScript==
// @name         BookliveDownloader
// @namespace    https://github.com/Timesient/manga-download-scripts
// @version      0.7
// @license      GPL-3.0
// @author       Timesient
// @description  Manga downloader for booklive.jp
// @icon         https://booklive.jp/favicon.ico
// @homepageURL  https://greasyfork.org/scripts/452562-booklivedownloader
// @supportURL   https://github.com/Timesient/manga-download-scripts/issues
// @match        https://booklive.jp/bviewer/*
// @require      https://unpkg.com/[email protected]/dist/axios.min.js
// @require      https://unpkg.com/[email protected]/dist/jszip.min.js
// @require      https://unpkg.com/[email protected]/dist/FileSaver.min.js
// @require      https://update.greasyfork.org/scripts/451810/1398192/ImageDownloaderLib.js
// @require      https://update.greasyfork.org/scripts/456423/1128886/SpeedReaderTools.js
// @grant        GM_info
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(async function(axios, JSZip, saveAs, ImageDownloader, SpeedReaderTools) {
  'use strict';

  // collect essential params
  const cid = new URL(window.location.href).searchParams.get('cid');
  const randomString = SpeedReaderTools.generateRandomString32(cid);

  // generate config data
  const config = await axios({
    method: 'GET',
    url: `https://booklive.jp/bib-api/bibGetCntntInfo?cid=${cid}&dmytime=${Date.now()}&k=${randomString}`
  }).then(res => {
    const data = res.data.items[0];
    return {
      title: data.Title,
      contentServer: data.ContentsServer,
      ctbl: SpeedReaderTools.getDecryptedTable(cid, randomString, data.ctbl),
      ptbl: SpeedReaderTools.getDecryptedTable(cid, randomString, data.ptbl),
      p: data.p
    }
  });

  // check if trial or not
  const isTrial = config.contentServer.includes('trial');

  // get content data
  const contentData = isTrial 
    ? await axios.get(`${config.contentServer}/content.js`).then(res => res.data).then(jsonp => JSON.parse(jsonp.slice(jsonp.indexOf('{'), jsonp.lastIndexOf('}') + 1)))
    : await axios.get(`${config.contentServer}/sbcGetCntnt.php?cid=${cid}&p=${config.p}`).then(res => res.data);

  // generate data of image files
  const doc = (new DOMParser()).parseFromString(contentData.ttx, 'text/html');
  let files = Array.from(doc.querySelectorAll('t-img')).map(element => {
    const filename = element.getAttribute('src');
    const width = element.getAttribute('orgwidth');
    const height = element.getAttribute('orgheight');
    const src = isTrial ? `${config.contentServer}/${filename}/M_H.jpg` : `${config.contentServer}/sbcGetImg.php?cid=${cid}&src=${encodeURIComponent(filename)}&p=${config.p}`;
    return { filename, width, height, src };
  });
  files = files.slice(0, files.length / 2);

  // setup ImageDownloader
  ImageDownloader.init({
    maxImageAmount: files.length,
    getImagePromises,
    title: config.title
  });

  // collect promises of image
  function getImagePromises(startNum, endNum) {
    return files
      .slice(startNum - 1, endNum)
      .map(file => getDecryptedImage(file)
        .then(ImageDownloader.fulfillHandler)
        .catch(ImageDownloader.rejectHandler)
      );
  }

  // get promise of decrypted image
  function getDecryptedImage(file) {
    return new Promise(async resolve => {
      const imageArrayBuffer = await axios.get(file.src, { responseType: 'arraybuffer' }).then(res => res.data);
      const image = document.createElement('img');
      image.src = 'data:image/jpg;base64,' + window.btoa(new Uint8Array(imageArrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), ''));
      image.onload = function () {
        // create canvas
        const canvas = document.createElement('canvas');
        let ctx = canvas.getContext('2d');
        canvas.width = file.width;
        canvas.height = file.height;

        // get coords
        const key = SpeedReaderTools.getDecryptionKey(file.filename, config.ctbl, config.ptbl);
        const decoder = new SpeedReaderTools.CoordDecoder(key[0], key[1]);
        const coords = decoder.getCoords(this);

        // draw pieces on correct position
        for (const { srcX, srcY, destX, destY, width, height } of coords) {
          ctx.drawImage(this, srcX, srcY, width, height, destX, destY, width, height);
        }

        // if trial, clear those transparent pixel
        if (isTrial) {
          let originalWidth;
          for (let w = canvas.width; w >= 0; w--) {
            const px = canvas.getContext('2d').getImageData(w, 0, 1, 1);
            if (!Array.from(px.data).every(data => data === 0)) {
              originalWidth = w;
              break;
            }
          }

          let originalHeight;
          for (let h = canvas.height; h >= 0; h--) {
            const px = canvas.getContext('2d').getImageData(0, h, 1, 1);
            if (!Array.from(px.data).every(data => data === 0)) {
              originalHeight = h;
              break;
            }
          }

          canvas.width = originalWidth;
          canvas.height = originalHeight;
          ctx = canvas.getContext('2d');

          for (const { srcX, srcY, destX, destY, width, height } of coords) {
            ctx.drawImage(this, srcX, srcY, width, height, destX, destY, width, height);
          }
        }

        canvas.toBlob(resolve);
      }
    });
  }

})(axios, JSZip, saveAs, ImageDownloader, SpeedReaderTools);