Goda漫画下载

打开章节页面,一键下载全部章节

질문, 리뷰하거나, 이 스크립트를 신고하세요.
/* eslint-disable no-multi-spaces */

// ==UserScript==
// @name               Goda漫画下载
// @namespace          http://tampermonkey.net/
// @version            0.1
// @description        打开章节页面,一键下载全部章节
// @author             PY-DNG
// @license            MIT
// @match              http*://cn.godamanga.com/chapterlist/*
// @require            https://greasyfork.org/scripts/456034/code/script.js
// @connect            godamanga.com
// @connect            godamanga.online
// @icon               data:image/png;base64,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
// @grant              GM_registerMenuCommand
// @grant              GM_xmlhttpRequest
// @grant              GM_download
// ==/UserScript==

(function __MAIN__() {
    'use strict';

	// Constances
	const CONST = {
		TextAllLang: {
			DEFAULT: 'zh-CN',
			'zh-CN': {
				DownloadAllChapters: '下载全部章节',
				FolderName: 'Goda漫画下载',
				DownloadFullName: '{FolderName}/{MangaName}/{ChapterName}/{ImageName}',
				ChapterFolderName: '{Number} - {Name}',
				ImageFileName: '{Number}.{Ext}'
			}
		}
	};

	// Init language
	const i18n = !Object.keys(CONST.TextAllLang).includes(navigator.language) ? navigator.language : CONST.TextAllLang.DEFAULT;
	CONST.Text = CONST.TextAllLang[i18n];

	main();
	function main() {
		GMXHRHook();
		GM_registerMenuCommand(CONST.Text.DownloadAllChapters, downloadAll);
	}

	function downloadAll() {
		const mangaName = $('header nav.ct-breadcrumbs a[href*="{URL}"] span'.replace('{URL}', $('.chapter_list_title').pathname)).innerText;
		const chapter_links = [...$All('ul.main.version-chaps a')].map(a => a.href).reverse();
		const len = chapter_links.length.toString().length;
		chapter_links.forEach((url, i) => downloadChapter(url, mangaName, fillNum(i+1, len)));
	}

	function downloadChapter(url, mangaName, chapterNo) {
		getDocument(url, function(oDom) {
			const chapterName = $(oDom, 'header nav.ct-breadcrumbs .last-item').innerText;
			const urls = [...$All(arguments[0], 'img[src*="/scomic/"]')].map(img => img.src);
			const len = urls.length.toString().length;
			urls.forEach((img_url, i) => downloadImage(img_url, mangaName, chapterName, chapterNo, fillNum(i+1, len)));
		});
	}

	function downloadImage(url, mangaName, title, chapterNo, imageNo, retry=3, err=null) {
		if (retry <= 0) {
			DoLog(LogLevel.Error, ['downloadImage: GM_xmlhttpRequest error(max retry reached)', err], true);
			return;
		}
		GM_xmlhttpRequest({
			url: url,
			responseType: 'blob',
			timeout: 20000,
			ontimeout: err => downloadImage(url, title, chapterNo, imageNo, retry-1, err),
			onerror: err => downloadImage(url, title, chapterNo, imageNo, retry-1, err),
			onload: (response) => {
				const blob = response.response;
				const dataUrl = URL.createObjectURL(blob);
				const ext = getExtname(blob.type.split(';')[0]) || 'unkown_filetype.jpg';
				const Text = CONST.Text;
				GM_download({
					name: replaceText(Text.DownloadFullName, {
						'{FolderName}': Text.FolderName,
						'{MangaName}': mangaName,
						'{ChapterName}': replaceText(Text.ChapterFolderName, {'{Number}' : chapterNo, '{Name}': title}),
						'{ImageName}': replaceText(Text.ImageFileName, {'{Number}': imageNo, '{Ext}': ext}),
					}),
					url: dataUrl
				})
			}
		});

		function getExtname(...args) {
			const map = {
				'image/png': 'png',
				'image/jpg': 'jpg',
				'image/gif': 'gif',
				'image/bmp': 'bmp',
				'image/jpeg': 'jpeg',
				'image/webp': 'webp',
				'image/tiff': 'tiff',
				'image/vnd.microsoft.icon': 'ico',
			};
			return map[args.find(a => map[a])];
		}
	}

	function fillNum(num, len) {
		const str = num.toString();
		return '0'.repeat(len-str.length) + str;
	}

	// Download and parse a url page into a html document(dom).
    // when xhr onload: callback.apply([dom, args])
    function getDocument(url, callback, args=[]) {
        GM_xmlhttpRequest({
            method       : 'GET',
            url          : url,
            responseType : 'blob',
			timeout      : 15 * 1000,
            onload       : function(response) {
                const htmlblob = response.response;
				parseDocument(htmlblob, callback, args);
            },
			onerror      : reqerror,
			ontimeout    : reqerror
        });

		function reqerror(e) {
			DoLog(LogLevel.Error, 'getDocument: Request Error');
			DoLog(LogLevel.Error, e);
			throw new Error('getDocument: Request Error')
		}
    }

	function parseDocument(htmlblob, callback, args=[]) {
		const reader = new FileReader();
		reader.onload = function(e) {
			const htmlText = reader.result;
			const dom = new DOMParser().parseFromString(htmlText, 'text/html');
			args = [dom].concat(args);
			callback.apply(null, args);
			//callback(dom, htmlText);
		}
		const charset = document.characterSet;
		reader.readAsText(htmlblob, charset);
	}

	// GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
	// Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
	// (If the request is invalid, such as url === '', will return false and will NOT make this request)
	// If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
	// Requires: function delItem(){...} & function uniqueIDMaker(){...}
	function GMXHRHook(maxXHR=5) {
		const GM_XHR = GM_xmlhttpRequest;
		const getID = uniqueIDMaker();
		let todoList = [], ongoingList = [];
		GM_xmlhttpRequest = safeGMxhr;

		function safeGMxhr() {
			// Get an id for this request, arrange a request object for it.
			const id = getID();
			const request = {id: id, args: arguments, aborter: null};

			// Deal onload function first
			dealEndingEvents(request);

			/* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
			// Stop invalid requests
			if (!validCheck(request)) {
				return false;
			}
			*/

			// Judge if we could start the request now or later?
			todoList.push(request);
			checkXHR();
			return makeAbortFunc(id);

			// Decrease activeXHRCount while GM_XHR onload;
			function dealEndingEvents(request) {
				const e = request.args[0];

				// onload event
				const oriOnload = e.onload;
				e.onload = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnload ? oriOnload.apply(null, arguments) : function() {};
				}

				// onerror event
				const oriOnerror = e.onerror;
				e.onerror = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
				}

				// ontimeout event
				const oriOntimeout = e.ontimeout;
				e.ontimeout = function() {
					reqFinish(request.id);
					checkXHR();
					oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
				}

				// onabort event
				const oriOnabort = e.onabort;
				e.onabort = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
				}
			}

			// Check if the request is invalid
			function validCheck(request) {
				const e = request.args[0];

				if (!e.url) {
					return false;
				}

				return true;
			}

			// Call a XHR from todoList and push the request object to ongoingList if called
			function checkXHR() {
				if (ongoingList.length >= maxXHR) {return false;};
				if (todoList.length === 0) {return false;};
				const req = todoList.shift();
				const reqArgs = req.args;
				const aborter = GM_XHR.apply(null, reqArgs);
				req.aborter = aborter;
				ongoingList.push(req);
				return req;
			}

			// Make a function that aborts a certain request
			function makeAbortFunc(id) {
				return function() {
					let i;

					// Check if the request haven't been called
					for (i = 0; i < todoList.length; i++) {
						const req = todoList[i];
						if (req.id === id) {
							// found this request: haven't been called
							delItem(todoList, i);
							return true;
						}
					}

					// Check if the request is running now
					for (i = 0; i < ongoingList.length; i++) {
						const req = todoList[i];
						if (req.id === id) {
							// found this request: running now
							req.aborter();
							reqFinish(id);
							checkXHR();
						}
					}

					// Oh no, this request is already finished...
					return false;
				}
			}

			// Remove a certain request from ongoingList
			function reqFinish(id) {
				let i;
				for (i = 0; i < ongoingList.length; i++) {
					const req = ongoingList[i];
					if (req.id === id) {
						ongoingList = delItem(ongoingList, i);
						return true;
					}
				}
				return false;
			}
		}
	}

	// Makes a function that returns a unique ID number each time
	function uniqueIDMaker() {
		let id = 0;
		return makeID;
		function makeID() {
			id++;
			return id;
		}
	}

	// Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
	function delItem(arr, delIndex) {
		arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
		return arr;
	}
})();