Greasy Fork is available in English.
打开章节页面,一键下载全部章节
/* 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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###2IxcRbGEklO/9FQ9PD5/bveT9s4leMAxRygvrZ/Qs1k0QRXVRFnaDiAJCgjjquX8ysbwUcfWj6+N1IwwQ8ZcF+dFlfz+5KEL/iAJAPOp7D9LyFrGktwSk6UY3TgWbgaaUTa8N2BzvznTv3NLaMbxHjRoAWTSACB4nxtJ2wNnfu3dtfKeJXBQB5EGxPzUsrYBVAYjIWk2clg0qFAzxFOnFXmABwMqvCXASt###bX7l4nnKjtx+ExxLKeCpMbf94imTop4ATabDi705w8gpiWg3wWRXUC0IDgJM0+vSOxmQ9p6Hsb4MhaXWltnIRwGkY4F0APZo5Zry64tOuoTC1/UgCQAYlu6ReJzq1yw1WVqgQReAAyGcLT9h1bQS+AIS/BiDHuy/ZPhg4SMyvkDJ+/kdNaldQnr1SAFRVDlBoK9jZOncOG8aNYEcvEEdImKeEwADg3NWfu38GMiLbjWUgFv+9cLMiJc+ZbQZAlyTZ0qa9pdLyfiKARAIAY6cE15XcCcK32fUkhnXxtCwAOMattrYpmQxaTaizmfkbIJwDkLD6calbc3MTpfOQ7HqAn8uMJHdUi+VXzQ7glT3lU65qUiuJ6LqccyRobuAbAHkW32DVpbQBIfr54rVjIEXubj+emXsIoB0AfmHY9rZjn7QeLDeQ0+taeukXKQ5QOOA8EFiZlwP6OkDNAVgWOYjTgnMMpIR99/S6urHgikPDwwpHG000DZtS+cOyRqfYyphJhAUEfA2MRQDac+w9P5aJxqMBGgH4XQI/aZv2qzPq6w9WU9ZHVgk8GUrzDiVoXAbmP9NEQowZAegI74LpZSI+qiV7N8EkcBNA8u4WJrQSO8GtsruFrXsJdx8BcBigLib+jWnpLed+0jNQyXP9ydYz8iLgeBMQjnDkM50mU19AjG8AvOgEMtfrOhRW4SiVs1iO2xbYC+BdIt5KZOwY6Z3ZHyVWH1sOUDzwXJKqZoN0h5bqpYw/IVAHu0GosmNLJaRX0Eg/IfogEwag6X0C/04T3lYJuzvz8awjcSB8frKR1QFORo28wcVsyDZDGSll6w6R00xYCKY0iPOsOwhACMHFUDMIUDeI32fgQ9LcrRN2b8PIyGE/yRlPNrdK/j22AChcpJyGnqyzmhpttk+DoZsVKA3GfNfjRi0ApxiYTq62bsKp6FFYlUMqdbEQWsTCMJPIcgyQ5gEo/J5ZdUPZfazpkKWyRwCMLO/tzURZvnsB0ikBgOKJ5gsydHV0mKLZN5hmEscwLWNgWoJQbxOSZFMyX9FDS8k1rS1S2iLQiPzTNn1u1tlDU5NJUezQ0dUl4JC8ep6qd3lZ/Cj0OSUBEIWFjcsYagCIC6VCGmcNACEtbFxeWwNAXCgV0jhrAAhpYePy2hoA4kKpkMZZA0BICxuX19YAEBdKhTTOGgBCWti4vLYGgLhQKqRx1gAQ0sLG5bX/D+OJ8/m7Dr1lAAAAAElFTkSuQmCC // @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.r###lt; 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; } })();