- // ==UserScript==
- // @name MCD
- // @namespace https://lelinhtinh.github.io
- // @description Manga Comic Downloader. Shortcut: Alt+Y.
- // @version 1.5.1
- // @icon https://i.imgur.com/GAM6cCg.png
- // @author Zzbaivong
- // @license MIT; https://baivong.mit-license.org/license.txt
- // @match https://www.kuaikanmanhua.com/*
- // @match https://newtoki*.*/webtoon/*
- // @match https://manhwa18.net/*
- // @match https://manytoon.com/comic/*
- // @match https://18comic.org/album/*
- // @require https://code.jquery.com/jquery-3.5.1.min.js
- // @require https://unpkg.com/jszip@3.1.5/dist/jszip.min.js
- // @require https://unpkg.com/file-saver@2.0.2/dist/FileSaver.min.js
- // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?v=a834d46
- // @noframes
- // @connect *
- // @supportURL https://github.com/lelinhtinh/Userscript/issues
- // @run-at document-start
- // @grant GM_addStyle
- // @grant GM_xmlhttpRequest
- // @grant GM.xmlHttpRequest
- // @grant GM_registerMenuCommand
- // ==/UserScript==
-
- 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 = 'zip'; // or 'zip'
-
- /**
- * Multithreading
- * @type {Number} [1 -> 32]
- */
- var threading = 4;
-
- /**
- * The number of times the download may be attempted.
- * @type {Number}
- */
- var tries = 5;
-
- /**
- * Image list will be ignored
- * @type {Array} url
- */
- var ignoreList = [];
-
- /**
- * Keep the original url
- * @type {Array} key
- */
- var keepOriginal = ['kkmh.com'];
-
- /**
- * HTTP referer
- * @param {Object} hostname
- */
- var referer = {};
-
- /* === DO NOT CHANGE === */
-
- window.URL = window._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,
- };
- }
-
- function noty(txt, status) {
- function destroy() {
- if (!$noty.length) return;
- $noty.fadeOut(300, function () {
- $noty.remove();
- $noty = [];
- });
- clearTimeout(notyTimeout);
- }
-
- function autoHide() {
- notyTimeout = setTimeout(function () {
- destroy();
- }, 2000);
- }
-
- if (!$noty.length) {
- var $wrap = $('<div>', {
- id: 'baivong_noty_wrap',
- }),
- $content = $('<div>', {
- id: 'baivong_noty_content',
- class: 'baivong_' + status,
- html: txt,
- }),
- $close = $('<div>', {
- id: 'baivong_noty_close',
- html: '×',
- });
-
- $noty = $wrap.append($content).append($close);
- $noty.appendTo('body').fadeIn(300);
- } else {
- $noty
- .find('#baivong_noty_content')
- .attr('class', 'baivong_' + status)
- .html(txt);
-
- $noty.show();
- clearTimeout(notyTimeout);
- }
-
- $noty
- .click(function () {
- destroy();
- })
- .hover(
- function () {
- clearTimeout(notyTimeout);
- },
- function () {
- autoHide();
- },
- );
- if (status !== 'warning' && status !== 'success') autoHide();
- }
-
- function targetLink(selector) {
- return configs.link
- .split(/\s*,\s*/)
- .map((i) => 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();
- window.removeEventListener('beforeunload', beforeleaving);
- }
-
- function notyError() {
- noty('ERR! Cannot download <strong>' + chapName + '</strong>', 'error');
- inProgress = false;
- cancelProgress();
- }
-
- function notyImages() {
- noty('ERR! <strong>' + chapName + '</strong> empty data', 'error');
- inProgress = false;
- 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('Start downloading <strong>' + chapName + '</strong>', 'warning');
- window.addEventListener('beforeunload', beforeleaving);
- }
-
- function notyWait() {
- document.title = '[…] ' + tit;
-
- noty('<strong>' + chapName + '</strong> is getting ready...', '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 is <strong>now ready</strong> to use', '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);
-
- $(targetLink('[href="' + _link + '"]')).css({
- color: 'violet',
- textDecoration: 'overline',
- textShadow: '0 0 1px violet, 0 0 1px violet, 0 0 1px violet',
- });
- }
- })
- .on('keyup', function (e) {
- if (e.which === 17 || e.which === 16) {
- e.preventDefault();
-
- if (dlAll.length && inCustom) {
- if (e.which === 16) 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();
- }
-
- function genFileName() {
- chapName = chapName
- .replace(/\s+/g, '_')
- .replace(/・/g, '·')
- .replace(/(^_+|_+$)/, '');
- if (hasDownloadError) chapName = '__ERROR__' + chapName;
- return chapName;
- }
-
- function endZip() {
- if (!inMerge) {
- dlZip = new JSZip();
- dlPrevZip = false;
- }
-
- 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;
- }
- }
- }
-
- function genZip() {
- noty('Create archive of <strong>' + chapName + '</strong>', 'warning');
-
- dlZip
- .generateAsync(
- {
- type: 'blob',
- compression: 'STORE',
- },
- function updateCallback(metadata) {
- noty('Zipping <strong>' + metadata.percent.toFixed(2) + '%</strong>', 'warning');
- },
- )
- .then(
- function (blob) {
- var zipName = genFileName() + '.' + outputExt;
-
- if (dlPrevZip) URL.revokeObjectURL(dlPrevZip);
- dlPrevZip = blob;
-
- noty(
- '<a href="' +
- URL.createObjectURL(dlPrevZip) +
- '" download="' +
- zipName +
- '"><strong>Click here</strong></a> if not automatically download',
- 'success',
- );
- linkSuccess();
-
- window.removeEventListener('beforeunload', beforeleaving);
- saveAs(blob, zipName);
-
- document.title = '[⇓] ' + tit;
- endZip();
- },
- function () {
- noty('ERR! Cannot zip file <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 = new URL(url),
- urlHost = urlObj.hostname,
- headers = {};
-
- 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') {
- dlFinal++;
- next();
- return;
- }
-
- if (
- !imgExt ||
- response.response.byteLength < 100 ||
- (response.statusText !== 'OK' && response.statusText !== '')
- ) {
- dlImgError(current, success, error, response, filename);
- } else {
- filename = filename + '.' + imgExt;
- dlFinal++;
- success(response, filename);
- }
- },
- onerror: function (err) {
- dlImgError(current, success, error, err, filename);
- },
- });
- }
-
- function next() {
- noty('Downloading <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 (response, filename) {
- dlZip.file(path + filename, response.response);
-
- next();
- },
- function (err, filename) {
- dlZip.file(path + filename + '_error.txt', err.statusText + '\r\n' + err.finalUrl);
-
- noty(err.statusText, 'error');
- linkError();
-
- next();
- },
- );
- }
- }
-
- function imageIgnore(url) {
- return ignoreList.some(function (v) {
- return url.indexOf(v) !== -1;
- });
- }
-
- 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);
-
- 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) return $(responseText);
-
- responseText = responseText.replace(/[\s\n]+src[\s\n]*=[\s\n]*/gi, ' data-src=');
- responseText = responseText.replace(/^[^<]*/, '');
- 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('Only <strong>one chapter</strong> can be downloaded at a time', 'error');
- return false;
- }
- inProgress = true;
- return true;
- }
-
- function getSource(callback) {
- var $link = $(configs.link);
- if (!$link.length) return;
-
- $link.on('contextmenu', function (e) {
- e.preventDefault();
- hasDownloadError = false;
- if (!oneProgress()) return;
-
- rightClickEvent(this, callback);
- });
-
- notyReady();
- }
-
- /* global __NUXT__ */
- function getKuaikanManhua() {
- getSource(function ($data) {
- $data = $data.filter('script:not([src]):contains("window.__NUXT__")');
- if (!$data.length) {
- notyImages();
- return;
- }
-
- eval($data.text());
-
- if (!__NUXT__) {
- notyImages();
- return;
- }
-
- var images = __NUXT__.data[0].comicInfo.comicImages;
- images = images.map(function (v) {
- return v.url;
- });
-
- if (!images.length) {
- notyImages();
- return;
- }
-
- checkImages(images);
- });
- }
-
- /* global html_data */
- function getNewToki69() {
- function html_encoder(s) {
- var i = 0,
- out = '',
- l = s.length;
- for (; i < l; i += 3) {
- out += String.fromCharCode(parseInt(s.substr(i, 2), 16));
- }
- return out;
- }
-
- getSource(function ($data) {
- var $images = $data.find('img[data-original^="https://"]:not([style])');
- if (!$images.length) {
- $images = $data.find('script:not([src]):contains("html_data")');
- if (!$images.length) {
- notyImages();
- return;
- }
-
- $images = $images.text();
- $images = /(var\s+html_data[\s\S]+?)(?=(document\.write|[\s\n]+$))/.exec($images);
- if (!$images) {
- notyImages();
- return;
- }
-
- eval($images[1]);
- $images = html_encoder(html_data);
- $images = $($images).find('img[data-original^="https://"]:not([style])');
- }
-
- var images = [];
- $images.each(function (i, v) {
- var $img = $(v);
- images[i] = $img.data('original');
- });
-
- checkImages(images);
- });
- }
-
- var configsDefault = {
- reverse: true,
- link: '',
- name: '',
- contents: '',
- imgSrc: '',
- filter: false,
- init: getSource,
- },
- configs,
- chapName,
- $noty = [],
- notyTimeout,
- domainName = location.host,
- tit = document.title,
- $doc = $(document),
- dlZip = new JSZip(),
- dlPrevZip = false,
- dlCurrent = 0,
- dlFinal = 0,
- dlTotal = 0,
- dlImages = [],
- dlAll = [],
- hasDownloadError = false,
- inProgress = false,
- inAuto = false,
- inCustom = false,
- inMerge = false;
-
- GM_registerMenuCommand('Download All Chapters', downloadAll);
- GM_registerMenuCommand('Download All To One File', downloadAllOne);
-
- $doc.on('keydown', function (e) {
- if (e.which === 89 && e.altKey) {
- // Alt+Y
- e.preventDefault();
- e.shiftKey ? downloadAllOne() : downloadAll();
- }
- });
-
- GM_addStyle(
- '#baivong_noty_wrap{display:none;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}#baivong_noty_content{color:#444}#baivong_noty_content strong{font-weight:700}#baivong_noty_content.baivong_info strong{color:#2196f3}#baivong_noty_content.baivong_success strong{color:#4caf50}#baivong_noty_content.baivong_warning strong{color:#ffc107}#baivong_noty_content.baivong_error strong{color:#f44336}#baivong_noty_content strong.centered{display:block;text-align:center}#baivong_noty_close{position:absolute;right:0;top:0;font-size:18px;color:#ddd;height:20px;width:20px;line-height:20px;text-align:center}#baivong_noty_wrap:hover #baivong_noty_close{color:#333}',
- );
-
- if (/(www\.)?kuaikanmanhua\.com/.test(domainName)) {
- configs = {
- link: '.title.fl a[href^="/web/comic/"]',
- name: 'h3.title',
- init: getKuaikanManhua,
- };
- } else if (/newtoki\d*\.(com|net)/.test(domainName)) {
- configs = {
- link: '.item-subject',
- name: function (_this) {
- return (
- $('[itemprop="description"] .view-content:first span').text().trim() +
- ' ' +
- $(_this)
- .contents()
- .filter(function (i, el) {
- return el.nodeType === 3;
- })
- .text()
- .trim()
- );
- },
- init: getNewToki69,
- };
- } else if (domainName === 'manhwa18.net') {
- configs = {
- link: '#tab-chapper .chapter',
- name: '[itemprop="name"]:last',
- contents: '.chapter-content',
- imgSrc: 'data-original',
- filter: true,
- };
- } else if (domainName === 'manytoon.com') {
- configs = {
- link: '.wp-manga-chapter a',
- name: function (_this) {
- return (
- $('.post-title h3')
- .contents()
- .filter(function (i, el) {
- return el.nodeType === 3;
- })
- .text()
- .trim() +
- ' ' +
- $(_this).text().trim()
- );
- },
- contents: '.reading-content',
- };
- } else if (domainName === '18comic.org') {
- configs = {
- link: '.episode:visible a, .dropdown-toggle.reading:visible',
- name: function (_this) {
- var $this = $(_this),
- mangaName = $('.panel-heading [itemprop="name"]:visible').text().trim();
- if ($this.hasClass('reading')) return mangaName;
- return (
- mangaName +
- ' ' +
- $this
- .find('li')
- .contents()
- .filter(function (i, el) {
- return el.nodeType === 3;
- })
- .text()
- .trim()
- );
- },
- contents: '.panel-body',
- imgSrc: 'data-original',
- };
- }
-
- 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();
- });