// ==UserScript== // @name Mangadex List Exporter // @namespace https://github.com/MarvNC // @version 0.22 // @description A userscript for exporting a MangaDex list to a .xml file for import to anime list sites. // @author Marv // @match https://mangadex.org/list* // @icon https://mangadex.org/favicon.ico // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js // @require http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js // @grant none // @downloadURL none // ==/UserScript== // 1000ms delay between requests for MangaDex const DELAY = 1000; (function () { 'use strict'; // main function that executes on button click let save = async () => { // disable the button btn.onclick = null; let userID = /(?<=\/list\/)\d+/.exec(document.URL)[0]; let url = `https://mangadex.org/api/v2/user/${userID}/followed-manga`; let response = await $.get(url); let IDs = response.data; console.log(IDs); // prettier-ignore let xml = ` 2 `; // create timer counting down to remaining time let countdownTimer = document.createElement('p'); countdownTimer.style = 'text-align:center;'; btn.parentElement.appendChild(countdownTimer); // loop through each manga ID in IDs for (let i = 0; i < IDs.length; i++) { console.log(`${i + 1} of ${IDs.length}: Getting details for manga ID: ${IDs[i].mangaId}`); // update time remaining, accounting for different delays countdownTimer.innerHTML = `Export time remaining: ${formatSeconds( ((IDs.length - i - 1) * DELAY) / 1000 )}`; // get the info from the manga then add it to xml getMangaInfo(IDs[i]).then((mangaInfo) => { btn.innerHTML = `${i + 1} of ${IDs.length} entries: Retrieved data for ${ mangaInfo.mangaTitle }`; // prettier-ignore xml += ` ${mangaInfo.malID} ${mangaInfo.mdID} ${mangaInfo.alID} ${mangaInfo.kitsuID} ${mangaInfo.muID} ${mangaInfo.volume} ${mangaInfo.chapter} 0000-00-00 0000-00-00 ${mangaInfo.rating} ${mangaInfo.status} 0 `; }); await timer(DELAY); } xml += ``; btn.innerHTML = `Completed list export of ${IDs.length} entries!`; // save the xml string as an xml with current date as filename let date = new Date(); let filename = `mangalist_${date.toISOString()}.xml`; let blob = new Blob([xml], { type: 'application/xml', }); saveAs(blob, filename); }; // the button to add var btn = document.createElement('BUTTON'); btn.innerHTML = `Click to export list; remember to set view mode to 'Simple list'`; btn.onclick = save; // add the button after user banner document.getElementsByClassName('card mb-3')[0].append(btn); })(); // accepts a manga list object thing var getMangaInfo = async (manga) => { const statuses = { 1: 'Reading', 2: 'Completed', 3: 'On hold', 4: 'Plan to read', 5: 'Dropped', 6: 'Re-reading', }; let url = `https://mangadex.org/api/v2/manga/${manga.mangaId}`; let mangaInfo = (await $.get(url)).data; let status = statuses[manga.followType]; let muID, alID, apSlug, kitsuID, malID; if (mangaInfo.links) { muID = mangaInfo.links.mu ? mangaInfo.links.mu : 0; alID = mangaInfo.links.al ? mangaInfo.links.al : 0; apSlug = mangaInfo.links.ap ? mangaInfo.links.ap : 0; kitsuID = mangaInfo.links.kt ? mangaInfo.links.kt : 0; malID = mangaInfo.links.mal ? mangaInfo.links.mal : 0; } let rating = manga.rating ? manga.rating : 0; return { mangaTitle: htmlDecode(mangaInfo.title), status: status, rating: rating, muID: muID, alID: alID, apSlug: apSlug, kitsuID: kitsuID, malID: malID, mdID: manga.mangaId, volume: manga.volume, chapter: manga.chapter, }; }; // Returns a Promise that resolves after "ms" Milliseconds var timer = (ms) => { return new Promise((res) => setTimeout(res, ms)); }; // seconds to HH:MM:SS var formatSeconds = (seconds) => { return new Date(seconds * 1000).toISOString().substr(11, 8); }; var htmlDecode = (value) => { return $('