ComicoDownloader

Manga downloader for comico.jp, comico.kr and pocketcomics.com

2024-06-21 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         ComicoDownloader
// @namespace    https://github.com/Timesient/manga-download-scripts
// @version      0.6
// @license      GPL-3.0
// @author       Timesient
// @description  Manga downloader for comico.jp, comico.kr and pocketcomics.com
// @icon         https://www.comico.jp/favicon/comico/favicon-32x32.png
// @homepageURL  https://greasyfork.org/scripts/451865-comicodownloader
// @supportURL   https://github.com/Timesient/manga-download-scripts/issues
// @match        https://www.comico.jp/*
// @match        https://www.comico.kr/*
// @match        https://www.pocketcomics.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://unpkg.com/[email protected]/crypto-js.js
// @require      https://update.greasyfork.org/scripts/451810/1398192/ImageDownloaderLib.js
// @grant        GM_info
// @grant        GM_xmlhttpRequest
// ==/UserScript==

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

  // determine regexp according to the host
  const re = ({
    'www.comico.jp': /https:\/\/www\.comico\.jp\/comic\/\d+\/chapter\/\d+\/.*/,
    'www.comico.kr': /https:\/\/www\.comico\.kr\/comic\/\d+\/chapter\/\d+\/.*/,
    'www.pocketcomics.com': /https:\/\/www\.pocketcomics\.com\/comic\/\d+\/chapter\/\d+\/.*/
  })[window.location.host];

  // reload page when enter or leave chapter
  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;

  // collect essential parameters
  const jsFilename = await axios.get(window.location.href).then(res => res.data.match(/<script src="\/js\/(?<filename>app\.[0-9a-zA-Z]{8}\.js)"><\/script>/).groups.filename);
  const webKey = await axios.get(`${window.location.origin}/js/${jsFilename}`).then(res => (res.data).match(/String\(t\),a="(?<key>[0-9a-z]*)"/).groups.key);
  const timestamp = Math.round(Date.now() / 1000);
  const checkSum = CryptoJS.SHA256(webKey + '0.0.0.0' + timestamp).toString(CryptoJS.enc.Hex);

  // get data of current episode
  const episodeData = await axios({
    method: 'GET',
    url: `${window.location.origin.replace('www', 'api')}${window.location.pathname}`,
    withCredentials: true,
    headers: {
      'X-comico-check-sum': checkSum,
      'X-comico-client-accept-mature': 'Y',
      'X-comico-client-immutable-uid': '0.0.0.0',
      'X-comico-client-os': 'other',
      'X-comico-client-platform': 'web',
      'X-comico-client-store': 'other',
      'X-comico-request-time': timestamp,
      'X-comico-timezone-id': 'Asia/Hong_Kong'
    }
  }).then(res => res.data.data.chapter);

  // not in episode page
  if (!episodeData) return;

  // get title and url of images
  const title = episodeData.name;
  const imageURLs = episodeData.images.map(image => `${AESDecoder(image.url)}?${image.parameter}`)

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

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

  // get promise of image
  function getImage(url) {
    return new Promise(resolve => {
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        responseType: 'arraybuffer',
        onload: res => resolve(res.response)
      });
    });
  }

  // use for decoding encrypted url of image
  function AESDecoder(text) {
    const key = 'a7fc9dc89f2c873d79397f8a0028a4cd';
    const iv = CryptoJS.enc.Utf8.parse(CryptoJS.enc.Hex.parse(''));
    const passPhrase = CryptoJS.enc.Utf8.parse(key);
    const decrypted = CryptoJS.AES.decrypt(text, passPhrase, {
      iv,
      mode: CryptoJS.mode.CBC
    });
    return CryptoJS.enc.Utf8.stringify(decrypted);
  }

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