ComicFuzDownloader

Manga downloader for comic-fuz.com

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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);