Mangadex List Exporter

A userscript for exporting a MangaDex list to a .xml file for import to anime list sites.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
// ==/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 = 
`<?xml version="1.0" encoding="UTF-8" ?>
	
<!--
Created by Mangadex List Export userscript
Programmed by Marv
-->

<myanimelist>

	<myinfo>
		<user_export_type>2</user_export_type>
	</myinfo>

`;
    // 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 += 
`	<manga>
		<manga_mangadb_id>${mangaInfo.malID}</manga_mangadb_id>
		<manga_mangadex_id>${mangaInfo.mdID}</manga_mangadex_id>
		<manga_anilist_id>${mangaInfo.alID}</manga_anilist_id>
		<manga_kitsu_id>${mangaInfo.kitsuID}</manga_kitsu_id>
		<manga_mangaupdates_id>${mangaInfo.muID}</manga_mangaupdates_id>
		<manga_animeplanet_slug><![CDATA[${mangaInfo.apSlug}]]></manga_animeplanet_slug>
		<manga_title><![CDATA[${mangaInfo.mangaTitle}]]></manga_title>
		<my_read_volumes>${mangaInfo.volume}</my_read_volumes>
		<my_read_chapters>${mangaInfo.chapter}</my_read_chapters>
		<my_start_date>0000-00-00</my_start_date>
		<my_finish_date>0000-00-00</my_finish_date>
		<my_score>${mangaInfo.rating}</my_score>
		<my_status>${mangaInfo.status}</my_status>
		<update_on_import>0</update_on_import>
	</manga>

`;
      });
      await timer(DELAY);
    }
    xml += `</myanimelist>`;
    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 $('<textarea/>').html(value).text();
};