// ==UserScript==
// @name MangaDex Downloader
// @version 1.4
// @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/feed
// @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") || document.URL.includes("https://mangadex.org/titles/feed")) {
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 = '';
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(".com/manga/") ? document.URL.split("/")[4] : 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/" + document.URL.split("/")[4]);
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" ? "" : " [" + language_iso[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" ? "" : " [" + language_iso[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);
}
*/