// ==UserScript== // @name Mangadex List Exporter // @namespace https://github.com/MarvNC // @version 0.29 // @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 // @downloadURL https://raw.githubusercontent.com/MarvNC/mangadex-list-exporter/master/mangadex-list-exporter.user.js // @updateURL https://raw.githubusercontent.com/MarvNC/mangadex-list-exporter/master/mangadex-list-exporter.user.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js // @grant none // ==/UserScript== // 1000ms delay between requests for MangaDex const DELAY = 1000; const apiURL = 'https://api.mangadex.org/v2/'; (function () { 'use strict'; // main function that executes on button click let save = async () => { // disable the button btn.onclick = null; let url = apiURL + 'user/me/followed-manga'; let json = await fetch(url, { credentials: 'include', }).then((response) => response.json()); let IDs = json.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`; 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 = apiURL + `manga/${manga.mangaId}`; let json = await fetch(url).then((response) => response.json()); let mangaInfo = json.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); }; function htmlDecode(html) { let txt = document.createElement('textarea'); txt.innerHTML = html; return txt.value; }