Manga downloader for ganma.jp
// ==UserScript== // @name GanmaDownloader // @namespace https://github.com/Timesient/manga-download-scripts // @version 0.9 // @license GPL-3.0 // @author Timesient // @description Manga downloader for ganma.jp // @icon https://ganma.jp/web/favicon.ico // @homepageURL https://greasyfork.org/scripts/451869-ganmadownloader // @supportURL https://github.com/Timesient/manga-download-scripts/issues // @match https://ganma.jp/* // @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://update.greasyfork.org/scripts/451810/1398192/ImageDownloaderLib.js // @grant GM_info // @grant GM_xmlhttpRequest // ==/UserScript== (async function(axios, JSZip, saveAs, ImageDownloader) { 'use strict'; // reload page when enter or leave chapter const re = /https:\/\/ganma\.jp\/web\/reader\/(?<alias>.*)\/(?<episodeID>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})\/\d+/; const oldHref = window.location.href; const timer = setInterval(() => { const newHref = window.location.href; if (re.exec(newHref) && re.exec(oldHref) && re.exec(newHref).groups.episodeID === re.exec(oldHref).groups.episodeID) 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; // init data receiver window._configData = []; // run scripts to get string of config const configString = await fetch(window.location.href) .then(res => res.text()) .then(text => (new DOMParser()).parseFromString(text, 'text/html')) .then(dom => Array.from(dom.body.querySelectorAll('script'))) .then(scriptElements => scriptElements.forEach(ele => eval(ele.textContent.replace('self.__next_f.push([', 'window._configData.push([')))) .then(_ => window._configData.toString()); // get url of images let urls = JSON.parse(stringSlicer(configString, `"singleModeDisplayUnits":`, ']')).map(item => item.url); try { const { imageURL: afterwordImageURL } = JSON.parse(stringSlicer(configString, `"afterword":`, '}')); if (typeof afterwordImageURL === 'string' && afterwordImageURL.startsWith('https://')) urls = urls.concat(afterwordImageURL); } catch (error) { console.log(error); console.log('no afterword image'); } // get title const title = document.title.split(' - ').slice(0, 2).join(' '); // setup ImageDownloader ImageDownloader.init({ maxImageAmount: urls.length, getImagePromises, title }); // collect promises of image function getImagePromises(startNum, endNum) { return urls .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) }); }); } // slice string according to text content function stringSlicer(string, startText, endText) { const startIndex = string.indexOf(startText) + startText.length; const endIndex = string.indexOf(endText, startIndex); return string.slice(startIndex, endIndex + endText.length); } })(axios, JSZip, saveAs, ImageDownloader);