Greasy Fork is available in English.

Douki

Import Anime and Manga Lists from Anilist (see https://anilist.co/forum/thread/2654 for more info)

// ==UserScript==
// @name        Douki
// @namespace   http://gilmoreg.com
// @description Import Anime and Manga Lists from Anilist (see https://anilist.co/forum/thread/2654 for more info)
// @version     0.2.5
// @include     https://myanimelist.net/*
// ==/UserScript==

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ([
/* 0 */,
/* 1 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.Log = void 0;
const const_1 = __webpack_require__(2);
const Util_1 = __webpack_require__(3);
const getCountLog = (operation, type) => document.querySelector(Util_1.id(`douki-${operation}-${type}-items`));
class Log {
    constructor() {
        this.errorLogElement = null;
        this.syncLogElement = null;
        this.debugLogElement = null;
    }
    get errorLog() {
        if (!this.errorLogElement) {
            this.errorLogElement = document.querySelector(Util_1.id(const_1.ERROR_LOG_ID));
        }
        return this.errorLogElement;
    }
    get syncLog() {
        if (!this.syncLogElement) {
            this.syncLogElement = document.querySelector(Util_1.id(const_1.SYNC_LOG_ID));
        }
        return this.syncLogElement;
    }
    get debugLog() {
        if (!this.debugLogElement) {
            this.debugLogElement = document.querySelector(Util_1.id(const_1.DEBUG_LOG_ID));
        }
        return this.debugLogElement;
    }
    clearErrorLog() {
        if (this.errorLog) {
            this.errorLog.innerHTML = '';
        }
    }
    clearSyncLog() {
        if (this.syncLog) {
            this.syncLog.innerHTML = '';
        }
    }
    clearDebugLog() {
        if (this.debugLog) {
            this.debugLog.innerHTML = '';
        }
    }
    clear(type = '') {
        console.clear();
        if (type !== 'error')
            this.clearSyncLog();
        if (type !== 'sync')
            this.clearErrorLog();
        this.clearDebugLog();
    }
    error(msg) {
        if (this.errorLog) {
            this.errorLog.innerHTML += `<li>${msg}</li>`;
        }
        else {
            console.error(msg);
        }
    }
    info(msg) {
        if (this.syncLog) {
            this.syncLog.innerHTML += `<li>${msg}</li>`;
        }
        else {
            console.info(msg);
        }
    }
    debug(msg) {
        if (this.debugLog) {
            this.debugLog.innerHTML += `<li>${msg}</li>`;
        }
        else {
            console.debug(msg);
        }
    }
    addCountLog(operation, type, max) {
        const opName = Util_1.getOperationDisplayName(operation);
        const logId = `douki-${operation}-${type}-items`;
        this.info(`${opName} <span id="${logId}">0</span> of ${max} ${type} items.`);
    }
    updateCountLog(operation, type, count) {
        const countLog = getCountLog(operation, type);
        if (!countLog)
            return;
        countLog.innerHTML = `${count}`;
    }
}
exports.Log = Log;
exports.default = new Log();


/***/ }),
/* 2 */
/***/ ((__unused_webpack_module, exports) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.DEBUG_LOG_ID = exports.DEBUG_SETTING_ID = exports.DROPDOWN_ITEM_ID = exports.DATE_SETTINGS_KEY = exports.SETTINGS_KEY = exports.ANILIST_USERNAME_ID = exports.ERROR_LOG_DIV_ID = exports.ERROR_LOG_TOGGLE_ID = exports.ERROR_LOG_ID = exports.SYNC_LOG_ID = exports.DOUKI_IMPORT_BUTTON_ID = exports.CONTENT_ID = exports.DATE_SETTING_ID = exports.DOUKI_ANILIST_IMPORT_ID = exports.DOUKI_FORM_ID = void 0;
exports.DOUKI_FORM_ID = 'douki-form';
exports.DOUKI_ANILIST_IMPORT_ID = 'douki-anilist-import';
exports.DATE_SETTING_ID = 'douki-date_format';
exports.CONTENT_ID = 'content';
exports.DOUKI_IMPORT_BUTTON_ID = 'douki-import';
exports.SYNC_LOG_ID = 'douki-sync-log';
exports.ERROR_LOG_ID = 'douki-error-log';
exports.ERROR_LOG_TOGGLE_ID = 'douki-error-log-toggle';
exports.ERROR_LOG_DIV_ID = 'douki-error-log-div';
exports.ANILIST_USERNAME_ID = 'douki-anilist-username';
exports.SETTINGS_KEY = 'douki-settings';
exports.DATE_SETTINGS_KEY = 'douki-settings-date';
exports.DROPDOWN_ITEM_ID = 'douki-sync';
exports.DEBUG_SETTING_ID = 'douki-debug';
exports.DEBUG_LOG_ID = 'douki-debug-log';


/***/ }),
/* 3 */
/***/ ((__unused_webpack_module, exports) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getOperationDisplayName = exports.id = exports.sleep = void 0;
const sleep = (ms) => new Promise(resolve => setTimeout(() => resolve(null), ms));
exports.sleep = sleep;
const id = (str) => `#${str}`;
exports.id = id;
const getOperationDisplayName = (operation) => {
    switch (operation) {
        case 'add':
            return 'Adding';
        case 'edit':
            return 'Updating';
        case 'complete':
            return 'Fixing';
        default:
            throw new Error('Unknown operation type');
    }
};
exports.getOperationDisplayName = getOperationDisplayName;


/***/ }),
/* 4 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.DomMethods = void 0;
const const_1 = __webpack_require__(2);
const Util_1 = __webpack_require__(3);
const importFormHTML = `
    <div id="${const_1.DOUKI_FORM_ID}">
        <h1 class="h1">Import From Anilist</h1>
        <div style="padding: 20px">
            <p><strong>NOTICE</strong>: Use this script at your own risk. The author takes no responsibility for any damages of any kind.</p>
            <p>It is <em>highly</em> recommended that you try this script out on a test MAL account before importing to your main account.</p>
            <p>Visit <a href="https://anilist.co/forum/thread/2654" target="_blank" rel="noopener noreferrer">the Anilist thread</a> for this script to ask questions or report problems.</p>
            <p>Please be patient. If the import goes any faster you will be in violation of MyAnimeList's Terms of Service.</p>
        </div>
        <form id="${const_1.DOUKI_ANILIST_IMPORT_ID}" style="padding: 5px 0px 10px 0px">
            <p style="margin: 10px"><label>Anilist Username: <input type="text" id="${const_1.ANILIST_USERNAME_ID}" /></label></p>
            <p style="margin: 10px">
            <label>Date Format:
                <select id="${const_1.DATE_SETTING_ID}" class="inputtext">
                <option value="a" selected>American (MM-DD-YY)
                <option value="e" >European (DD-MM-YY)
                </select>
            </label>
            <label>Debug Mode:
                <input id="${const_1.DEBUG_SETTING_ID}" type="checkbox" name="debug">
            </label>
            </p>
            <p style="margin: 10px"><button id="${const_1.DOUKI_IMPORT_BUTTON_ID}">Import</button></p>
        </form>
        <br />
        <ul id="${const_1.SYNC_LOG_ID}" style="list-type: none;"></ul>
        <p style="margin: 10px"><button id="${const_1.ERROR_LOG_TOGGLE_ID}" style="border: none">Show items that could not be synced</button></p>
        <div id="${const_1.ERROR_LOG_DIV_ID}" style="display: none;">
            <p style="margin: 10px">Anilist does not have a MAL ID for the following items. If a verified MAL entry exists for any of these, contact an Anilist data mod to have it added.</p>
            <ul id="${const_1.ERROR_LOG_ID}" style="list-type: none;"></ul>
        </div>
        <div>
            <ul id="${const_1.DEBUG_LOG_ID}" style="list-type: none;"></ul>
        </div>
    </div>
`;
const getLocalStorageSetting = (setting) => {
    if (localStorage) {
        const value = localStorage.getItem(setting);
        if (value)
            return JSON.parse(value);
    }
    return null;
};
const setLocalStorageSetting = (setting, value) => {
    if (localStorage) {
        localStorage.setItem(setting, JSON.stringify(value));
    }
};
class DomMethods {
    constructor() {
        this.csrfToken = null;
    }
    addDropDownItem() {
        if (document.querySelector(Util_1.id(const_1.DROPDOWN_ITEM_ID)))
            return;
        const selector = '.header-menu-dropdown > ul > li:last-child';
        const dropdown = document.querySelector(selector);
        if (dropdown) {
            const html = `<li><a aria-role="button" style="cursor: pointer" id="${const_1.DROPDOWN_ITEM_ID}">Import from Anilist</a></li>`;
            dropdown.insertAdjacentHTML('afterend', html);
            const link = document.querySelector(Util_1.id(const_1.DROPDOWN_ITEM_ID));
            link && link.addEventListener('click', function (e) {
                e.preventDefault();
                window.location.replace('https://myanimelist.net/import.php');
            });
        }
    }
    addImportForm(syncFn) {
        if (document.querySelector(Util_1.id(const_1.DOUKI_FORM_ID)))
            return;
        const element = document.querySelector(Util_1.id(const_1.CONTENT_ID));
        if (!element) {
            throw new Error('Unable to add form to page');
        }
        element.insertAdjacentHTML('afterend', importFormHTML);
        this.addImportFormEventListeners(syncFn);
    }
    // TODO break this up
    addImportFormEventListeners(syncFn) {
        const importButton = document.querySelector(Util_1.id(const_1.DOUKI_IMPORT_BUTTON_ID));
        importButton && importButton.addEventListener('click', function (e) {
            syncFn(e);
        });
        const textBox = document.querySelector(Util_1.id(const_1.ANILIST_USERNAME_ID));
        textBox && textBox.addEventListener('change', function (e) {
            setLocalStorageSetting(const_1.SETTINGS_KEY, e.target.value);
        });
        const username = getLocalStorageSetting(const_1.SETTINGS_KEY);
        if (username && textBox) {
            textBox.value = username;
        }
        const dateFormatPicker = document.querySelector(Util_1.id(const_1.DATE_SETTING_ID));
        dateFormatPicker && dateFormatPicker.addEventListener('change', function (e) {
            setLocalStorageSetting(const_1.DATE_SETTINGS_KEY, e.target.value);
        });
        const dateOption = getLocalStorageSetting(const_1.DATE_SETTINGS_KEY);
        if (dateOption && dateFormatPicker) {
            dateFormatPicker.value = dateOption;
        }
        const errorToggle = document.querySelector(Util_1.id(const_1.ERROR_LOG_TOGGLE_ID));
        errorToggle && errorToggle.addEventListener('click', function (e) {
            e.preventDefault();
            const errorLog = document.querySelector(Util_1.id(const_1.ERROR_LOG_DIV_ID));
            if (errorLog.style.display === 'none') {
                errorLog.style.display = 'block';
            }
            else {
                errorLog.style.display = 'none';
            }
        });
    }
    getDateSetting() {
        const dateSetting = document.querySelector(Util_1.id(const_1.DATE_SETTING_ID));
        if (!dateSetting || !dateSetting.value)
            throw new Error('Unable to get date setting');
        return dateSetting.value;
    }
    getDebugSetting() {
        const debugSetting = document.querySelector(Util_1.id(const_1.DEBUG_SETTING_ID));
        if (!debugSetting)
            throw new Error('Unable to get debug setting');
        return debugSetting.checked;
    }
    getCSRFToken() {
        if (this.csrfToken)
            return this.csrfToken;
        const csrfTokenMeta = document.querySelector('meta[name~="csrf_token"]');
        if (!csrfTokenMeta)
            throw new Error('Unable to get CSRF token - no meta element');
        const csrfToken = csrfTokenMeta.getAttribute('content');
        if (!csrfToken)
            throw new Error('Unable to get CSRF token - no content attribute');
        this.csrfToken = csrfToken;
        return csrfToken;
    }
    getMALUsername() {
        const malUsernameElement = document.querySelector('.header-profile-link');
        if (!malUsernameElement)
            return null;
        return malUsernameElement.innerText;
    }
    getAnilistUsername() {
        const anilistUserElement = document.querySelector('#douki-anilist-username');
        if (!anilistUserElement)
            throw new Error('Unable to get Anilist username');
        return anilistUserElement.value;
    }
}
exports.DomMethods = DomMethods;
exports.default = new DomMethods();


/***/ }),
/* 5 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getAnilistList = void 0;
const Log_1 = __webpack_require__(1);
const flatten = (obj) => 
// Outer reduce concats arrays built by inner reduce
Object.keys(obj).reduce((accumulator, list) => 
// Inner reduce builds an array out of the lists
accumulator.concat(Object.keys(obj[list]).reduce((acc2, item) => 
// @ts-ignore
acc2.concat(obj[list][item]), [])), []);
const uniqify = (arr) => {
    const seen = new Set();
    return arr.filter(item => (seen.has(item.media.idMal) ? false : seen.add(item.media.idMal)));
};
// Anilist Functions
const anilistCall = (query, variables) => fetch('https://graphql.anilist.co', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
    },
    body: JSON.stringify({
        query,
        variables,
    }),
});
const fetchList = (userName) => anilistCall(`
      query ($userName: String) {
        anime: MediaListCollection(userName: $userName, type: ANIME) {
          lists {
            entries {
              status
              score(format:POINT_10)
              progress
              startedAt {
                year
                month
                day
              }
              completedAt {
                year
                month
                day
              }
              repeat
              media {
                idMal
                title {
                  romaji
                }
              }
            }
          }
        },
        manga: MediaListCollection(userName: $userName, type: MANGA) {
          lists {
            entries {
              status
              score(format:POINT_10)
              progress
              progressVolumes
              startedAt {
                year
                month
                day
              }
              completedAt {
                year
                month
                day
              }
              repeat
              media {
                idMal
                title {
                  romaji
                }
              }
            }
          }
        }
      }
    `, {
    userName
})
    .then(res => res.json())
    .then(res => res.data)
    .then(res => ({
    anime: uniqify(flatten(res.anime.lists)),
    manga: uniqify(flatten(res.manga.lists)),
}));
const sanitize = (item, type) => ({
    type,
    progress: item.progress,
    progressVolumes: item.progressVolumes,
    startedAt: {
        year: item.startedAt.year || 0,
        month: item.startedAt.month || 0,
        day: item.startedAt.day || 0,
    },
    completedAt: {
        year: item.completedAt.year || 0,
        month: item.completedAt.month || 0,
        day: item.completedAt.day || 0
    },
    repeat: item.repeat,
    status: item.status,
    score: item.score,
    id: item.media.idMal,
    title: item.media.title.romaji
});
const filterNoMalId = (item) => {
    if (item.id)
        return true;
    Log_1.default.error(`${item.type}: ${item.title}`);
    return false;
};
const getAnilistList = (username) => fetchList(username)
    .then(lists => ({
    anime: lists.anime
        .map(item => sanitize(item, 'anime'))
        .filter(item => filterNoMalId(item)),
    manga: lists.manga
        .map(item => sanitize(item, 'manga'))
        .filter(item => filterNoMalId(item)),
}));
exports.getAnilistList = getAnilistList;


/***/ }),
/* 6 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
const Util_1 = __webpack_require__(3);
const MALEntry_1 = __webpack_require__(7);
const Log_1 = __webpack_require__(1);
const Dom_1 = __webpack_require__(4);
class MAL {
    constructor(username, csrfToken, log = Log_1.default, dom = Dom_1.default) {
        this.username = username;
        this.csrfToken = csrfToken;
        this.Log = log;
        this.dom = dom;
    }
    createMALHashMap(malList, type) {
        const hashMap = {};
        malList.forEach(item => {
            hashMap[item[`${type}_id`]] = item;
        });
        return hashMap;
    }
    async getMALHashMap(type, list = [], page = 1) {
        const offset = (page - 1) * 300;
        const nextList = await fetch(`https://myanimelist.net/${type}list/${this.username}/load.json?offset=${offset}&status=7`)
            .then(async (res) => {
            if (res.status !== 200) {
                await Util_1.sleep(2000);
                return this.getMALHashMap(type, list, page);
            }
            return res.json();
        });
        if (nextList && nextList.length) {
            await Util_1.sleep(1500);
            return this.getMALHashMap(type, [...list, ...nextList], page + 1);
        }
        this.Log.info(`Fetched MyAnimeList ${type} list.`);
        return this.createMALHashMap([...list, ...nextList], type);
    }
    async getEntriesList(anilistList, type) {
        const malHashMap = await this.getMALHashMap(type);
        return anilistList.map(entry => MALEntry_1.createMALEntry(entry, malHashMap[entry.id], this.csrfToken, this.dom));
    }
    async malEdit(data) {
        const { type, id } = data;
        const formData = await data.formData();
        return fetch(`https://myanimelist.net/ownlist/${type}/${id}/edit?hideLayout`, {
            credentials: 'include',
            headers: {
                accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
                'accept-language': 'en-US,en;q=0.9,ja;q=0.8',
                'cache-control': 'max-age=0',
                'content-type': 'application/x-www-form-urlencoded',
                'upgrade-insecure-requests': '1'
            },
            referrer: `https://myanimelist.net/ownlist/${type}/${id}/edit?hideLayout`,
            referrerPolicy: 'no-referrer-when-downgrade',
            body: formData,
            method: 'POST',
            mode: 'cors'
        }).then((res) => {
            if (res.status === 200)
                return res;
            throw new Error(`Error updating ${type} id ${id}`);
        }).then((res) => res.text())
            .then((text) => {
            if (text.match(/.+Successfully updated entry.+/))
                return;
            throw new Error(`Error updating ${type} id ${id}`);
        });
    }
    malAdd(data) {
        return fetch(`https://myanimelist.net/ownlist/${data.type}/add.json`, {
            method: 'post',
            headers: {
                'Accept': '*/*',
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                'x-requested-with': 'XMLHttpRequest'
            },
            body: JSON.stringify(data.postData)
        })
            .then((res) => {
            if (res.status === 200)
                return res;
            throw new Error(JSON.stringify(data));
        });
    }
    async syncList(type, list, operation) {
        if (!list || !list.length) {
            return;
        }
        this.Log.addCountLog(operation, type, list.length);
        let itemCount = 0;
        const fn = operation === 'add' ? this.malAdd : this.malEdit;
        for (let item of list) {
            await Util_1.sleep(500);
            try {
                await fn(item);
                itemCount++;
                this.Log.updateCountLog(operation, type, itemCount);
            }
            catch (e) {
                console.error(e);
                this.Log.info(`Error for ${type} <a href="https://myanimelist.net/${type}/${item.id}" target="_blank" rel="noopener noreferrer">${item.title}</a>. Try adding or updating it manually.`);
            }
        }
    }
    async syncType(type, anilistList) {
        this.Log.info(`Fetching MyAnimeList ${type} list...`);
        let list = await this.getEntriesList(anilistList, type);
        const addList = list.filter(entry => entry.shouldAdd());
        await this.syncList(type, addList, 'add');
        // Refresh list to get episode/chapter counts of new completed items
        if (addList.length) {
            this.Log.info(`Refreshing MyAnimeList ${type} list...`);
            list = await this.getEntriesList(anilistList, type);
        }
        const updateList = list.filter(entry => entry.shouldUpdate());
        await this.syncList(type, updateList, 'edit');
    }
}
exports.default = MAL;


/***/ }),
/* 7 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.MALEntryManga = exports.MALEntryAnime = exports.BaseMALEntry = exports.createMALEntry = void 0;
const MALForm_1 = __webpack_require__(8);
const Dom_1 = __webpack_require__(4);
const Log_1 = __webpack_require__(1);
const createMALEntry = (al, mal, csrfToken, dom) => al.type === 'anime' ?
    new MALEntryAnime(al, mal, csrfToken, dom) :
    new MALEntryManga(al, mal, csrfToken, dom);
exports.createMALEntry = createMALEntry;
const MALStatus = {
    Current: 1,
    Completed: 2,
    Paused: 3,
    Dropped: 4,
    Planning: 6
};
const getStatus = (status) => {
    // MAL status: 1/watching, 2/completed, 3/onhold, 4/dropped, 6/plantowatch
    // MAL handles REPEATING as a boolean, and keeps status as COMPLETE
    switch (status.trim()) {
        case 'CURRENT':
            return MALStatus.Current;
        case 'REPEATING':
        case 'COMPLETED':
            return MALStatus.Completed;
        case 'PAUSED':
            return MALStatus.Paused;
        case 'DROPPED':
            return MALStatus.Dropped;
        case 'PLANNING':
            return MALStatus.Planning;
        default:
            throw new Error(`unknown status "${status}"`);
    }
};
const createMALFormData = (malData) => {
    let formData = '';
    Object.keys(malData).forEach(key => {
        formData += `${encodeURIComponent(key)}=${encodeURIComponent(malData[key])}&`;
    });
    return formData.replace(/&$/, '');
};
class BaseMALEntry {
    constructor(al, mal, csrfToken = '', dom = Dom_1.default, log = Log_1.default) {
        this.alData = al;
        this.malData = mal;
        this.csrfToken = csrfToken;
        this._postData = this.createPostData();
        this.dom = dom;
        this.log = log;
    }
    createBaseMALPostItem() {
        return {
            status: getStatus(this.alData.status),
            csrf_token: this.csrfToken,
            score: this.alData.score || 0,
            finish_date: {
                year: this.alData.completedAt.year || 0,
                month: this.alData.completedAt.month || 0,
                day: this.alData.completedAt.day || 0
            },
            start_date: {
                year: this.alData.startedAt.year || 0,
                month: this.alData.startedAt.month || 0,
                day: this.alData.startedAt.day || 0
            }
        };
    }
    buildDateString(date) {
        if (date.month === 0 && date.day === 0 && date.year === 0)
            return null;
        const dateSetting = this.dom.getDateSetting();
        const month = `${String(date.month).length < 2 ? '0' : ''}${date.month}`;
        const day = `${String(date.day).length < 2 ? '0' : ''}${date.day}`;
        const year = `${date.year ? String(date.year).slice(-2) : 0}`;
        if (dateSetting === 'a') {
            return `${month}-${day}-${year}`;
        }
        return `${day}-${month}-${year}`;
    }
    shouldUpdate() {
        // If something went wrong or it didn't get added, update will not work
        if (!this.malData || !this._postData) {
            return false;
        }
        const debug = this.dom.getDebugSetting();
        return Object.keys(this._postData).some(key => {
            switch (key) {
                case 'csrf_token':
                case 'anime_id':
                case 'manga_id':
                // This data is not part of the load.json list and so can't be used as update test
                case 'num_watched_times':
                case 'num_read_times':
                    return false;
                case 'start_date':
                case 'finish_date':
                    {
                        // @ts-ignore
                        const dateString = this.buildDateString(this._postData[key]);
                        if (dateString !== this.malData[`${key}_string`]) {
                            if (debug) {
                                this.log.debug(`${this.alData.title}: ${key} differs; MAL ${this.malData[`${key}_string`]} AL ${dateString}`);
                            }
                            return true;
                        }
                        return false;
                    }
                case 'num_read_chapters':
                case 'num_read_volumes':
                case 'num_watched_episodes':
                    // Anlist and MAL have different volume, episode, and chapter counts for some media;
                    // If the item is marked as completed, ignore differences (Status 2 is COMPLETED)
                    // EXCEPT when the count is 0, in which case this was newly added without a count and needs
                    // to be updated now that the count is available
                    {
                        if (this.malData.status === MALStatus.Completed && this.malData[key] !== 0) {
                            return false;
                        }
                        if (this._postData[key] !== this.malData[key]) {
                            if (debug) {
                                this.log.debug(`${this.alData.title} ${key} differs; MAL ${this.malData[key]} AL ${this._postData[key]}`);
                            }
                            return true;
                        }
                        return false;
                    }
                default:
                    {
                        // Treat falsy values as equivalent (!= doesn't do the trick here)
                        if (!this._postData[key] && !this.malData[key]) {
                            return false;
                        }
                        if (this._postData[key] !== this.malData[key]) {
                            if (debug) {
                                this.log.debug(`${this.alData.title} ${key} differs; MAL ${this.malData[key]} AL ${this._postData[key]}`);
                            }
                            return true;
                        }
                        return false;
                    }
            }
        });
    }
    shouldAdd() {
        return !this.malData;
    }
    formData() {
        throw new Error("Method not implemented.");
    }
    createPostData() {
        throw new Error("Method not implemented.");
    }
    get type() {
        return this.alData.type;
    }
    get id() {
        return this.alData.id;
    }
    get title() {
        return this.alData.title;
    }
    get postData() {
        return this._postData;
    }
}
exports.BaseMALEntry = BaseMALEntry;
class MALEntryAnime extends BaseMALEntry {
    constructor(al, mal, csrfToken = '', dom = Dom_1.default) {
        super(al, mal, csrfToken, dom);
    }
    createPostData() {
        const result = this.createBaseMALPostItem();
        result.anime_id = this.alData.id;
        if (this.alData.repeat)
            result.num_watched_times = this.alData.repeat;
        /* Setting num_watched_episodes */
        // If this is a new item, malData is undefined, so set count to 0
        // When the list refreshes the count will be available and be set then
        if (!this.malData) {
            result.num_watched_episodes = 0;
            return result;
        }
        // If malData.anime_num_episodes is 0, the show is currently airing;
        // We're forced to use AL's count even though that might be wrong
        if (this.malData.anime_num_episodes === 0) {
            result.num_watched_episodes = this.alData.progress;
            return result;
        }
        // If the show is completed, use MAL's count in case AL's count is different;
        // We don't want MAL showing higher or lower than their own count
        if (result.status === MALStatus.Completed) {
            result.num_watched_episodes = this.malData.anime_num_episodes;
            return result;
        }
        // Othewrise, use MAL's count as a max
        result.num_watched_episodes = Math.min(this.alData.progress, this.malData.anime_num_episodes);
        return result;
    }
    async formData() {
        const malFormData = new MALForm_1.MALForm(this.alData.type, this.alData.id);
        await malFormData.get();
        const formData = {
            anime_id: this.malData.anime_id,
            aeps: this.malData.anime_num_episodes || 0,
            astatus: this.malData.anime_airing_status,
            'add_anime[status]': this._postData.status,
            'add_anime[num_watched_episodes]': this._postData.num_watched_episodes || 0,
            'add_anime[score]': this._postData.score || '',
            'add_anime[start_date][month]': this._postData.start_date && this._postData.start_date.month || '',
            'add_anime[start_date][day]': this._postData.start_date && this._postData.start_date.day || '',
            'add_anime[start_date][year]': this._postData.start_date && this._postData.start_date.year || '',
            'add_anime[finish_date][month]': this._postData.finish_date && this._postData.finish_date.month || '',
            'add_anime[finish_date][day]': this._postData.finish_date && this._postData.finish_date.day || '',
            'add_anime[finish_date][year]': this._postData.finish_date && this._postData.finish_date.year || '',
            'add_anime[tags]': this.malData.tags || '',
            'add_anime[priority]': malFormData.priority,
            'add_anime[storage_type]': malFormData.storageType,
            'add_anime[storage_value]': malFormData.storageValue,
            'add_anime[num_watched_times]': this._postData.num_watched_times || 0,
            'add_anime[rewatch_value]': malFormData.rewatchValue,
            'add_anime[comments]': malFormData.comments,
            'add_anime[is_asked_to_discuss]': malFormData.discussionSetting,
            'add_anime[sns_post_type]': malFormData.SNSSetting,
            submitIt: 0,
            csrf_token: this.csrfToken,
        };
        if (this.alData.status === 'REPEATING') {
            formData['add_anime[is_rewatching]'] = 1;
        }
        return createMALFormData(formData);
    }
}
exports.MALEntryAnime = MALEntryAnime;
class MALEntryManga extends BaseMALEntry {
    constructor(al, mal, csrfToken = '', dom = Dom_1.default) {
        super(al, mal, csrfToken, dom);
    }
    createPostData() {
        const result = this.createBaseMALPostItem();
        result.manga_id = this.alData.id;
        if (this.alData.repeat)
            result.num_read_times = this.alData.repeat;
        /* Setting num_read_chapters and num_read_volumes */
        // If this is a new item, malData is undefined, so set count to 0
        // When the list refreshes the count will be available and be set then
        if (!this.malData) {
            result.num_read_chapters = 0;
            result.num_read_volumes = 0;
            return result;
        }
        // If malData.manga_num_chapters is 0, the manga is still publishing;
        // We're forced to use AL's count even though that might be wrong
        if (this.malData.manga_num_chapters === 0) {
            result.num_read_chapters = this.alData.progress;
            result.num_read_volumes = this.alData.progressVolumes;
            return result;
        }
        // If the manga is completed, use MAL's count in case AL's count is different;
        // We don't want MAL showing higher or lower than their own count
        if (result.status === MALStatus.Completed) {
            result.num_read_chapters = this.malData.manga_num_chapters;
            result.num_read_volumes = this.malData.manga_num_volumes;
            return result;
        }
        // Othewrise, use MAL's count as a max
        result.num_read_chapters = Math.min(this.alData.progress, this.malData.manga_num_chapters);
        result.num_read_volumes = Math.min(this.alData.progressVolumes, this.malData.manga_num_volumes);
        return result;
    }
    async formData() {
        const malFormData = new MALForm_1.MALForm(this.alData.type, this.alData.id);
        await malFormData.get();
        const formData = {
            entry_id: 0,
            manga_id: this.malData.manga_id,
            'add_manga[status]': this._postData.status,
            'add_manga[num_read_volumes]': this._postData.num_read_volumes || 0,
            last_completed_vol: '',
            'add_manga[num_read_chapters]': this._postData.num_read_chapters || 0,
            'add_manga[score]': this._postData.score || '',
            'add_manga[start_date][month]': this._postData.start_date && this._postData.start_date.month || '',
            'add_manga[start_date][day]': this._postData.start_date && this._postData.start_date.day || '',
            'add_manga[start_date][year]': this._postData.start_date && this._postData.start_date.year || '',
            'add_manga[finish_date][month]': this._postData.finish_date && this._postData.finish_date.month || '',
            'add_manga[finish_date][day]': this._postData.finish_date && this._postData.finish_date.day || '',
            'add_manga[finish_date][year]': this._postData.finish_date && this._postData.finish_date.year || '',
            'add_manga[tags]': this.malData.tags || '',
            'add_manga[priority]': malFormData.priority,
            'add_manga[storage_type]': malFormData.storageType,
            'add_manga[num_retail_volumes]': malFormData.numRetailVolumes,
            'add_manga[num_read_times]': this._postData.num_read_times || 0,
            'add_manga[reread_value]': malFormData.rereadValue,
            'add_manga[comments]': malFormData.comments,
            'add_manga[is_asked_to_discuss]': malFormData.discussionSetting,
            'add_manga[sns_post_type]': malFormData.SNSSetting,
            csrf_token: this.csrfToken,
            submitIt: 0
        };
        if (this.alData.status === 'REPEATING') {
            formData['add_manga[is_rewatching]'] = 1;
        }
        return createMALFormData(formData);
    }
}
exports.MALEntryManga = MALEntryManga;


/***/ }),
/* 8 */
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.MALForm = void 0;
const Util_1 = __webpack_require__(3);
class MALForm {
    constructor(type, id) {
        this.document = null;
        this.type = type;
        this.id = id;
    }
    fetchDocument(type, id) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.onload = function () {
                return resolve(this.responseXML ? this.responseXML : null);
            };
            xhr.onerror = function (e) {
                reject(e);
            };
            xhr.open('GET', `https://myanimelist.net/ownlist/${type}/${id}/edit`);
            xhr.responseType = 'document';
            xhr.send();
        });
    }
    getElement(id) {
        if (!this.document)
            throw new Error('Document not loaded');
        return this.document.querySelector(`#add_${this.type}_${id}`);
    }
    async get() {
        await Util_1.sleep(500);
        const document = await this.fetchDocument(this.type, this.id);
        if (document) {
            this.document = document;
        }
        else {
            throw new Error('Unable to fetch form data');
        }
    }
    get priority() {
        const el = this.getElement('priority');
        if (!el)
            throw new Error('Unable to get priority');
        return el.value;
    }
    get storageType() {
        const el = this.getElement('storage_type');
        if (!el)
            throw new Error('Unable to get storage type');
        return el.value;
    }
    get storageValue() {
        const el = this.getElement('storage_value');
        if (!el)
            return '0';
        return el.value;
    }
    get numRetailVolumes() {
        const el = this.getElement('num_retail_volumes');
        if (!el)
            return '0';
        return el.value;
    }
    get rewatchValue() {
        const el = this.getElement('rewatch_value');
        if (!el)
            throw new Error('Unable to get rewatch value');
        return el.value;
    }
    get rereadValue() {
        const el = this.getElement('reread_value');
        if (!el)
            throw new Error('Unable to get reread value');
        return el.value;
    }
    get comments() {
        const el = this.getElement('comments');
        if (!el)
            throw new Error('Unable to get comments');
        return el.value;
    }
    get discussionSetting() {
        const el = this.getElement('is_asked_to_discuss');
        if (!el)
            throw new Error('Unable to get discussion value');
        return el.value;
    }
    get SNSSetting() {
        const el = this.getElement('sns_post_type');
        if (!el)
            throw new Error('Unable to get SNS setting');
        return el.value;
    }
}
exports.MALForm = MALForm;


/***/ })
/******/ 	]);
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
var exports = __webpack_exports__;

Object.defineProperty(exports, "__esModule", ({ value: true }));
const Log_1 = __webpack_require__(1);
const Dom_1 = __webpack_require__(4);
const Anilist_1 = __webpack_require__(5);
const MAL_1 = __webpack_require__(6);
// Main business logic
const sync = async (e) => {
    e.preventDefault();
    const anilistUsername = Dom_1.default.getAnilistUsername();
    if (!anilistUsername)
        return;
    const malUsername = Dom_1.default.getMALUsername();
    if (!malUsername) {
        Log_1.default.info('You must be logged in!');
        return;
    }
    const csrfToken = Dom_1.default.getCSRFToken();
    Log_1.default.clear();
    Log_1.default.info(`Fetching data from Anilist...`);
    const anilistList = await Anilist_1.getAnilistList(anilistUsername);
    if (!anilistList) {
        Log_1.default.info(`No data found for user ${anilistUsername}.`);
        return;
    }
    Log_1.default.info(`Fetched Anilist data.`);
    const mal = new MAL_1.default(malUsername, csrfToken);
    await mal.syncType('anime', anilistList.anime);
    await mal.syncType('manga', anilistList.manga);
    Log_1.default.info('Import complete.');
};
// Entrypoint
(() => {
    'use strict';
    Dom_1.default.addDropDownItem();
    if (window.location.pathname === '/import.php') {
        Dom_1.default.addImportForm(sync);
    }
})();

})();

/******/ })()
;