您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); } })(); })(); /******/ })() ;
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址