// ==UserScript== // @name MangaDex Downloader // @version 1.5 // @description A userscript to add download-buttons to mangadex // @author NO_ob, icelord // @homepage https://github.com/NO-ob/mangadex-scripts // @match https://mangadex.org // @match https://mangadex.org/settings // @match https://www.mangadex.org/settings // @match https://mangadex.org/title/* // @match https://mangadex.org/titles/* // @match https://www.mangadex.org/title/* // @icon https://mangadex.org/favicon.ico // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js // @grant GM_xmlhttpRequest // @grant GM.setValue // @grant GM.getValue // @namespace https://greasyfork.org/users/821816 // @downloadURL none // ==/UserScript== //Required to retrieve iso_codes var language_iso = { 'ar': 'Arabic', 'bn': 'Bengali', 'bg': 'Bulgarian', 'ca': 'ca', 'zh': 'Chinese', 'cs': 'Czech', 'da': 'Danish', 'nl': 'Dutch', 'en': 'English', 'fil': 'Filipino', // idk 'fi': 'Finnish', 'fr': 'French', 'de': 'German', 'el': 'Greek', 'hu': 'Hungarian', 'id': 'Indonesian', 'it': 'Italian', 'ja': 'Japanese', 'ko': 'Korean', 'ms': 'Malaysian', 'mn': 'Mongolian', 'fa': 'Persian', 'pl': 'Polish', 'pt': 'Portuguese (Brazil)', 'Portuguese (Portugal)': 'xdsdsdsd', //idk 'ro': 'Romanian', 'ru': 'Russian', 'sh': 'Serbo-Croatian', 'Spanish (LATAM)': 'spa', //idk 'es': 'Spanish (Spain)', 'sv': 'Swedish', 'th': 'Thai', 'tr': 'Thai', 'vi': 'Vietnamese' }; (function() { 'use strict'; //Settings or download // Need to observe constantly or script wont observe changes when loading (new MutationObserver(pageObserve)).observe(document, { childList: true, subtree: true }); })(); // observe for page to actually load, fuck webapps function pageObserve(changes, observer) { // Check if scriptRan elem has been added to the page so elems aren't added on every observer change if (!document.querySelector("div#scriptRan")) { if (document.URL.includes("https://mangadex.org/settings")) { addScriptSettings(); } else if (document.URL.includes("https://mangadex.org/title")) { addDownloadButtons(); } } else { //console.log("scriptRan still on page"); } } function addObserverElem(parent) { let elem = document.createElement("div"); elem.setAttribute("id", "scriptRan"); parent.appendChild(elem); } function addScriptSettings() { if (document.querySelector("div.grid-auto-rows")) { addObserverElem(document.querySelector("div.grid-auto-rows")); let settingsGroup = document.querySelector("div.grid-auto-rows"); let navBar = document.querySelector("div.static.self-start"); let newNavItem = document.createElement("div"); newNavItem.innerHTML = '
Download Settings
'; navBar.appendChild(newNavItem); navBar.lastChild.addEventListener('click', () => { window.location.href = window.location.href.split("#")[0] + "#dlSettings"; }, false); //Add options let newSettingsDiv = document.createElement("div"); newSettingsDiv.innerHTML = '
' + '' + '
' + '
' + '
Download Settings
' + '
' + '
' + '
File extension of downloaded manga.
' + '' + '
' + '
' + '
Type of release info to pack into the archive.
' + '' + '
' + '
' + '
Number of parallel downloads.
' + '
' + '' + '
' + '
' + '
' + '
Saves downloader settings.
' + '
' + '' + '
' + '
' + '
' + '
'; settingsGroup.appendChild(newSettingsDiv); //Add handler to save options document.getElementById('save_downloader_settings').addEventListener('click', () => { localStorage.setItem('file-extension', document.getElementById('file-extension').value); localStorage.setItem('parallel-downloads', parseInt(document.getElementById('parallel-downloads').value)); localStorage.setItem('chapter-info', document.getElementById('chapter-info').value); alert('Updated settings!'); }, false); } } function addDownloadButtons() { if (document.querySelectorAll("div.flex.chapter").length > 0 || document.querySelectorAll("div.chapter-feed__container").length > 0) { (document.querySelectorAll("div.flex.chapter").length > 0) ? addObserverElem(document.querySelectorAll("div.flex.chapter")[0]): addObserverElem(document.querySelectorAll("div.chapter-feed__container")[0]); console.log("flex chapter length =" + document.querySelectorAll("div.flex.chapter").length); document.querySelectorAll("div.flex.chapter").forEach((chapterRow) => { let chapterID = chapterRow.querySelector("div > div > a").href.split('/').pop(); let dlButton = document.createElement("button"); let mangaID = document.URL.includes("/title/") ? document.querySelector("a.group.flex.items-start").getAttribute("to").split("/")[2] : document.querySelector("div.flex.chapter").parentElement.parentElement.parentElement.parentElement.querySelector("div.chapter-feed__title > a").href.split("/")[4]; dlButton.innerHTML = "Download"; dlButton.setAttribute("class", "dlButton"); dlButton.setAttribute("id", "dl-" + chapterID); let divForButton = chapterRow.querySelector("div > div.flex.space-x-2.items-center"); divForButton.insertBefore(dlButton, divForButton.firstChild); divForButton.firstChild.addEventListener('click', () => { startChapterDownload(chapterID, divForButton, mangaID); }, false); }); } } //Function to download a chapter (called by download-buttons) async function startChapterDownload(chapterID, parent, mangaID) { //Inject progressbar let progressDiv = document.createElement("div"); progressDiv.innerHTML = '
' + '
' + '
' + '
'; parent.removeChild(parent.firstChild); parent.insertBefore(progressDiv, parent.firstChild); //Mark downloaded chapter as read if (parent.querySelector("svg.feather-eye")) { parent.querySelector("svg.feather-eye").dispatchEvent(new MouseEvent('click')); parent.querySelector("svg.feather-eye").dispatchEvent(new MouseEvent('mousedown')); } let chapterData = await getChapterMetaData(chapterID); if (chapterData != null) { let urlList = await getFileUrls(chapterData); if (urlList.length > 0) { let mangaData = await getMangaData(mangaID); if (mangaData != null) { createZipFile(mangaData, urlList, chapterData); } } } } async function getChapterMetaData(chapterID) { //https://api.mangadex.org/chapter/2de64986-f092-4027-ab35-f78c4a1b54f2 let resp = await fetch("https://api.mangadex.org/chapter/" + chapterID); if (resp.ok) { let json = await resp.json(); console.log("Got chapter metadata for: " + json.data.id); return json.data; } else { console.log("Failed to get metadata for: " + chapterID); return null; } } async function getFileUrls(chapterData) { //https://api.mangadex.org/at-home/server/2de64986-f092-4027-ab35-f78c4a1b54f2 let urlList = []; let resp = await fetch("https://api.mangadex.org/at-home/server/" + chapterData.id); let json = await resp.json(); if (resp.ok) { for (let i = 0; i < chapterData.attributes.data.length; i++) { urlList.push(json.baseUrl + "/data/" + chapterData.attributes.hash + "/" + chapterData.attributes.data[i]); } console.log("Created url list for: " + chapterData.id); console.log("Url list length is: " + urlList.length); } else { console.log("Failed to get baseURL for: " + chapterData.ID); } return urlList; } async function getMangaData(mangaID) { //https://api.mangadex.org/manga/036fce64-6de7-4668-b7ba-66596d32e059 let resp = await fetch("https://api.mangadex.org/manga/" + mangaID); console.log("https://api.mangadex.org/manga/" + mangaID); if (resp.ok) { let json = await resp.json(); console.log("Got manga metadata for: " + json.data.attributes.title.en + "[" + json.data.id + "]"); return json.data } else { console.log("Failed to get metadata for manga: " + document.URL.split("/")[4]); } return null; } async function getUser(userID) { console.log("getting user: " + userID); let resp = await fetch("https://api.mangadex.org/user/" + userID); if (resp.ok) { let user = await resp.json(); return user.data; } else { console.log("Failed to get metadata for user: " + userID); } return null; } async function getScanlationGroupName(groupID) { console.log("getting group: " + groupID); let resp = await fetch("https://api.mangadex.org/group/" + groupID); if (resp.ok) { let group = await resp.json(); return group.data.attributes.name != null ? [group.data.attributes.name] : []; } else { console.log("Failed to get metadata for group: " + groupID); } return []; } function normalizeAltNames(alts) { let altNames = []; alts.forEach((alt) => { if (alt.en) { altNames.push(alt.en.normalize()); } }); return altNames; } function getTags(tags) { let themeList = []; let genreList = []; let formatList = []; tags.forEach((tag) => { switch (tag.attributes.group) { case "theme": themeList.push(tag.attributes.name.en.normalize()); break; case "genre": genreList.push(tag.attributes.name.en.normalize()); break; case "theme": formatList.push(tag.attributes.name.en.normalize()); break; } }); return { "theme": themeList, "genre": genreList, "formatList": formatList } } async function createZipFile(mangaData, urlList, chapterData) { //Fetch page-urls and download them let id = chapterData.id; let tagsMap = getTags(mangaData.attributes.tags); // let uploaderID = ""; let groupID = ""; chapterData.relationships.forEach((relationship) => { if (relationship.type == "user") { uploaderID = relationship.id; } }); chapterData.relationships.forEach((relationship) => { if (relationship.type == "scanlation_group") { groupID = relationship.id; } }); let uploader = await getUser(uploaderID); let group = await getScanlationGroupName(groupID); let link = 'https://mangadex.org/chapter/' + mangaData.id; const chapterInfo = { manga: mangaData.attributes.title.en, altnames: normalizeAltNames(mangaData.attributes.altTitles), link: 'https://mangadex.org/chapter/' + mangaData.id, chapter: chapterData.attributes.chapter, volume: chapterData.attributes.volume || null, title: chapterData.attributes.title || null, groups: group.join(), genres: tagsMap.genre, //get user from chapterdata https://api.mangadex.org/user/ id uploader: uploader, posted: chapterData.attributes.publishAt, language: language_iso[chapterData.attributes.translatedLanguage], translated: chapterData.attributes.translatedLanguage, images: urlList }; //Fetch all pages using JSZip let zip = new JSZip(); let zipFilename = chapterInfo.manga + (chapterInfo.language == "English" ? "" : " [" + chapterInfo.language + "]") + " - c" + (chapterInfo.chapter < 100 ? chapterInfo.chapter < 10 ? '00' + chapterInfo.chapter : '0' + chapterInfo.chapter : chapterInfo.chapter) + (chapterInfo.volume ? " (v" + (chapterInfo.volume < 10 ? '0' + chapterInfo.volume : chapterInfo.volume) + ")" : "") + " [" + chapterInfo.groups + "]" + (localStorage.getItem("file-extension") || '.zip'); let page_count = chapterInfo.images.length; let active_downloads = 0; let failed = false; //Build metadata-file based on setting if (localStorage.getItem("chapter-info") == '1') { let textFile = ''; textFile += chapterInfo.manga + '\n'; textFile += chapterInfo.altnames.join(', ') + '\n'; textFile += chapterInfo.link + '\n\n'; textFile += 'Chapter: ' + chapterInfo.chapter + '\n'; textFile += 'Volume: ' + (chapterInfo.volume !== null ? chapterInfo.volume : 'Unknown') + '\n'; textFile += 'Title: ' + (chapterInfo.title != null ? chapterInfo.title : 'Unknown') + '\n'; textFile += 'Groups: ' + chapterInfo.groups + '\n'; textFile += 'Genres: ' + chapterInfo.genres.join(', ') + '\n'; textFile += 'Uploader: ' + (chapterInfo.uploader != null ? chapterInfo.uploader.attributes.username + ' (ID: ' + chapterInfo.uploader.id + ')\n' : ""); textFile += 'Posted: ' + chapterInfo.posted + '\n'; textFile += 'Language: ' + chapterInfo.language + (chapterInfo.translated ? ' (TL) \n' : '\n'); textFile += 'Length: ' + chapterInfo.images.length + '\n\n'; chapterInfo.images.forEach((image, i) => { textFile += 'Image ' + (i + 1) + ': ' + image + '\n'; }); textFile += '\n\nDownloaded at ' + (new Date()) + '\n'; textFile += 'Generated by MangaDex Downloader. https://github.com/NO-ob/mangadex-scripts/'; zip.file('info.txt', textFile.replace(/\n/gi, '\r\n')); } else if (localStorage.getItem("chapter-info") == '2') { zip.file('info.json', JSON.stringify(chapterInfo, null, 4)); } console.log("Starting chapter download"); let page_urls = urlList; let interval = setInterval(() => { if (active_downloads < (localStorage.getItem("parallel-downloads") || 3) && page_urls.length > 0) { let to_download = page_urls.shift(); let current_page = page_count - page_urls.length; let page_filename = (chapterInfo.manga + (chapterInfo.language == "English" ? "" : " [" + chapterInfo.language + "]") + " - c" + (chapterInfo.chapter < 100 ? chapterInfo.chapter < 10 ? '00' + chapterInfo.chapter : '0' + chapterInfo.chapter : chapterInfo.chapter) + (chapterInfo.volume ? " (v" + (chapterInfo.volume < 10 ? '0' + chapterInfo.volume : chapterInfo.volume) + ")" : "") + " - p" + (current_page < 100 ? current_page < 10 ? '00' + current_page : '0' + current_page : current_page) + " [" + chapterInfo.groups + "]" + '.' + to_download.split('.').pop()) .replace(/[\/\?<>\\:\*\|":\x00-\x1f\x80-\x9f]/gi, '_') active_downloads++; GM_xmlhttpRequest({ method: 'GET', url: to_download, responseType: 'arraybuffer', onload: function(data) { zip.file(page_filename, data.response, { binary: true }); if (!failed) { setProgress(id, ((page_count - page_urls.length) / page_count) * 100); } active_downloads--; }, onerror: function(data) { alert('A page-download failed. Check the console for more details.'); console.error(data); clearInterval(interval); setProgress(chapterData.id, -1); } }); } else if (active_downloads === 0 && page_urls.length === 0) { clearInterval(interval); zip.generateAsync({ type: "blob" }).then((zipFile) => { saveAs(zipFile, zipFilename); setProgress(chapterData.id, -1); }); } }, 500); } //Set progress of download for id function setProgress(id, progress) { console.log(id); if (progress !== -1) { document.getElementById('progress-in-' + id).style.width = progress + '%'; } else { document.getElementById('progress-out-' + id).remove(); } } /* function getAPIKey(){ var URL = "https://api.mangadex.org/auth/login"; GM_xmlhttpRequest ( { method: "POST", url: URL, responseType: "text/*", headers: { 'Content-Type': 'application/json; charset=UTF-8' }, data : JSON.stringify({'username' : '', 'password':''}), onload: function(response) {printKey(response.responseText);} }); } function printKey(responseText){ let resp = JSON.parse(responseText); console.log(resp.token.session); console.log(resp.token.refresh); } */