KissAnime-Bookmarks to MyAnimeList export [UPDATED 2020]

Generates an xml-File from your KA-Bookmarks that can be imported to MAL

// ==UserScript==
// @name         KissAnime-Bookmarks to MyAnimeList export [UPDATED 2020]
// @namespace    https://greasyfork.org/users/412318
// @version      3.7
// @description  Generates an xml-File from your KA-Bookmarks that can be imported to MAL
// @author       Hentai God and henrik9999
// @match        *://kissanime.ru/BookmarkList*
// @connect      api.malsync.moe
// @connect      graphql.anilist.co
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlHttpRequest
// @run-at       document-idle
// ==/UserScript==

/* beautify preserve:start */
function XMLWriter(a,b){if(a)this.encoding=a;if(b)this.version=b};(function(){XMLWriter.prototype={encoding:'ISO-8859-1',version:'1.0',formatting:'indented',indentChar:'\t',indentation:1,newLine:'\n',writeStartDocument:function(a){this.close();this.stack=[];this.standalone=a},writeEndDocument:function(){this.active=this.root;this.stack=[]},writeDocType:function(a){this.doctype=a},writeStartElement:function(c,d){if(d)c=d+':'+c;var a=this,b=a.active,e={n:c,a:{},c:[]};if(b){b.c.push(e);this.stack.push(b)}else a.root=e;a.active=e},writeEndElement:function(){this.active=this.stack.pop()||this.root},writeAttributeString:function(a,b){if(this.active)this.active.a[a]=b},writeString:function(a){if(this.active)this.active.c.push(a)},writeElementString:function(a,b,c){this.writeStartElement(a,c);this.writeString(b);this.writeEndElement()},writeCDATA:function(a){this.writeString('<![CDATA['+a+']]>')},writeComment:function(a){this.writeString('<!-- '+a+' -->')},flush:function(){var a=this,b='',c='',d=a.indentation,e=a.formatting.toLowerCase()=='indented',f='<?xml version="'+a.version+'" encoding="'+a.encoding+'"';if(a.stack&&a.stack[0])a.writeEndDocument();if(a.standalone!==undefined)f+=' standalone="'+!!a.standalone+'"';f+=' ?>';f=[f];if(a.doctype&&a.root)f.push('<!DOCTYPE '+a.root.n+' '+a.doctype+'>');if(e){while(d--)b+=a.indentChar}if(a.root)k(a.root,c,b,f);return f.join(e?a.newLine:'')},close:function(){var a=this;if(a.root)j(a.root);a.active=a.root=a.stack=null},getDocument:window.ActiveXObject?function(){var a=new ActiveXObject('Microsoft.XMLDOM');a.async=!1;a.loadXML(this.flush());return a}:function(){return(new DOMParser()).parseFromString(this.flush(),'text/xml')}};function j(a){var l=a.c.length;while(l--){if(typeof a.c[l]=='object')j(a.c[l])}a.n=a.a=a.c=null};function k(a,b,c,d){var e=b+'<'+a.n,f=a.c.length,g,h,i=0;for(g in a.a)e+=' '+g+'="'+a.a[g]+'"';e+=f?'>':' />';d.push(e);if(f){do{h=a.c[i++];if(typeof h=='string'){if(f==1)return d.push(d.pop()+h+'</'+a.n+'>');else d.push(b+c+h)}else if(typeof h=='object')k(h,b+c,c,d)}while(i<f);d.push(b+'</'+a.n+'>')}}})();

var blobData = (function() {
    var a = document.createElement("a");
    document.body.appendChild(a);
    a.style = "display: none";
    return function(data) {
        var xml = data,
            blob = new Blob([xml], {
                type: "octet/stream"
            }),
            url = window.URL.createObjectURL(blob);
        return url;
    };
}());
/* beautify preserve:end */

const api = {};

if (typeof GM_xmlhttpRequest !== 'undefined') {
    api.GM_xmlhttpRequest = GM_xmlhttpRequest;
} else if (
    typeof GM !== 'undefined' &&
    typeof GM.xmlHttpRequest !== 'undefined'
) {
    api.GM_xmlhttpRequest = GM.xmlHttpRequest;
}

const all = document.getElementsByClassName("trAnime");
const animes = document.getElementsByClassName("aAnime");
var current = 0;
var watchedcomplete = 0;
var unwatched = 0;
var watching = 0;
var total = all.length;

var v = new XMLWriter();
v.encoding = "UTF-8";
v.writeStartDocument(true);
v.writeStartElement('myanimelist');
v.writeComment("Document generated by the KA-BookMarks to MAL Script v3.7!");
v.writeComment("Created by TheTh0rus!");
v.writeComment("Updated by https://greasyfork.org/en/users/412318-henrik9999");


function init() {
    document.documentElement.innerHTML += "<div style='position:fixed;top:0px;left:0px;width:100%;height:100%;background-color:white;color:black;text-align:center;z-index:100000;' id='BMMAL'></div>";
    var span = document.createElement("span");
    span.id = "counter";
    var bar = document.createElement("progress");
    bar.setAttribute("value", 0);
    bar.setAttribute("max", total);
    bar.style = "width: 100%;";
    bar.id = "bar";
    var alertEl = document.createElement("span");
    alertEl.id = "alertHolder";
    alertEl.style = "color: red;";

    //var button = document.createElement("button");
    //button.innerHTML = "Export button incase of being stuck at the end (do not use only if stuck)";
    //button.addEventListener("click", function() {
    //    finished(true)
    //});

    document.getElementById("BMMAL").appendChild(bar);
    document.getElementById("BMMAL").appendChild(span);
    document.getElementById("BMMAL").appendChild(document.createElement("br"));
    //document.getElementById("BMMAL").appendChild(button);
    document.getElementById("BMMAL").appendChild(document.createElement("br"));
    document.getElementById("BMMAL").appendChild(alertEl);
    mainLoop();
}

function mainLoop() {
    var i = current;

    if (i < animes.length) {

        var title = animes[i].innerHTML;
        title = title.replace(/ *(\(dub\)|\(sub\)|\(uncensored\)|\(uncut\)|\(subbed\)|\(dubbed\))/i, '');
        title = title.replace(/ *\([^)]+audio\)/i, '');
        title = title.replace(/ BD( |$)/i, '');
        title = title.trim();

        var id = animes[i].getAttribute("href").split("/")[2].toLowerCase();

        //if completed and watched
        if (all[i].innerHTML.indexOf("Completed") > -1 && all[i].innerHTML.indexOf('style="display: inline" class="aRead"') > -1) {
            watchedcomplete += 1;
            current += 1;
            searchMalSync(id, title, "Completed");
            return;
        }
        //if airing and watched
        if (all[i].innerHTML.indexOf("Completed") == -1 && all[i].innerHTML.indexOf('style="display: inline" class="aUnRead"') == -1 && all[i].innerHTML.indexOf("Episode") > -1) {
            let episode = all[i].innerHTML.match(/episode\s\d+/i)
            if (episode && episode[0] && episode[0].replace(/\D+/g, '')) {
                episode = Number(episode[0].replace(/\D+/g, ''));
                watching += 1;
                current += 1;
                searchMalSync(id, title, "Watching", episode);
                return;
            }
        }
        //if not watching
        if (all[i].innerHTML.indexOf('style="display: inline" class="aUnRead"') > -1) {
            unwatched += 1;
            current += 1;
            searchMalSync(id, title, "Plan to Watch");
            return;
        }
        current += 1;
        mainLoop();
    } else {
        finished();
    }

}

async function searchMalSync(kissId, animeTitle, status, episode = 0) {
    console.log("searching malsync", kissId)
    const data = await new Promise((resolve, reject) => {
        api.GM_xmlhttpRequest({
            method: "GET",
            url: "https://api.malsync.moe/page/Kissanime/" + kissId,
            onload: function(response) {
                if (response.status === 200 && response.responseText) {
                    resolve(JSON.parse(response.responseText));
                } else {
                    resolve();
                }
            },
            onerror: function(response) {
                resolve();
            }
        });
    })
    if (data && data.malUrl) {
        getAnilistFromMalId(data.malUrl.split("/")[4], animeTitle, status, episode)
    } else {
        searchAnilist(animeTitle, status, episode)
    }
}

async function getAnilistFromMalId(malId, animeTitle, status, episode) {
    console.log("getting anilistdata for", malId);

    const query =
        `query ($id: Int, $type: MediaType) {
  Media(idMal: $id, type: $type) {
    idMal
    title {
      romaji
    }
    episodes
  }
}`
    const queryData = JSON.stringify({
        query,
        variables: {
            id: malId,
            type: "ANIME"
        }
    });
    const data = await new Promise((resolve, reject) => {
        api.GM_xmlhttpRequest({
            method: "POST",
            url: "https://graphql.anilist.co",
            data: queryData,
            headers: {
                'content-type': 'application/json',
                accept: 'application/json'
            },
            onload: function(response) {
                if (response.status === 200 && response.responseText) {
                    resolve(JSON.parse(response.responseText));
                } else {
                    resolve();
                }
            },
            onerror: function(response) {
                resolve();
            }
        });
    })
    if (data && data.data && data.data.Media && data.data.Media.idMal) {
        handleAnime(data.data.Media, animeTitle, status, episode)
    } else {
        handleError("could not find " + animeTitle + " on AniList")
    }


}

async function searchAnilist(animeTitle, status, episode) {
    console.log("searching anilist for", animeTitle);

    const query = `
    query ($search: String) {
      anime: Page (perPage: 10) {
        results: media (type: ANIME, search: $search) {
          idMal
          title {
            romaji
          }
          episodes
        }
      }
    }
  `;
    const queryData = JSON.stringify({
        query,
        variables: {
            search: animeTitle
        }
    });
    const data = await new Promise((resolve, reject) => {
        api.GM_xmlhttpRequest({
            method: "POST",
            url: "https://graphql.anilist.co",
            data: queryData,
            headers: {
                'content-type': 'application/json',
                accept: 'application/json'
            },
            onload: function(response) {
                if (response.status === 200 && response.responseText) {
                    resolve(JSON.parse(response.responseText));
                } else {
                    resolve();
                }
            },
            onerror: function(response) {
                resolve();
            }
        });
    })
    if (data && data.data && data.data.anime && data.data.anime.results && data.data.anime.results[0] && data.data.anime.results[0].idMal) {
        handleAnime(data.data.anime.results[0], animeTitle, status, episode)
    } else {
        handleError("could not find " + animeTitle + " on AniList")
    }


}

function handleAnime(data, animeTitle, status, episode) {
    try {
        console.log("handling anime:", data, animeTitle, status);
        var watched = 0;
        if (status == "Completed") {
            watched = data.episodes;
        }
        if (status == "Watching") {
            watched = episode;
        }
        v.writeStartElement('anime');
        v.writeComment("kissanime title " + animeTitle);
        v.writeComment("found title " + data.title.romaji);
        v.writeElementString('series_animedb_id', "" + data.idMal + "");
        v.writeElementString('my_status', "" + status + "");
        v.writeElementString('update_on_import', '1');
        v.writeElementString('my_watched_episodes', "" + watched + "");
        v.writeEndElement();
    } catch (e) {
        handleError("Scraping-error at " + animeTitle, false);
    }
    if (document.getElementById("bar") && document.getElementById("counter")) {
        document.getElementById("bar").value = current;
        document.getElementById("counter").innerHTML = "<b>" + current + " / " + total + "</b>";
    }
    setTimeout(function() {
        mainLoop();
    }, 1000);
}

function finished(skip = false) {
    if (current == total || skip) {
        v.writeStartElement('myinfo');

        v.writeElementString('user_export_type', '1');
        v.writeElementString('user_total_anime', "" + total + "");
        v.writeElementString('user_total_watching', "" + watching + "");
        v.writeElementString('user_total_completed', "" + watchedcomplete + "");
        v.writeElementString('user_total_onhold', '0');
        v.writeElementString('user_total_dropped', '0');
        v.writeElementString('user_total_plantowatch', "" + unwatched + "");

        v.writeEndElement();
        v.writeEndElement();
        v.writeEndDocument();
        let alerts = document.getElementById("alertHolder").innerHTML;
        document.getElementById("BMMAL").innerHTML = "<h3><a href='" + blobData(v.flush().replace('"true"', '"yes"')) + "' id='MALdl' style='color:blue;font-weight:bold;' download='importToMAL.xml'>Download MyAnimeList XML</a></h3>";
        document.getElementById("BMMAL").innerHTML += alerts;

        current = total + 1;
    }
}

function handleError(msg, next = true) {
    console.error(msg);
    addAlert(msg);
    if (next) {
        mainLoop();
    }
}

function addAlert(txt) {
    document.getElementById("alertHolder").innerHTML += "<br>" + txt;
}


init();