ComicFuzDownloader

Manga downloader for comic-fuz.com

// ==UserScript==
// @name         ComicFuzDownloader
// @namespace    https://github.com/Timesient/manga-download-scripts
// @version      0.9
// @license      GPL-3.0
// @author       Timesient
// @description  Manga downloader for comic-fuz.com
// @icon         https://comic-fuz.com/favicons/favicon-32x32.png
// @homepageURL  https://greasyfork.org/scripts/451863-comicfuzdownloader
// @supportURL   https://github.com/Timesient/manga-download-scripts/issues
// @match        https://comic-fuz.com/*
// @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
// @grant        GM_info
// @grant        GM_xmlhttpRequest
// ==/UserScript==

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

  // if not in specific manga chapter
  const re4Path = /https:\/\/comic-fuz\.com\/manga\/\d+.*/;
  const timer4Path = setInterval(() => {
    if (re4Path.test(window.location.href)) {
      const currentTitle = document.querySelector('[class*=Chapter_chapter__isReading] h3[class*=Chapter_chapter__name]')?.textContent;
      const chapters = __NEXT_DATA__?.props?.pageProps?.chapters?.reduce((acc, cur) => acc.concat(cur.chapters), []);
      if (currentTitle && chapters) {
        for (const chapter of chapters) {
          if (chapter.chapterMainName === currentTitle) {
            clearInterval(timer4Path)
            window.location.href = `https://comic-fuz.com/manga/viewer/${chapter.chapterId}`;
          }
        }
      }
    }
  }, 200);

  // reload page when enter or leave chapter
  const re = /https:\/\/comic-fuz\.com\/(manga|book|magazine)\/(viewer|\d+).*/;
  const oldHref = window.location.href;
  const timer = setInterval(() => {
    const newHref = window.location.href;
    if (newHref === oldHref) return;
    if (re.test(newHref) || re.test(oldHref)) {
      clearInterval(timer);
      window.location.reload();
    }
  }, 200);

  // return if not reading chapter now
  if (!re.test(oldHref)) return;

  // get type and id
  const { type, id } = window.location.pathname.match(/\/(?<type>.*)\/viewer\/(?<id>.*)/).groups;

  // set the endpoint of API request
  const endpoint = ({
    'manga': 'manga_viewer',
    'book': 'book_viewer_2',
    'magazine': 'magazine_viewer_2',
  })[type];

  // set the body of API request
  const requestBody = ({
    'manga': {
      deviceInfo: { deviceType: 2 },
      chapterId: id,
      useTicket: false,
      consumePoint: { event: 0, paid: 0 }
    },
    'book': {
      deviceInfo: { deviceType: 2 },
      bookIssueId: id,
      purchaseRequest: false,
      consumePaidPoint: 0
    },
    'magazine': {
      deviceInfo: { deviceType: 2 },
      magazineIssueId: id,
      purchaseRequest: false,
      consumePaidPoint: 0
    },
  })[type];

  // get config data from API
  const url = `https://api.comic-fuz.com/v1/${endpoint}`;
  const config = await fetch(url, {
    method: 'POST',
    body: createInfoSchema().encodeInfo(requestBody),
    credentials: 'include',
  }).then(res => res.text());

  // get encrypted image data
  const imageDataRegexp = /(\/[fkh].*&e=\d{10}).*([0-9a-z]{32})"@([0-9a-z]{64})/gm;
  const encryptedImageData = Array.from(config.matchAll(imageDataRegexp)).map(item => ({ url: 'https://img.comic-fuz.com' + item[1], iv: item[2], key: item[3] }));

  // setup ImageDownloader
  ImageDownloader.init({
    maxImageAmount: encryptedImageData.length,
    getImagePromises,
    title: `comic-fuz-${type}-${id}`
  });

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

  // get promise of decrypted image
  async function getDecryptedImage(data) {
    function En(e) {
      const t = e.match(/.{1,2}/g);
      return new Uint8Array(t.map((function (e) {
        return parseInt(e, 16)
      })));
    }

    const encryptedImage = await axios.get(data.url, { responseType: 'arraybuffer' }).then(res => res.data);
    const key = await crypto.subtle.importKey('raw', En(data.key), "AES-CBC", false, ['decrypt']);
    const decryptedImage = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: En(data.iv) }, key, encryptedImage);

    return decryptedImage;
  }

  // I copy these code from somewhere else before, but I cant find the source now.
  // But surely they are related to 'protobuf' and 'protobufjs'
  function createInfoSchema() {
    const exports = {};

    exports.encodeInfo = function (message) {
      var bb = popByteBuffer();
      _encodeInfo(message, bb);
      return toUint8Array(bb);
    }

    function _encodeInfo(message, bb) {
      // optional DeviceInfo deviceInfo = 1;
      var $deviceInfo = message.deviceInfo;
      if ($deviceInfo !== undefined) {
        writeVarint32(bb, 10);
        var nested = popByteBuffer();
        _encodeDeviceInfo($deviceInfo, nested);
        writeVarint32(bb, nested.limit);
        writeByteBuffer(bb, nested);
        pushByteBuffer(nested);
      }

      // optional uint32 chapterId = 2;
      var $chapterId = message.chapterId;
      if ($chapterId !== undefined) {
        writeVarint32(bb, 16);
        writeVarint32(bb, $chapterId);
      }

      // I try this and it works!
      var $bookIssueId = message.bookIssueId;
      if ($bookIssueId !== undefined) {
        writeVarint32(bb, 16);
        writeVarint32(bb, $bookIssueId);
      }

      // I try this and it works!
      var $magazineIssueId = message.magazineIssueId;
      if ($magazineIssueId !== undefined) {
        writeVarint32(bb, 16);
        writeVarint32(bb, $magazineIssueId);
      }

      // optional bool useTicket = 3;
      var $useTicket = message.useTicket;
      if ($useTicket !== undefined) {
        writeVarint32(bb, 24);
        writeByte(bb, $useTicket ? 1 : 0);
      }

      // optional ConsumePoint consumePoint = 4;
      var $consumePoint = message.consumePoint;
      if ($consumePoint !== undefined) {
        writeVarint32(bb, 34);
        var nested = popByteBuffer();
        _encodeConsumePoint($consumePoint, nested);
        writeVarint32(bb, nested.limit);
        writeByteBuffer(bb, nested);
        pushByteBuffer(nested);
      }
    };

    exports.decodeInfo = function (binary) {
      return _decodeInfo(wrapByteBuffer(binary));
    }

    function _decodeInfo(bb) {
      var message = {};

      end_of_message: while (!isAtEnd(bb)) {
        var tag = readVarint32(bb);

        switch (tag >>> 3) {
          case 0:
            break end_of_message;

            // optional DeviceInfo deviceInfo = 1;
          case 1: {
            var limit = pushTemporaryLength(bb);
            message.deviceInfo = _decodeDeviceInfo(bb);
            bb.limit = limit;
            break;
          }

            // optional uint32 chapterId = 2;
          case 2: {
            message.chapterId = readVarint32(bb) >>> 0;
            break;
          }

            // optional bool useTicket = 3;
          case 3: {
            message.useTicket = !!readByte(bb);
            break;
          }

            // optional ConsumePoint consumePoint = 4;
          case 4: {
            var limit = pushTemporaryLength(bb);
            message.consumePoint = _decodeConsumePoint(bb);
            bb.limit = limit;
            break;
          }

          default:
            skipUnknownField(bb, tag & 7);
        }
      }

      return message;
    };

    exports.encodeDeviceInfo = function (message) {
      var bb = popByteBuffer();
      _encodeDeviceInfo(message, bb);
      return toUint8Array(bb);
    }

    function _encodeDeviceInfo(message, bb) {
      // optional uint32 deviceType = 3;
      var $deviceType = message.deviceType;
      if ($deviceType !== undefined) {
        writeVarint32(bb, 24);
        writeVarint32(bb, $deviceType);
      }
    };

    exports.decodeDeviceInfo = function (binary) {
      return _decodeDeviceInfo(wrapByteBuffer(binary));
    }

    function _decodeDeviceInfo(bb) {
      var message = {};

      end_of_message: while (!isAtEnd(bb)) {
        var tag = readVarint32(bb);

        switch (tag >>> 3) {
          case 0:
            break end_of_message;

            // optional uint32 deviceType = 3;
          case 3: {
            message.deviceType = readVarint32(bb) >>> 0;
            break;
          }

          default:
            skipUnknownField(bb, tag & 7);
        }
      }

      return message;
    };

    exports.encodeConsumePoint = function (message) {
      var bb = popByteBuffer();
      _encodeConsumePoint(message, bb);
      return toUint8Array(bb);
    }

    function _encodeConsumePoint(message, bb) {
      // optional uint32 event = 1;
      var $event = message.event;
      if ($event !== undefined) {
        writeVarint32(bb, 8);
        writeVarint32(bb, $event);
      }

      // optional uint32 paid = 2;
      var $paid = message.paid;
      if ($paid !== undefined) {
        writeVarint32(bb, 16);
        writeVarint32(bb, $paid);
      }
    };

    exports.decodeConsumePoint = function (binary) {
      return _decodeConsumePoint(wrapByteBuffer(binary));
    }

    function _decodeConsumePoint(bb) {
      var message = {};

      end_of_message: while (!isAtEnd(bb)) {
        var tag = readVarint32(bb);

        switch (tag >>> 3) {
          case 0:
            break end_of_message;

            // optional uint32 event = 1;
          case 1: {
            message.event = readVarint32(bb) >>> 0;
            break;
          }

            // optional uint32 paid = 2;
          case 2: {
            message.paid = readVarint32(bb) >>> 0;
            break;
          }

          default:
            skipUnknownField(bb, tag & 7);
        }
      }

      return message;
    };

    function pushTemporaryLength(bb) {
      var length = readVarint32(bb);
      var limit = bb.limit;
      bb.limit = bb.offset + length;
      return limit;
    }

    function skipUnknownField(bb, type) {
      switch (type) {
        case 0: while (readByte(bb) & 0x80) { } break;
        case 2: skip(bb, readVarint32(bb)); break;
        case 5: skip(bb, 4); break;
        case 1: skip(bb, 8); break;
        default: throw new Error("Unimplemented type: " + type);
      }
    }

    // The code below was modified from https://github.com/protobufjs/bytebuffer.js
    // which is under the Apache License 2.0.

    var f32 = new Float32Array(1);
    var f32_u8 = new Uint8Array(f32.buffer);

    var f64 = new Float64Array(1);
    var f64_u8 = new Uint8Array(f64.buffer);

    var bbStack = [];

    function popByteBuffer() {
      const bb = bbStack.pop();
      if (!bb) return { bytes: new Uint8Array(64), offset: 0, limit: 0 };
      bb.offset = bb.limit = 0;
      return bb;
    }

    function pushByteBuffer(bb) {
      bbStack.push(bb);
    }

    function wrapByteBuffer(bytes) {
      return { bytes, offset: 0, limit: bytes.length };
    }

    function toUint8Array(bb) {
      var bytes = bb.bytes;
      var limit = bb.limit;
      return bytes.length === limit ? bytes : bytes.subarray(0, limit);
    }

    function skip(bb, offset) {
      if (bb.offset + offset > bb.limit) {
        throw new Error('Skip past limit');
      }
      bb.offset += offset;
    }

    function isAtEnd(bb) {
      return bb.offset >= bb.limit;
    }

    function grow(bb, count) {
      var bytes = bb.bytes;
      var offset = bb.offset;
      var limit = bb.limit;
      var finalOffset = offset + count;
      if (finalOffset > bytes.length) {
        var newBytes = new Uint8Array(finalOffset * 2);
        newBytes.set(bytes);
        bb.bytes = newBytes;
      }
      bb.offset = finalOffset;
      if (finalOffset > limit) {
        bb.limit = finalOffset;
      }
      return offset;
    }

    function advance(bb, count) {
      var offset = bb.offset;
      if (offset + count > bb.limit) {
        throw new Error('Read past limit');
      }
      bb.offset += count;
      return offset;
    }

    function writeByteBuffer(bb, buffer) {
      var offset = grow(bb, buffer.limit);
      var from = bb.bytes;
      var to = buffer.bytes;

      // This for loop is much faster than subarray+set on V8
      for (var i = 0, n = buffer.limit; i < n; i++) {
        from[i + offset] = to[i];
      }
    }

    function readByte(bb) {
      return bb.bytes[advance(bb, 1)];
    }

    function writeByte(bb, value) {
      var offset = grow(bb, 1);
      bb.bytes[offset] = value;
    }

    function readVarint32(bb) {
      var c = 0;
      var value = 0;
      var b;
      do {
        b = readByte(bb);
        if (c < 32) value |= (b & 0x7F) << c;
        c += 7;
      } while (b & 0x80);
      return value;
    }

    function writeVarint32(bb, value) {
      value >>>= 0;
      while (value >= 0x80) {
        writeByte(bb, (value & 0x7f) | 0x80);
        value >>>= 7;
      }
      writeByte(bb, value);
    }

    return exports;
  }

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