manga comic downloader

Tải truyện tranh từ các trang chia sẻ ở Việt Nam. Nhấn Alt+Y để tải toàn bộ.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name            manga comic downloader
// @namespace       https://baivong.github.io
// @description     Tải truyện tranh từ các trang chia sẻ ở Việt Nam. Nhấn Alt+Y để tải toàn bộ.
// @version         3.4.7
// @icon            https://raw.githubusercontent.com/lelinhtinh/Userscript/master/manga_comic_downloader/icon.png
// @author          Zzbaivong
// @license         MIT; https://baivong.mit-license.org/license.txt
// @match           http://*.truyentranh8.com/*
// @match           https://*.truyentranh8.com/*
// @match           http://*.truyentranh8.net/*
// @match           https://*.truyentranh8.net/*
// @match           http://*.truyentranh8.org/*
// @match           https://*.truyentranh8.org/*
// @match           http://*.truyentranh86.com/*
// @match           https://*.truyentranh86.com/*
// @match           http://*.truyentranh869.com/*
// @match           https://*.truyentranh869.com/*
// @match           https://mangaxy.com/*
// @match           https://*.truyentranh.net/*
// @match           https://*.hamtruyen.com/*
// @match           https://*.hamtruyenmoi.com/*
// @match           https://*.a3manga.com/*
// @match           https://*.a3mnga.com/*
// @match           http://truyentranhtuan.com/*
// @match           https://truyentranhlh.net/*
// @match           https://truyenhay24h.com/*
// @match           https://thichtruyentranh.com/*
// @match           https://lxhentai.com/*
// @match           https://hentaivn.net/*
// @match           https://hentaivn.moe/*
// @match           https://otakusan.net/*
// @match           https://*.ngonphong.com/*
// @match           https://*.nettruyen.com/*
// @match           http://*.nettruyen.com/*
// @match           https://*.nettruyentop.com/*
// @match           http://*.nettruyentop.com/*
// @match           http://*.nettruyenonline.com/*
// @match           https://*.nettruyenonline.com/*
// @match           http://*.nettruyenapp.com/*
// @match           https://*.nettruyenapp.com/*
// @match           http://*.nettruyenpro.com/*
// @match           https://*.nettruyenpro.com/*
// @match           http://*.nettruyengo.com/*
// @match           https://*.nettruyengo.com/*
// @match           https://*.nettruyenmax.com/*
// @match           http://*.nettruyenmoi.com/*
// @match           https://*.nettruyenbb.com/*
// @match           https://*.kingtruyen.com/*
// @match           http://nhattruyen.com/*
// @match           http://nhattruyengo.com/*
// @match           http://*.hamtruyentranh.net/*
// @match           https://ttmanga.com/*
// @match           http://truyen.vnsharing.site/*
// @match           http://*.blogtruyen.com/*
// @match           https://*.blogtruyen.com/*
// @match           https://*.blogtruyenmoi.com/*
// @match           http://*.blogtruyen.vn/*
// @match           https://*.blogtruyen.vn/*
// @match           http://*.blogtruyen.top/*
// @match           https://*.blogtruyen.top/*
// @match           https://truyensieuhay.com/*
// @match           http://truyenqqvn.com/*
// @match           http://truyenqqvn.net/*
// @match           http://truyenqqno.com/*
// @match           http://truyenqqpro.com/*
// @match           https://hentaicube.net/*
// @match           https://hentaicb.top/*
// @match           http://*.tuthienbao.com/*
// @match           https://vietcomic.net/*
// @match           https://hamtruyentranh.com/*
// @match           https://khotruyentranhz.com/*
// @match           https://truyenvn.com/*
// @match           https://truyenvn.vip/*
// @match           https://truyenvnpro.com/*
// @match           https://*.saytruyen.net/*
// @match           https://*.saytruyen.com/*
// @match           https://*.sayhentai.net/*
// @match           https://cocomic.net/truyen-tranh/*
// @require         https://code.jquery.com/jquery-3.6.0.min.js
// @require         https://unpkg.com/@zip.js/[email protected]/dist/zip.min.js
// @require         https://unpkg.com/[email protected]/dist/FileSaver.min.js
// @require         https://cdn.jsdelivr.net/npm/[email protected]/dist/ponyfill.min.js
// @require         https://cdn.jsdelivr.net/npm/[email protected]/StreamSaver.min.js
// @require         https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?v=a834d46
// @require         https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @require         https://cdn.jsdelivr.net/gh/Joe12387/detectIncognito@53bb1cde47a849dd166443a24b92f15432d7c40a/dist/es5/detectIncognito.min.js
// @resource        success https://unpkg.com/[email protected]/Low_Volume_-20dB/Complete_and_Success/Success_2.m4a
// @resource        error https://unpkg.com/[email protected]/Low_Volume_-20dB/Errors_and_Cancel/Error_2.m4a
// @noframes
// @connect         *
// @supportURL      https://github.com/lelinhtinh/Userscript/issues
// @run-at          document-start
// @grant           GM_addStyle
// @grant           GM_xmlhttpRequest
// @grant           GM.xmlHttpRequest
// @grant           GM.getResourceUrl
// @grant           GM_getResourceURL
// @grant           GM_registerMenuCommand
// ==/UserScript==

/* global zip, streamSaver, CryptoJS, detectIncognito */
window._URL = window.URL || window.webkitURL;

jQuery(function ($) {
  /**
   * Output extension
   * @type {String} zip
   *                cbz
   *
   * Tips: Convert .zip to .cbz
   * Windows
   * $ ren *.zip *.cbz
   * Linux
   * $ rename 's/\.zip$/\.cbz/' *.zip
   */
  var outputExt = 'cbz'; // or 'zip'

  /**
   * Multithreading
   * @type {Number} [1 -> 32]
   */
  var threading = 4;

  /**
   * The number of times the download may be attempted.
   * @type {Number}
   */
  var tries = 5;

  /**
   * Enable audio cues.
   * @type {Boolean}
   */
  var audioCues = true;

  /**
   * Image list will be ignored
   * @type {Array} url
   */
  var ignoreList = [
    'http://truyentranhtam.net/templates/main/images/gioithieubanbe3.png',
    'http://1.bp.blogspot.com/-U1SdU4_52Xs/WvLvn1OjvHI/AAAAAAAEugM/dLBgVGSeUN0bVy-FoFfIZvrCJ07YQew7wCHMYCw/s0/haybaoloi.png',
    '/public/images/loading.gif',
    'http://truyentranhlh.net/wp-content/uploads/2015/10/lhmanga.png',
    '/Content/Img/1eeef5d2-b936-496d-ba41-df1b21d0166a.jpg',
    '/Content/Img/d79886b3-3699-47b2-bbf4-af6149c2e8fb.jpg',
    'http://st.beeng.net/files/uploads/images/21/c8/21c8d2c3599c485e31f270675bc57e4c.jpeg',
    '00k9jbV.gif',
  ];

  /**
   * Keep the original url
   * @type {Array} key
   */
  var keepOriginal = [
    'proxy.truyen.cloud',
    '.ttmanga.com',
    '.fbcdn.net',
    'mangaqq.net',
    'mangaqq.com',
    'truyenqqvn.net',
    'truyenqqvn.com',
    'truyenqqtop.com',
    'truyenqqpro.com',
    '.upanhmoi.net',
    'qqtaku.com',
    'qqtaku.net',
    'trangshop.net',
    '.beeng.net',
    '.beeng.org',
    'forumnt.com',
    'hoitruyentranh.com',
    'hoihentai.com',
    'i02.hentaivn.net',
    'truyentop1.com',
    'anhnhanh.org',
    'truyenvua.xyz',
    'hamtruyen.vn',
    '.xem-truyen.com',
  ];

  /**
   * HTTP referer
   * @param {Object} hostname
   */
  var referer = {
    'i8.xem-truyen.com': 'https://blogtruyenmoi.com',
    'i.blogtruyen.com': 'https://blogtruyen.com',
    'truyen.cloud': 'http://www.nettruyen.com',
    'proxy.truyen.cloud': 'http://www.nettruyen.com',
    'i.netsnippet.com': 'http://www.nettruyen.com/',
    'forumnt.com': 'http://www.nettruyen.com/',
    'upload.forumnt.com': 'http://www.nettruyen.com/',
    'upload2.forumnt.com': 'http://www.nettruyen.com/',
    'upload.upanhmoi.net': 'https://upanhmoi.net',
    'upload2.upanhmoi.net': 'https://upanhmoi.net',
    'upload3.upanhmoi.net': 'https://upanhmoi.net',
    'upload4.upanhmoi.com': 'https://upanhmoi.com',
    'upload5.upanhmoi.com': 'https://upanhmoi.com',
    'upload6.upanhmoi.com': 'https://upanhmoi.com',
    'upload7.upanhmoi.com': 'https://upanhmoi.com',
    'upload8.upanhmoi.com': 'https://upanhmoi.com',
    'upload9.upanhmoi.com': 'https://upanhmoi.com',
    'img1.upanhmoi.net': 'https://upanhmoi.net',
    'img2.upanhmoi.net': 'https://upanhmoi.net',
    'proxy1.ttmanga.com': 'https://ttmanga.com',
    'proxy2.ttmanga.com': 'https://ttmanga.com',
    'proxy3.ttmanga.com': 'https://ttmanga.com',
    'cdn.lhmanga.com': 'https://truyentranhlh.net',
    'cdn1.lhmanga.com': 'https://truyentranhlh.net',
    'storage.fshare.vn': 'https://truyentranh.net',
    'ocumeo.com': 'https://www.a3manga.com/',
    'www.ocumeo.com': 'https://www.a3manga.com/',
    'mangaqq.net': 'http://truyenqqpro.com/',
    'mangaqq.com': 'http://truyenqqpro.com/',
    'truyenqq.net': 'http://truyenqqpro.com/',
    'truyenvua.xyz': 'http://truyenqqpro.com/',
    'i02.hentaivn.net': 'https://hentaivn.net/',
    'i1.hentaivn.net': 'https://hentaivn.net/',
    'i.imgur.com': 'https://imgur.com/',
  };

  /* === DO NOT CHANGE === */

  window.URL = window._URL;

  var successSound, errorSound;
  if (audioCues) {
    GM.getResourceUrl('success').then(function (url) {
      successSound = new Audio(url);
    });
    GM.getResourceUrl('error').then(function (url) {
      errorSound = new Audio(url);
    });
  }

  function getImageType(arrayBuffer) {
    if (!arrayBuffer.byteLength)
      return {
        mime: null,
        ext: null,
      };

    var ext = '',
      mime = '',
      dv = new DataView(arrayBuffer, 0, 5),
      numE1 = dv.getUint8(0, true),
      numE2 = dv.getUint8(1, true),
      hex = numE1.toString(16) + numE2.toString(16);

    switch (hex) {
      case '8950':
        ext = 'png';
        mime = 'image/png';
        break;
      case '4749':
        ext = 'gif';
        mime = 'image/gif';
        break;
      case 'ffd8':
        ext = 'jpg';
        mime = 'image/jpeg';
        break;
      case '424d':
        ext = 'bmp';
        mime = 'image/bmp';
        break;
      case '5249':
        ext = 'webp';
        mime = 'image/webp';
        break;
      default:
        ext = null;
        mime = null;
        break;
    }

    return {
      mime: mime,
      ext: ext,
    };
  }

  var notyTimeout;
  function noty(txt, status) {
    function destroy() {
      var $noty = $doc.find('#mcd_noty_wrap');
      if (!$noty.length) return;
      $noty.fadeOut(300, function () {
        $noty.remove();
        $noty = [];
      });
      clearTimeout(notyTimeout);
    }

    function autoHide() {
      notyTimeout = setTimeout(function () {
        destroy();
      }, 2000);
    }

    var $noty = $doc.find('#mcd_noty_wrap');
    if (!$noty.length) {
      var $wrap = $('<div>', {
          id: 'mcd_noty_wrap',
        }),
        $content = $('<div>', {
          id: 'mcd_noty_content',
          class: 'mcd_' + status,
          html: txt,
        }),
        $close = $('<div>', {
          id: 'mcd_noty_close',
          html: '&times;',
        });

      $noty = $wrap.append($content).append($close);
      $noty.appendTo('body');
    }

    $noty
      .find('#mcd_noty_content')
      .attr('class', 'mcd_' + status)
      .html(txt);

    $noty.fadeIn(300);
    clearTimeout(notyTimeout);

    $doc
      .on('click', '#mcd_noty_wrap', function () {
        destroy();
      })
      .on('mouseenter', '#mcd_noty_wrap', function () {
        clearTimeout(notyTimeout);
      })
      .on('mouseleave', '#mcd_noty_wrap', autoHide);

    if (status !== 'warning' && status !== 'success') autoHide();
  }

  function targetLink(selector) {
    return configs.link
      .split(/\s*,\s*/)
      .map(function (i) {
        return i + selector;
      })
      .join(',');
  }

  function linkError() {
    $(targetLink('[href="' + configs.href + '"]')).css({
      color: 'red',
      textShadow: '0 0 1px red, 0 0 1px red, 0 0 1px red',
    });
    hasDownloadError = true;
  }

  function linkSuccess() {
    var $currLink = $(targetLink('[href="' + configs.href + '"]'));
    if (!hasDownloadError)
      $currLink.css({
        color: 'green',
        textShadow: '0 0 1px green, 0 0 1px green, 0 0 1px green',
      });
  }

  function beforeleaving(e) {
    e.preventDefault();
    e.returnValue = '';
  }

  function cancelProgress() {
    linkError();
    inProgress = false;
    window.removeEventListener('beforeunload', beforeleaving);
    setTimeout(() => {
      if (dlAll.length || inProgress || !errorSound) return;
      errorSound.play();
    }, 0);
  }

  function notyError() {
    noty('Lỗi! Không tải được <strong>' + chapName + '</strong>', 'error');
    cancelProgress();
  }

  function notyImages() {
    noty('Lỗi! <strong>' + chapName + '</strong> không có dữ liệu', 'error');
    cancelProgress();
  }

  function notySuccess(source) {
    if (threading < 1) threading = 1;
    if (threading > 32) threading = 32;

    dlImages = source.map(function (url) {
      return {
        url: url,
        attempt: tries,
      };
    });
    dlTotal = dlImages.length;
    addZip();

    noty('Bắt đầu tải <strong>' + chapName + '</strong>', 'warning');
    window.addEventListener('beforeunload', beforeleaving);
  }

  function notyWait() {
    document.title = '[…] ' + tit;

    noty('<strong>' + chapName + '</strong> đang lấy dữ liệu...', 'warning');

    dlAll = dlAll.filter(function (l) {
      return configs.href.indexOf(l) === -1;
    });

    $(targetLink('[href="' + configs.href + '"]')).css({
      color: 'orange',
      fontWeight: 'bold',
      fontStyle: 'italic',
      textDecoration: 'underline',
      textShadow: '0 0 1px orange, 0 0 1px orange, 0 0 1px orange',
    });
  }

  function dlAllGen() {
    dlAll = [];
    $(configs.link).each(function (i, el) {
      dlAll[i] = $(el).attr('href');
    });
    if (configs.reverse) dlAll.reverse();
  }

  function notyReady() {
    noty('Script đã <strong>sẵn sàng</strong> làm việc', 'info');

    dlAllGen();

    $doc
      .on('click', configs.link, function (e) {
        if (!e.ctrlKey && !e.shiftKey) return;

        e.preventDefault();
        var _link = $(this).attr('href');

        if (e.ctrlKey && e.shiftKey) {
          dlAll = dlAll.filter(function (l) {
            return _link.indexOf(l) === -1;
          });

          $(targetLink('[href="' + _link + '"]')).css({
            color: 'gray',
            fontWeight: 'bold',
            fontStyle: 'italic',
            textDecoration: 'line-through',
            textShadow: '0 0 1px gray, 0 0 1px gray, 0 0 1px gray',
          });
        } else {
          if (!inCustom) {
            dlAll = [];
            inCustom = true;
          }

          dlAll.push(_link);

          var color = e.shiftKey ? 'violet' : 'purple';
          $(targetLink('[href="' + _link + '"]')).css({
            color: color,
            textDecoration: 'overline',
            textShadow: '0 0 1px ' + color + ', 0 0 1px ' + color + ', 0 0 1px ' + color,
          });
        }
      })
      .on('keyup', function (e) {
        if (e.key === 'Control' || e.key === 'Shift') {
          e.preventDefault();

          if (dlAll.length && inCustom) {
            if (e.key === 'Shift') inMerge = true;
            downloadAll();
          }
        }
      });
  }

  function downloadAll() {
    if (inProgress || inAuto) return;
    if (!inCustom && !dlAll.length) dlAllGen();
    if (!dlAll.length) return;
    inAuto = true;
    $(targetLink('[href$="' + dlAll[0] + '"]')).trigger('contextmenu');
  }

  function downloadAllOne() {
    inMerge = true;
    downloadAll();
  }

  async function saveFile(blob, zipName) {
    noty('Tải hoàn tất <strong>' + zipName + '</strong>', 'info');

    try {
      const { isPrivate } = await detectIncognito();
      if (isPrivate) throw 'isPrivate';

      const fileStream = streamSaver.createWriteStream(zipName, {
        size: blob.size,
      });
      const readableStream = blob.stream();

      if (window.WritableStream && readableStream.pipeTo) {
        return readableStream.pipeTo(fileStream);
      }
      window.writer = fileStream.getWriter();

      const reader = readableStream.getReader();
      const pump = () =>
        reader.read().then((res) => (res.done ? window.writer.close() : window.writer.write(res.value).then(pump)));
      pump();

      return;
    } catch (_err) {
      console.warn(_err);
      saveAs(blob, zipName);
    }
  }

  function genFileName() {
    return chapName
      .replace(/\s+/g, '_')
      .replace(/・/g, '·')
      .replace(/(^_+|_+$)/, '');
  }

  function endZip() {
    if (!inMerge) {
      zipFileStream = new TransformStream();
      zipFileBlobPromise = new Response(zipFileStream.readable).blob();
      zipWriter = new zip.ZipWriter(zipFileStream.writable, { zip64: true });
      downloadController = new AbortController();
      downloadSignal = downloadController.signal;
      zipData = [];
    }

    dlCurrent = 0;
    dlFinal = 0;
    dlTotal = 0;
    dlImages = [];

    hasDownloadError = false;
    inProgress = false;

    if (inAuto) {
      if (dlAll.length) {
        $(targetLink('[href$="' + dlAll[0] + '"]')).trigger('contextmenu');
      } else {
        inAuto = false;
        inCustom = false;
      }
    }

    setTimeout(() => {
      if (dlAll.length || inProgress || !successSound) return;
      successSound.play();
    }, 0);
  }

  async function genZip() {
    noty('Tạo file nén của <strong>' + chapName + '</strong>', 'warning');

    try {
      await Promise.all(zipData);
      await zipWriter.close();
      const zipFileBlob = await zipFileBlobPromise;

      saveFile(zipFileBlob, genFileName() + '.' + outputExt);
      linkSuccess();
      window.removeEventListener('beforeunload', beforeleaving);

      document.title = '[⇓] ' + tit;
      endZip();
    } catch (err) {
      console.error(err);
      if (downloadSignal.reason == err || (downloadSignal.reason && downloadSignal.reason.code == err.code)) {
        zip.terminateWorkers();
      }
      noty('Lỗi nén dữ liệu của <strong>' + chapName + '</strong>', 'error');
      cancelProgress();

      document.title = '[x] ' + tit;
      endZip();
    }
  }

  function dlImgError(current, success, error, err, filename) {
    if (dlImages[current].attempt <= 0) {
      dlFinal++;
      error(err, filename);
      return;
    }

    setTimeout(function () {
      dlImg(current, success, error);
      dlImages[current].attempt--;
    }, 2000);
  }

  function dlImg(current, success, error) {
    var url = dlImages[current].url,
      filename = ('0000' + dlCurrent).slice(-4),
      urlObj,
      urlHost,
      headers = {};

    if (url.indexOf('//') === 0) url = location.protocol + url;
    urlObj = new URL(url);
    urlHost = urlObj.hostname;

    if (referer[urlHost]) {
      headers.referer = referer[urlHost];
      headers.origin = referer[urlHost];
    } else {
      headers.referer = location.origin;
      headers.origin = location.origin;
    }

    GM.xmlHttpRequest({
      method: 'GET',
      url: url,
      responseType: 'arraybuffer',
      headers: headers,
      onload: function (response) {
        var imgExt = getImageType(response.response).ext;
        if (imgExt === 'gif') return next();

        if (
          !imgExt ||
          response.response.byteLength < 100 ||
          (response.statusText !== 'OK' && response.statusText !== '')
        ) {
          dlImgError(current, success, error, response, filename);
        } else {
          filename = filename + '.' + imgExt;
          success(response.response, filename);
        }
      },
      onerror: function (err) {
        dlImgError(current, success, error, err, filename);
      },
    });
  }

  function next() {
    noty('Đang tải xuống <strong>' + dlFinal++ + '/' + dlTotal + '</strong>', 'warning');
    if (dlFinal < dlCurrent) return;

    if (dlFinal < dlTotal) {
      addZip();
    } else {
      if (inMerge) {
        if (dlAll.length) {
          linkSuccess();
          endZip();
        } else {
          inMerge = false;
          genZip();
        }
      } else {
        genZip();
      }
    }
  }

  function addZip() {
    var max = dlCurrent + threading,
      path = '';

    if (max > dlTotal) max = dlTotal;
    if (inMerge) path = genFileName() + '/';

    for (dlCurrent; dlCurrent < max; dlCurrent++) {
      dlImg(
        dlCurrent,
        function (buff, filename) {
          var fileData = zipWriter.add(path + filename, new Response(buff).body, {
            signal: downloadSignal,
            onend: next,
          });
          zipData.push(fileData);
        },
        function (err, filename) {
          zipData.push(
            zipWriter.add(path + filename + '_error.txt', new Blob([err.statusText + '\r\n' + err.finalUrl]).stream(), {
              signal: downloadSignal,
              onend: next,
            }),
          );
          noty(err.statusText, 'error');
          linkError();
        },
      );
    }
  }

  function imageIgnore(url) {
    return ignoreList.some(function (v) {
      return url.indexOf(v) !== -1;
    });
  }

  function protocolUrl(url) {
    if (url.indexOf('//') === 0) url = location.protocol + url;
    if (url.search(/https?:\/\//) !== 0) url = 'http://' + url;
    return url;
  }

  function redirectSSL(url) {
    if (
      url.search(/(i\.imgur\.com|\.blogspot\.com|\.fbcdn\.net|storage\.fshare\.vn)/i) !== -1 &&
      url.indexOf('http://') === 0
    )
      url = url.replace(/^http:\/\//, 'https://');

    return url;
  }

  function decodeUrl(url) {
    var parser = new DOMParser(),
      dom = parser.parseFromString('<!doctype html><body>' + url, 'text/html');

    return decodeURIComponent(dom.body.textContent);
  }

  function imageFilter(url) {
    url = decodeUrl(url);
    url = url.trim();
    url = url.replace(/^.+(&|\?)url=/, '');
    url = url.replace(/(https?:\/\/)lh(\d)(\.bp\.blogspot\.com)/, '$1$2$3');
    url = url.replace(/(https?:\/\/)lh\d\.(googleusercontent|ggpht)\.com/, '$14.bp.blogspot.com');
    url = url.replace(/\?.+$/, '');
    if (url.indexOf('imgur.com') !== -1) {
      url = url.replace(/(\/)(\w{5}|\w{7})(s|b|t|m|l|h)(\.(jpe?g|png|webp))$/, '$1$2$4');
    } else if (url.indexOf('blogspot.com') !== -1) {
      url = url.replace(/\/([^/]+-)?(Ic42)(-[^/]+)?\//, '/$2/');
      url = url.replace(/\/(((s|w|h)\d+|(w|h)\d+-(w|h)\d+))?-?(c|d|g)?\/(?=[^/]+$)/, '/');
      url += '?imgmax=16383';
    } else {
      url = url.replace(/(\?|&).+/, '');
    }
    url = encodeURI(url);
    url = protocolUrl(url);
    url = redirectSSL(url);

    return url;
  }

  function checkImages(images) {
    var source = [];

    if (!images.length) {
      notyImages();
    } else {
      $.each(images, function (i, v) {
        v = v.replace(/^[\s\n]+|[\s\n]+$/g, '');

        var keep = keepOriginal.some(function (key) {
          return v.indexOf(key) !== -1;
        });
        if (keep) {
          source.push(v);
          return;
        }

        if (imageIgnore(v) || typeof v === 'undefined') return;
        if (/[><"']/.test(v)) return;

        if (
          (v.indexOf(location.origin) === 0 || (v.indexOf('/') === 0 && v.indexOf('//') !== 0)) &&
          !/^(\.(jpg|png)|webp|jpeg)$/.test(v.slice(-4))
        ) {
          return;
        } else if (v.indexOf('http') !== 0 && v.indexOf('//') !== 0) {
          v = location.origin + (v.indexOf('/') === 0 ? '' : '/') + v;
        } else if (v.indexOf('http') === 0 || v.indexOf('//') === 0) {
          v = imageFilter(v);
        } else {
          return;
        }

        source.push(v);
      });

      notySuccess(source);
    }
  }

  function getImages($contents) {
    var images = [];
    $contents.each(function (i, v) {
      var $img = $(v);
      images[i] =
        (!configs.imgSrc ? $img.data('src') || $img.data('original') : $img.attr(configs.imgSrc)) || $img.attr('src');
    });

    checkImages(images);
  }

  function getContents($source) {
    var method = 'find';
    if (configs.filter) method = 'filter';

    var $entry = $source[method](configs.contents).find('img');
    if (!$entry.length) {
      notyImages();
    } else {
      getImages($entry);
    }
  }

  function cleanSource(response) {
    var responseText = response.responseText;
    if (!configs.imgSrc) {
      responseText = responseText.replace(/[\s\n]+src[\s\n]*=[\s\n]*/gi, ' data-src=');
    }
    responseText = responseText.replace(/^[^<]*/, '');

    if (configs.imgSrc) return $(responseText);
    return $(responseText);
  }

  function rightClickEvent(_this, callback) {
    var $this = $(_this),
      name = configs.name;

    configs.href = $this.attr('href');
    chapName = $this.text().trim();

    if (typeof name === 'function') {
      chapName = name(_this, chapName);
    } else if (typeof name === 'string') {
      chapName = $(name).text().trim() + ' ' + chapName;
    }

    notyWait();

    GM.xmlHttpRequest({
      method: 'GET',
      url: configs.href,
      onload: function (response) {
        var $data = cleanSource(response);
        if (typeof callback === 'function') {
          callback($data);
        } else {
          getContents($data);
        }
      },
      onerror: function () {
        notyError();
      },
    });
  }

  function oneProgress() {
    if (inProgress) {
      noty('Chỉ được phép <strong>tải một truyện</strong> mỗi lần', 'error');
      return false;
    }
    inProgress = true;
    return true;
  }

  function getSource(callback) {
    var $link = $(configs.link);
    if (!$link.length) return;

    $doc.on('contextmenu', configs.link, function (e) {
      e.preventDefault();
      hasDownloadError = false;
      if (!oneProgress()) return;

      rightClickEvent(e.currentTarget, callback);
    });

    notyReady();
  }

  function getA3Manga() {
    getSource(function ($data) {
      var chapterCode = $data.find('script:contains("htmlContent")');
      var chapterCrypto = $data.filter('script:contains("htmlContent")');
      if (!chapterCode.length || !chapterCrypto.length) {
        notyImages();
      } else {
        chapterCrypto = chapterCrypto.text().match(/(CryptoJSAesDecrypt\(.+?(?:(;}|htmlContent\);)))/g);

        var chapterDecrypt = new Function(
          chapterCode.text() + 'function ' + chapterCrypto[0] + 'return ' + chapterCrypto[1],
        );
        this.CryptoJS = CryptoJS;
        chapterDecrypt.apply(this);

        var chapterHTML = chapterDecrypt();
        var images = chapterHTML.match(/(?<=(data-(lqz53ud|3dn5rc9)="))(.+?)(?=")/g);
        if (!images) {
          notyImages();
          return;
        }

        images = images.map((imgLink) => {
          imgLink = imgLink.replace(/LqZ53ud|3Dn5rc9/g, '.');
          imgLink = imgLink.replace(/pPdp7FG|gNa8fuX/g, ':');
          imgLink = imgLink.replace(/9pyrBcb|hT3k3S6/g, '/');
          return imgLink;
        });
        checkImages(images);
      }
    });
  }

  function getTruyenTranhTuan() {
    getSource(function ($data) {
      var $entry = $data.find('#read-title').next('script');
      if (!$entry.length) {
        notyImages();
      } else {
        $data = $entry.text().match(/var\s+slides_page_path\s*=\s*(.+?);/)[1];
        $data = JSON.parse($data);

        var type_server = 1;
        var slides_page;
        var slides_page_path = [];
        var use_server_gg = true;
        var slides_page_url_path = $data;

        if (slides_page_url_path.length > 0) {
          type_server = 2;
        } else {
          type_server = 1;
        }

        if (!use_server_gg) {
          type_server = 1;
        }
        if (type_server != 1) {
          slides_page = slides_page_url_path;
        } else {
          slides_page = slides_page_path;
          var i,
            j,
            length_chapter = slides_page.length - 1;
          for (i = 0; i < length_chapter; i++)
            for (j = i + 1; j < slides_page.length; j++)
              if (slides_page[j] < slides_page[i]) {
                var temp = slides_page[j];
                slides_page[j] = slides_page[i];
                slides_page[i] = temp;
              }
        }

        checkImages(slides_page);
      }
    });
  }

  function getTruyenHay24h() {
    getSource(function ($data) {
      $data = $data.find('#dvContentChap').siblings('script').text();
      $data = $data.match(/GI2017\(([^;]+);/)[1];
      $data = $data.split(/[,']+/);

      $.post('/TH24Service.asmx/GetChapterImages', {
        PID: $data[0],
        ChapNumber: $data[1],
        cc18: $data[2],
        name: '',
        s: 0,
      })
        .done(function (response) {
          var images = [];
          $(response)
            .find('string')
            .each(function (i, v) {
              images[i] = v.textContent.replace(/\.(jpe?g|png)\w*$/, '.$1');
            });

          checkImages(images);
        })
        .fail(function () {
          notyError();
        });
    });
  }

  function getThichTruyenTranh() {
    getSource(function ($data) {
      $data = $data.find('#content_read').next('script').text();
      $data = $data.match(/https?:\/\/[^"]+/g);
      if (!$data.length) {
        notyImages();
      } else {
        checkImages($data);
      }
    });
  }

  function getHentaiVN() {
    var chapterList = document.querySelector('#inner-listshowchapter');
    if (chapterList === null) return;

    noty('wait a moment...', 'warning');
    setTimeout(() => {
      chapterList.scrollIntoView();
    }, 0);

    var observer = new MutationObserver(function (mutations_list) {
      mutations_list.forEach(function (mutation) {
        if (mutation.type !== 'childList') return;
        if (!mutation.target.querySelector(configs.link)) return;

        getSource();
        observer.disconnect();
      });
    });

    observer.observe(chapterList, {
      attributes: false,
      childList: true,
      subtree: false,
    });
  }

  function getOtakuSan() {
    var $link = $(configs.link);
    if (!$link.length) return;

    $link.on('contextmenu', function (e) {
      e.preventDefault();
      hasDownloadError = false;
      if (!oneProgress()) return;

      var $this = $(this);

      configs.href = $this.attr('href');
      chapName = $('h1.title').first().attr('title') + ' ' + $this.text().trim();
      notyWait();

      GM.xmlHttpRequest({
        method: 'GET',
        url: configs.href,
        withCredentials: true,
        headers: {
          host: 'otakusan.net',
        },
        onload: function () {
          GM.xmlHttpRequest({
            method: 'POST',
            url: '/Manga/UpdateView',
            responseType: 'json',
            withCredentials: true,
            headers: {
              host: 'otakusan.net',
              'content-length': 14,
              'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
            },
            data: 'chapId=' + configs.href.match(/\/(\d+)\/.+$/)[1],
            onload: function (response) {
              var res = response.response;
              if (!res.chapid) {
                notyImages();
              } else {
                notySuccess(JSON.parse(res.view));
              }
            },
          });
        },
      });
    });

    notyReady();
  }

  function getTtManga() {
    getSource(function ($data) {
      var data = $data.find('#divImage').siblings('script').first().text();
      if (!/lstImages\.push\("([^"]+)"\)/.test(data)) {
        notyImages();
      } else {
        var regex = /lstImages\.push\("([^"]+)"\)/gi,
          matches,
          output = [];

        // eslint-disable-next-line no-cond-assign
        while ((matches = regex.exec(data))) {
          output.push(decodeURIComponent(matches[1]));
        }
        checkImages(output);
      }
    });
  }

  function getTruyenSieuHay() {
    function decrypt(des, id) {
      id = id.substring(2, id.length - 3);
      var passphrase = CryptoJS.enc.Utf8.parse(id.toLowerCase());
      var iv = CryptoJS.enc.Utf8.parse('gqLOHUioQ0QjhuvI');
      var decrypted = CryptoJS.AES.decrypt(des, passphrase, {
        iv: iv,
        mode: CryptoJS.mode.CBC,
      });
      var result = decrypted.toString(CryptoJS.enc.Utf8);
      return result;
    }

    getSource(function ($data) {
      if ($data.find('#wrap_alertvip').length) {
        notyImages();
        return;
      }

      var sID = $data.find('.content-chap-image').find('script:not([type]):first').text();
      sID = /\bgetContentchap\('(.+?)','(.+?)'\)/.exec(sID);
      if (!sID) {
        notyImages();
        return;
      }

      $.ajax({
        type: 'POST',
        url: '/Service.asmx/getContentChap',
        data: '{ sID: "' + sID[1] + '",chuc:"' + sID[2] + '" }',
        contentType: 'application/json; charset=utf-8',
        dataType: 'json',
        success: function (data) {
          if (data.Message) {
            notyImages();
            return;
          }

          data = JSON.parse(data.d);
          var $contents = decrypt(data.des, data.id);
          $contents = $($contents).filter('img');
          getImages($contents);
        },
        error: function () {
          notyImages();
        },
      });
    });
  }

  function getVietComic() {
    getSource(function ($data) {
      var data = $data.filter('script:not([src]):contains("Loadimage(i)")');
      if (!data.length) {
        notyImages();
        return;
      }

      data = data.text().match(/data\s=\s'(.+?)';/);
      if (!data) {
        notyImages();
        return;
      }

      data = data[1];
      data = data.split('|');
      checkImages(data);
    });
  }

  var configsDefault = {
      reverse: true,
      link: '',
      name: '',
      contents: '',
      imgSrc: '',
      filter: false,
      init: getSource,
    },
    configs,
    chapName,
    domainName = location.host,
    tit = document.title,
    $doc = $(document),
    // zipMime = outputExt === 'cbz' ? 'application/vnd.comicbook+zip' : 'application/zip',
    zipFileStream = new TransformStream(),
    zipFileBlobPromise = new Response(zipFileStream.readable).blob(),
    zipWriter = new zip.ZipWriter(zipFileStream.writable, { zip64: true }),
    downloadController = new AbortController(),
    downloadSignal = downloadController.signal,
    zipData = [],
    dlCurrent = 0,
    dlFinal = 0,
    dlTotal = 0,
    dlImages = [],
    dlAll = [],
    hasDownloadError = false,
    inProgress = false,
    inAuto = false,
    inCustom = false,
    inMerge = false;

  streamSaver.mitm = 'https://lelinhtinh.github.io/stream/mitm.html';

  GM_registerMenuCommand('Download All Chapters', downloadAll);
  GM_registerMenuCommand('Download All To One File', downloadAllOne);

  $doc.on('keydown', function (e) {
    if (e.code === 'KeyY' && e.altKey) {
      // Alt+Y
      e.preventDefault();
      e.shiftKey ? downloadAllOne() : downloadAll();
    }
  });

  GM_addStyle(
    '#mcd_noty_wrap{background:#fff;position:fixed;z-index:2147483647;right:20px;top:20px;min-width:150px;max-width:100%;padding:15px 25px;border:1px solid #ddd;border-radius:2px;box-shadow:0 0 0 1px rgba(0,0,0,.1),0 1px 10px rgba(0,0,0,.35);cursor:pointer}#mcd_noty_content{color:#444}#mcd_noty_content strong{font-weight:700}#mcd_noty_content.mcd_info strong{color:#2196f3}#mcd_noty_content.mcd_success strong{color:#4caf50}#mcd_noty_content.mcd_warning strong{color:#ffc107}#mcd_noty_content.mcd_error strong{color:#f44336}#mcd_noty_content strong.centered{display:block;text-align:center}#mcd_noty_close{position:absolute;right:0;top:0;font-size:18px;color:#ddd;height:20px;width:20px;line-height:20px;text-align:center}#mcd_noty_wrap:hover #mcd_noty_close{color:#333}',
  );

  switch (domainName) {
    case 'truyentranh8.com':
    case 'truyentranh8.net':
    case 'truyentranh8.org':
    case 'truyentranh869.com':
    case 'truyentranh86.com':
      configs = {
        link: '#ChapList a',
        name: function (_this) {
          return $('.breadcrumb li:last').text().trim() + ' ' + $(_this).find('span, strong, h2').text().trim();
        },
        contents: '#reading-detail',
      };
      break;
    case 'mangaxy.com':
      configs = {
        link: '#ChapList a',
        name: function (_this) {
          var title = $('h1.comics-title').text();
          var chapTitle = $(_this).find('div.episode-title').text();
          return `${title} ${chapTitle}`;
        },
        contents: '#reading-detail',
      };
      break;
    case 'truyentranh.net':
    case 'www.truyentranh.net':
      configs = {
        reverse: false,
        link: '.chapter-list-item-box a',
        name: function (_this) {
          return $('h1').text().trim() + ' ' + $(_this).text().trim();
        },
        contents: '.manga-reading-box',
      };
      break;
    case 'hamtruyen.com':
    case 'www.hamtruyen.com':
    case 'hamtruyenmoi.com':
      configs = {
        link: '.list-chaps a',
        name: 'h3.story-name',
        contents: '.list-images',
      };
      break;
    case 'm.hamtruyen.com':
      configs = {
        link: '.list-chap a',
        name: '.tentruyen',
        contents: '#content_chap',
      };
      break;
    case 'a3manga.com':
    case 'www.a3manga.com':
    case 'a3mnga.com':
    case 'www.a3mnga.com':
    case 'www.ngonphong.com':
      configs = {
        link: '.chapter-table a.text-capitalize',
        init: getA3Manga,
      };
      break;
    case 'truyentranhtuan.com':
      configs = {
        link: '.chapter-name a',
        init: getTruyenTranhTuan,
      };
      break;
    case 'truyentranhlh.net':
      configs = {
        link: '.list-chapters a',
        contents: '#chapter-content',
        name: function (_this) {
          return $('.series-name').text().trim() + ' ' + $(_this).find('.chapter-name').text().trim();
        },
      };
      break;
    case 'truyenhay24h.com':
      configs = {
        link: '.nano .chapname a',
        name: '.name_sp',
        init: getTruyenHay24h,
      };
      break;
    case 'thichtruyentranh.com':
      configs = {
        reverse: false,
        link: '#listChapterBlock .ul_listchap a',
        init: getThichTruyenTranh,
      };
      break;
    case 'lxhentai.com':
      configs = {
        link: '[style="max-height: 500px"] a',
        contents: '[class="text-center"]',
        name: function (_this) {
          return (
            $('.grow.text-lg.ml-1.text-ellipsis.font-semibold:first').text().trim() +
            ' ' +
            $(_this).find('span.text-ellipsis').text().trim()
          );
        },
      };
      break;
    case 'hentaivn.net':
    case 'hentaivn.moe':
      configs = [
        {
          link: '.listing a',
          name: function (_this) {
            var title = $('h1[itemprop="name"]').find('a').text();
            title = title.split('-')[0].trim();
            var chapTitle = $(_this).find('.chuong_t').attr('title');
            chapTitle = chapTitle.split('-')[1].trim();
            return `${title} ${chapTitle}`;
          },
          contents: '#image',
          init: getHentaiVN,
        },
        {
          link: '.episodes a',
          name: '[itemprop="name"] b',
          contents: '#image',
        },
      ];
      break;
    case 'otakusan.net':
      configs = {
        link: '.read-chapter a',
        name: 'h1.header',
        init: getOtakuSan,
      };
      break;
    case 'www.nettruyen.com':
    case 'nhattruyen.com':
    case 'nhattruyengo.com':
    case 'www.nettruyenapp.com':
    case 'www.nettruyenpro.com':
    case 'www.nettruyengo.com':
    case 'www.nettruyenmoi.com':
    case 'www.nettruyenmax.com':
      configs = {
        link: '#nt_listchapter .chapter a',
        name: '.title-detail',
        contents: '.reading-detail.box_doc',
        imgSrc: 'data-original',
      };
      break;
    case 'www.nettruyentop.com':
    case 'www.nettruyenonline.com':
    case 'www.nettruyenbb.com':
    case 'kingtruyen.com':
      configs = {
        link: '#nt_listchapter .chapter a',
        name: '.title-detail',
        contents: '.readimg,.reading-detail.box_doc',
      };
      break;

    case 'www.hamtruyentranh.net':
      configs = {
        link: '#examples a',
        name: function (_this) {
          var $clone = $(_this).clone();
          $clone.find('span').remove();
          return $('.title-manga').text().trim() + ' ' + $clone.text().trim();
        },
        contents: '.each-page',
      };
      break;
    case 'ttmanga.com':
      configs = {
        link: '#list-chapter a',
        init: getTtManga,
      };
      break;
    case 'truyen.vnsharing.site':
      configs = {
        link: '#manga-chaplist a',
        contents: '.read_content',
      };
      break;
    case 'blogtruyen.com':
    case 'blogtruyen.vn':
    case 'blogtruyen.top':
    case 'blogtruyenmoi.com':
    case 'www.blogtruyen.com':
    case 'www.blogtruyen.vn':
    case 'www.blogtruyen.top':
      configs = {
        link: '#list-chapters .title a',
        contents: '#content',
      };
      break;
    case 'm.blogtruyen.com':
    case 'm.blogtruyen.vn':
    case 'm.blogtruyen.top':
    case 'm.blogtruyenmoi.com':
      configs = {
        link: '.list-chapter a',
        name: function (_this) {
          return $('h1.title').text().trim() + ' ' + $(_this).find('span').text().trim();
        },
        contents: '.content',
      };
      break;
    case 'truyensieuhay.com':
      configs = {
        link: '#chapter-list-flag a',
        name: 'h1',
        init: getTruyenSieuHay,
      };
      break;
    case 'truyenqq.com':
    case 'truyenqq.net':
    case 'truyenqqtop.com':
    case 'truyenqqpro.com':
      configs = {
        link: '.works-chapter-list a',
        name: 'h1',
        contents: '.chapter_content',
      };
      break;
    case 'hentaicube.net':
    case 'hentaicb.top':
      configs = {
        link: '.wp-manga-chapter a',
        name: '.post-title',
        contents: '.reading-content',
        imgSrc: 'data-src',
      };
      break;
    case 'tuthienbao.com':
    case 'www.tuthienbao.com':
      configs = {
        link: 'a[id^="thread_title_"]',
        contents: '.quotecontent',
      };
      break;
    case 'vietcomic.net':
      configs = {
        link: '.chapter-list a:not([rel="nofollow"])',
        name: function (_this, chapName) {
          return $('.manga-info-text h1').text().trim() + ' ' + chapName;
        },
        init: getVietComic,
      };
      break;
    case 'hamtruyentranh.com':
      configs = {
        reverse: false,
        link: '#chaps .chapter-title a:not([target])',
        name: 'h1',
        contents: '.table-responsive',
        imgSrc: 'id-source',
      };
      break;
    case 'khotruyentranhz.com':
      configs = {
        link: '#chapters-list-content .chapter-list a',
        name: 'h1',
        contents: '.box-chapter-content',
      };
      break;
    case 'truyenvn.com':
    case 'truyenvn.vip':
    case 'truyenvnpro.com':
      configs = {
        link: '#chapterList a',
        name: function (_this) {
          return $('h1.name').text().trim() + ' ' + $(_this).find('span:first').text().trim();
        },
        contents: '.content-text',
      };
      break;
    case 'saytruyen.net':
    case 'www.saytruyen.net':
    case 'saytruyen.com':
    case 'www.saytruyen.com':
    case 'sayhentai.net':
    case 'www.sayhentai.net':
    case 'sayhentai.tv':
      configs = {
        link: '.main-col .wp-manga-chapter a',
        name: '.post-title h1',
        contents: '#chapter_content',
      };
      break;
    case 'cocomic.net':
      configs = {
        link: '.chapter-list .citem a',
        name: 'h1',
        contents: '.chapter-content',
      };
      break;
    default:
      configs = {};
      break;
  }

  if (Array.isArray(configs)) {
    var isMobile = /mobi|android|touch|mini/i.test(navigator.userAgent.toLowerCase());
    configs = configs[isMobile ? 1 : 0];
  }
  if (!configs) return;

  configs = $.extend(configsDefault, configs);
  configs.init();
});