Douki

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

  1. // ==UserScript==
  2. // @name Douki
  3. // @namespace douki-e7d98778-9b83-45eb-a189-456bd1ce2ee1
  4. // @description Import Anime and Manga Lists from Anilist (see https://anilist.co/forum/thread/2654 for more info)
  5. // @version 0.2.5
  6. // @include https://myanimelist.net/*
  7. // ==/UserScript==
  8.  
  9. /******/ (() => { // webpackBootstrap
  10. /******/ "use strict";
  11. /******/ var __webpack_modules__ = ([
  12. /* 0 */,
  13. /* 1 */
  14. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  15.  
  16.  
  17. Object.defineProperty(exports, "__esModule", ({ value: true }));
  18. exports.Log = void 0;
  19. const const_1 = __webpack_require__(2);
  20. const Util_1 = __webpack_require__(3);
  21. const getCountLog = (operation, type) => document.querySelector(Util_1.id(`douki-${operation}-${type}-items`));
  22. class Log {
  23. constructor() {
  24. this.errorLogElement = null;
  25. this.syncLogElement = null;
  26. this.debugLogElement = null;
  27. }
  28. get errorLog() {
  29. if (!this.errorLogElement) {
  30. this.errorLogElement = document.querySelector(Util_1.id(const_1.ERROR_LOG_ID));
  31. }
  32. return this.errorLogElement;
  33. }
  34. get syncLog() {
  35. if (!this.syncLogElement) {
  36. this.syncLogElement = document.querySelector(Util_1.id(const_1.SYNC_LOG_ID));
  37. }
  38. return this.syncLogElement;
  39. }
  40. get debugLog() {
  41. if (!this.debugLogElement) {
  42. this.debugLogElement = document.querySelector(Util_1.id(const_1.DEBUG_LOG_ID));
  43. }
  44. return this.debugLogElement;
  45. }
  46. clearErrorLog() {
  47. if (this.errorLog) {
  48. this.errorLog.innerHTML = '';
  49. }
  50. }
  51. clearSyncLog() {
  52. if (this.syncLog) {
  53. this.syncLog.innerHTML = '';
  54. }
  55. }
  56. clearDebugLog() {
  57. if (this.debugLog) {
  58. this.debugLog.innerHTML = '';
  59. }
  60. }
  61. clear(type = '') {
  62. console.clear();
  63. if (type !== 'error')
  64. this.clearSyncLog();
  65. if (type !== 'sync')
  66. this.clearErrorLog();
  67. this.clearDebugLog();
  68. }
  69. error(msg) {
  70. if (this.errorLog) {
  71. this.errorLog.innerHTML += `<li>${msg}</li>`;
  72. }
  73. else {
  74. console.error(msg);
  75. }
  76. }
  77. info(msg) {
  78. if (this.syncLog) {
  79. this.syncLog.innerHTML += `<li>${msg}</li>`;
  80. }
  81. else {
  82. console.info(msg);
  83. }
  84. }
  85. debug(msg) {
  86. if (this.debugLog) {
  87. this.debugLog.innerHTML += `<li>${msg}</li>`;
  88. }
  89. else {
  90. console.debug(msg);
  91. }
  92. }
  93. addCountLog(operation, type, max) {
  94. const opName = Util_1.getOperationDisplayName(operation);
  95. const logId = `douki-${operation}-${type}-items`;
  96. this.info(`${opName} <span id="${logId}">0</span> of ${max} ${type} items.`);
  97. }
  98. updateCountLog(operation, type, count) {
  99. const countLog = getCountLog(operation, type);
  100. if (!countLog)
  101. return;
  102. countLog.innerHTML = `${count}`;
  103. }
  104. }
  105. exports.Log = Log;
  106. exports.default = new Log();
  107.  
  108.  
  109. /***/ }),
  110. /* 2 */
  111. /***/ ((__unused_webpack_module, exports) => {
  112.  
  113.  
  114. Object.defineProperty(exports, "__esModule", ({ value: true }));
  115. 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;
  116. exports.DOUKI_FORM_ID = 'douki-form';
  117. exports.DOUKI_ANILIST_IMPORT_ID = 'douki-anilist-import';
  118. exports.DATE_SETTING_ID = 'douki-date_format';
  119. exports.CONTENT_ID = 'content';
  120. exports.DOUKI_IMPORT_BUTTON_ID = 'douki-import';
  121. exports.SYNC_LOG_ID = 'douki-sync-log';
  122. exports.ERROR_LOG_ID = 'douki-error-log';
  123. exports.ERROR_LOG_TOGGLE_ID = 'douki-error-log-toggle';
  124. exports.ERROR_LOG_DIV_ID = 'douki-error-log-div';
  125. exports.ANILIST_USERNAME_ID = 'douki-anilist-username';
  126. exports.SETTINGS_KEY = 'douki-settings';
  127. exports.DATE_SETTINGS_KEY = 'douki-settings-date';
  128. exports.DROPDOWN_ITEM_ID = 'douki-sync';
  129. exports.DEBUG_SETTING_ID = 'douki-debug';
  130. exports.DEBUG_LOG_ID = 'douki-debug-log';
  131.  
  132.  
  133. /***/ }),
  134. /* 3 */
  135. /***/ ((__unused_webpack_module, exports) => {
  136.  
  137.  
  138. Object.defineProperty(exports, "__esModule", ({ value: true }));
  139. exports.getOperationDisplayName = exports.id = exports.sleep = void 0;
  140. const sleep = (ms) => new Promise(resolve => setTimeout(() => resolve(null), ms));
  141. exports.sleep = sleep;
  142. const id = (str) => `#${str}`;
  143. exports.id = id;
  144. const getOperationDisplayName = (operation) => {
  145. switch (operation) {
  146. case 'add':
  147. return 'Adding';
  148. case 'edit':
  149. return 'Updating';
  150. case 'complete':
  151. return 'Fixing';
  152. default:
  153. throw new Error('Unknown operation type');
  154. }
  155. };
  156. exports.getOperationDisplayName = getOperationDisplayName;
  157.  
  158.  
  159. /***/ }),
  160. /* 4 */
  161. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  162.  
  163.  
  164. Object.defineProperty(exports, "__esModule", ({ value: true }));
  165. exports.DomMethods = void 0;
  166. const const_1 = __webpack_require__(2);
  167. const Util_1 = __webpack_require__(3);
  168. const importFormHTML = `
  169. <div id="${const_1.DOUKI_FORM_ID}">
  170. <h1 class="h1">Import From Anilist</h1>
  171. <div style="padding: 20px">
  172. <p><strong>NOTICE</strong>: Use this script at your own risk. The author takes no responsibility for any damages of any kind.</p>
  173. <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>
  174. <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>
  175. <p>Please be patient. If the import goes any faster you will be in violation of MyAnimeList's Terms of Service.</p>
  176. </div>
  177. <form id="${const_1.DOUKI_ANILIST_IMPORT_ID}" style="padding: 5px 0px 10px 0px">
  178. <p style="margin: 10px"><label>Anilist Username: <input type="text" id="${const_1.ANILIST_USERNAME_ID}" /></label></p>
  179. <p style="margin: 10px">
  180. <label>Date Format:
  181. <select id="${const_1.DATE_SETTING_ID}" class="inputtext">
  182. <option value="a" selected>American (MM-DD-YY)
  183. <option value="e" >European (DD-MM-YY)
  184. </select>
  185. </label>
  186. <label>Debug Mode:
  187. <input id="${const_1.DEBUG_SETTING_ID}" type="checkbox" name="debug">
  188. </label>
  189. </p>
  190. <p style="margin: 10px"><button id="${const_1.DOUKI_IMPORT_BUTTON_ID}">Import</button></p>
  191. </form>
  192. <br />
  193. <ul id="${const_1.SYNC_LOG_ID}" style="list-type: none;"></ul>
  194. <p style="margin: 10px"><button id="${const_1.ERROR_LOG_TOGGLE_ID}" style="border: none">Show items that could not be synced</button></p>
  195. <div id="${const_1.ERROR_LOG_DIV_ID}" style="display: none;">
  196. <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>
  197. <ul id="${const_1.ERROR_LOG_ID}" style="list-type: none;"></ul>
  198. </div>
  199. <div>
  200. <ul id="${const_1.DEBUG_LOG_ID}" style="list-type: none;"></ul>
  201. </div>
  202. </div>
  203. `;
  204. const getLocalStorageSetting = (setting) => {
  205. if (localStorage) {
  206. const value = localStorage.getItem(setting);
  207. if (value)
  208. return JSON.parse(value);
  209. }
  210. return null;
  211. };
  212. const setLocalStorageSetting = (setting, value) => {
  213. if (localStorage) {
  214. localStorage.setItem(setting, JSON.stringify(value));
  215. }
  216. };
  217. class DomMethods {
  218. constructor() {
  219. this.csrfToken = null;
  220. }
  221. addDropDownItem() {
  222. if (document.querySelector(Util_1.id(const_1.DROPDOWN_ITEM_ID)))
  223. return;
  224. const selector = '.header-menu-dropdown > ul > li:last-child';
  225. const dropdown = document.querySelector(selector);
  226. if (dropdown) {
  227. const html = `<li><a aria-role="button" style="cursor: pointer" id="${const_1.DROPDOWN_ITEM_ID}">Import from Anilist</a></li>`;
  228. dropdown.insertAdjacentHTML('afterend', html);
  229. const link = document.querySelector(Util_1.id(const_1.DROPDOWN_ITEM_ID));
  230. link && link.addEventListener('click', function (e) {
  231. e.preventDefault();
  232. window.location.replace('https://myanimelist.net/import.php');
  233. });
  234. }
  235. }
  236. addImportForm(syncFn) {
  237. if (document.querySelector(Util_1.id(const_1.DOUKI_FORM_ID)))
  238. return;
  239. const element = document.querySelector(Util_1.id(const_1.CONTENT_ID));
  240. if (!element) {
  241. throw new Error('Unable to add form to page');
  242. }
  243. element.insertAdjacentHTML('afterend', importFormHTML);
  244. this.addImportFormEventListeners(syncFn);
  245. }
  246. // TODO break this up
  247. addImportFormEventListeners(syncFn) {
  248. const importButton = document.querySelector(Util_1.id(const_1.DOUKI_IMPORT_BUTTON_ID));
  249. importButton && importButton.addEventListener('click', function (e) {
  250. syncFn(e);
  251. });
  252. const textBox = document.querySelector(Util_1.id(const_1.ANILIST_USERNAME_ID));
  253. textBox && textBox.addEventListener('change', function (e) {
  254. setLocalStorageSetting(const_1.SETTINGS_KEY, e.target.value);
  255. });
  256. const username = getLocalStorageSetting(const_1.SETTINGS_KEY);
  257. if (username && textBox) {
  258. textBox.value = username;
  259. }
  260. const dateFormatPicker = document.querySelector(Util_1.id(const_1.DATE_SETTING_ID));
  261. dateFormatPicker && dateFormatPicker.addEventListener('change', function (e) {
  262. setLocalStorageSetting(const_1.DATE_SETTINGS_KEY, e.target.value);
  263. });
  264. const dateOption = getLocalStorageSetting(const_1.DATE_SETTINGS_KEY);
  265. if (dateOption && dateFormatPicker) {
  266. dateFormatPicker.value = dateOption;
  267. }
  268. const errorToggle = document.querySelector(Util_1.id(const_1.ERROR_LOG_TOGGLE_ID));
  269. errorToggle && errorToggle.addEventListener('click', function (e) {
  270. e.preventDefault();
  271. const errorLog = document.querySelector(Util_1.id(const_1.ERROR_LOG_DIV_ID));
  272. if (errorLog.style.display === 'none') {
  273. errorLog.style.display = 'block';
  274. }
  275. else {
  276. errorLog.style.display = 'none';
  277. }
  278. });
  279. }
  280. getDateSetting() {
  281. const dateSetting = document.querySelector(Util_1.id(const_1.DATE_SETTING_ID));
  282. if (!dateSetting || !dateSetting.value)
  283. throw new Error('Unable to get date setting');
  284. return dateSetting.value;
  285. }
  286. getDebugSetting() {
  287. const debugSetting = document.querySelector(Util_1.id(const_1.DEBUG_SETTING_ID));
  288. if (!debugSetting)
  289. throw new Error('Unable to get debug setting');
  290. return debugSetting.checked;
  291. }
  292. getCSRFToken() {
  293. if (this.csrfToken)
  294. return this.csrfToken;
  295. const csrfTokenMeta = document.querySelector('meta[name~="csrf_token"]');
  296. if (!csrfTokenMeta)
  297. throw new Error('Unable to get CSRF token - no meta element');
  298. const csrfToken = csrfTokenMeta.getAttribute('content');
  299. if (!csrfToken)
  300. throw new Error('Unable to get CSRF token - no content attribute');
  301. this.csrfToken = csrfToken;
  302. return csrfToken;
  303. }
  304. getMALUsername() {
  305. const malUsernameElement = document.querySelector('.header-profile-link');
  306. if (!malUsernameElement)
  307. return null;
  308. return malUsernameElement.innerText;
  309. }
  310. getAnilistUsername() {
  311. const anilistUserElement = document.querySelector('#douki-anilist-username');
  312. if (!anilistUserElement)
  313. throw new Error('Unable to get Anilist username');
  314. return anilistUserElement.value;
  315. }
  316. }
  317. exports.DomMethods = DomMethods;
  318. exports.default = new DomMethods();
  319.  
  320.  
  321. /***/ }),
  322. /* 5 */
  323. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  324.  
  325.  
  326. Object.defineProperty(exports, "__esModule", ({ value: true }));
  327. exports.getAnilistList = void 0;
  328. const Log_1 = __webpack_require__(1);
  329. const flatten = (obj) =>
  330. // Outer reduce concats arrays built by inner reduce
  331. Object.keys(obj).reduce((accumulator, list) =>
  332. // Inner reduce builds an array out of the lists
  333. accumulator.concat(Object.keys(obj[list]).reduce((acc2, item) =>
  334. // @ts-ignore
  335. acc2.concat(obj[list][item]), [])), []);
  336. const uniqify = (arr) => {
  337. const seen = new Set();
  338. return arr.filter(item => (seen.has(item.media.idMal) ? false : seen.add(item.media.idMal)));
  339. };
  340. // Anilist Functions
  341. const anilistCall = (query, variables) => fetch('https://graphql.anilist.co', {
  342. method: 'POST',
  343. headers: {
  344. 'Content-Type': 'application/json',
  345. Accept: 'application/json',
  346. },
  347. body: JSON.stringify({
  348. query,
  349. variables,
  350. }),
  351. });
  352. const fetchList = (userName) => anilistCall(`
  353. query ($userName: String) {
  354. anime: MediaListCollection(userName: $userName, type: ANIME) {
  355. lists {
  356. entries {
  357. status
  358. score(format:POINT_10)
  359. progress
  360. startedAt {
  361. year
  362. month
  363. day
  364. }
  365. completedAt {
  366. year
  367. month
  368. day
  369. }
  370. repeat
  371. media {
  372. idMal
  373. title {
  374. romaji
  375. }
  376. }
  377. }
  378. }
  379. },
  380. manga: MediaListCollection(userName: $userName, type: MANGA) {
  381. lists {
  382. entries {
  383. status
  384. score(format:POINT_10)
  385. progress
  386. progressVolumes
  387. startedAt {
  388. year
  389. month
  390. day
  391. }
  392. completedAt {
  393. year
  394. month
  395. day
  396. }
  397. repeat
  398. media {
  399. idMal
  400. title {
  401. romaji
  402. }
  403. }
  404. }
  405. }
  406. }
  407. }
  408. `, {
  409. userName
  410. })
  411. .then(res => res.json())
  412. .then(res => res.data)
  413. .then(res => ({
  414. anime: uniqify(flatten(res.anime.lists)),
  415. manga: uniqify(flatten(res.manga.lists)),
  416. }));
  417. const sanitize = (item, type) => ({
  418. type,
  419. progress: item.progress,
  420. progressVolumes: item.progressVolumes,
  421. startedAt: {
  422. year: item.startedAt.year || 0,
  423. month: item.startedAt.month || 0,
  424. day: item.startedAt.day || 0,
  425. },
  426. completedAt: {
  427. year: item.completedAt.year || 0,
  428. month: item.completedAt.month || 0,
  429. day: item.completedAt.day || 0
  430. },
  431. repeat: item.repeat,
  432. status: item.status,
  433. score: item.score,
  434. id: item.media.idMal,
  435. title: item.media.title.romaji
  436. });
  437. const filterNoMalId = (item) => {
  438. if (item.id)
  439. return true;
  440. Log_1.default.error(`${item.type}: ${item.title}`);
  441. return false;
  442. };
  443. const getAnilistList = (username) => fetchList(username)
  444. .then(lists => ({
  445. anime: lists.anime
  446. .map(item => sanitize(item, 'anime'))
  447. .filter(item => filterNoMalId(item)),
  448. manga: lists.manga
  449. .map(item => sanitize(item, 'manga'))
  450. .filter(item => filterNoMalId(item)),
  451. }));
  452. exports.getAnilistList = getAnilistList;
  453.  
  454.  
  455. /***/ }),
  456. /* 6 */
  457. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  458.  
  459.  
  460. Object.defineProperty(exports, "__esModule", ({ value: true }));
  461. const Util_1 = __webpack_require__(3);
  462. const MALEntry_1 = __webpack_require__(7);
  463. const Log_1 = __webpack_require__(1);
  464. const Dom_1 = __webpack_require__(4);
  465. class MAL {
  466. constructor(username, csrfToken, log = Log_1.default, dom = Dom_1.default) {
  467. this.username = username;
  468. this.csrfToken = csrfToken;
  469. this.Log = log;
  470. this.dom = dom;
  471. }
  472. createMALHashMap(malList, type) {
  473. const hashMap = {};
  474. malList.forEach(item => {
  475. hashMap[item[`${type}_id`]] = item;
  476. });
  477. return hashMap;
  478. }
  479. async getMALHashMap(type, list = [], page = 1) {
  480. const offset = (page - 1) * 300;
  481. const nextList = await fetch(`https://myanimelist.net/${type}list/${this.username}/load.json?offset=${offset}&status=7`)
  482. .then(async (res) => {
  483. if (res.status !== 200) {
  484. await Util_1.sleep(2000);
  485. return this.getMALHashMap(type, list, page);
  486. }
  487. return res.json();
  488. });
  489. if (nextList && nextList.length) {
  490. await Util_1.sleep(1500);
  491. return this.getMALHashMap(type, [...list, ...nextList], page + 1);
  492. }
  493. this.Log.info(`Fetched MyAnimeList ${type} list.`);
  494. return this.createMALHashMap([...list, ...nextList], type);
  495. }
  496. async getEntriesList(anilistList, type) {
  497. const malHashMap = await this.getMALHashMap(type);
  498. return anilistList.map(entry => MALEntry_1.createMALEntry(entry, malHashMap[entry.id], this.csrfToken, this.dom));
  499. }
  500. async malEdit(data) {
  501. const { type, id } = data;
  502. const formData = await data.formData();
  503. return fetch(`https://myanimelist.net/ownlist/${type}/${id}/edit?hideLayout`, {
  504. credentials: 'include',
  505. headers: {
  506. accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
  507. 'accept-language': 'en-US,en;q=0.9,ja;q=0.8',
  508. 'cache-control': 'max-age=0',
  509. 'content-type': 'application/x-www-form-urlencoded',
  510. 'upgrade-insecure-requests': '1'
  511. },
  512. referrer: `https://myanimelist.net/ownlist/${type}/${id}/edit?hideLayout`,
  513. referrerPolicy: 'no-referrer-when-downgrade',
  514. body: formData,
  515. method: 'POST',
  516. mode: 'cors'
  517. }).then((res) => {
  518. if (res.status === 200)
  519. return res;
  520. throw new Error(`Error updating ${type} id ${id}`);
  521. }).then((res) => res.text())
  522. .then((text) => {
  523. if (text.match(/.+Successfully updated entry.+/))
  524. return;
  525. throw new Error(`Error updating ${type} id ${id}`);
  526. });
  527. }
  528. malAdd(data) {
  529. return fetch(`https://myanimelist.net/ownlist/${data.type}/add.json`, {
  530. method: 'post',
  531. headers: {
  532. 'Accept': '*/*',
  533. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  534. 'x-requested-with': 'XMLHttpRequest'
  535. },
  536. body: JSON.stringify(data.postData)
  537. })
  538. .then((res) => {
  539. if (res.status === 200)
  540. return res;
  541. throw new Error(JSON.stringify(data));
  542. });
  543. }
  544. async syncList(type, list, operation) {
  545. if (!list || !list.length) {
  546. return;
  547. }
  548. this.Log.addCountLog(operation, type, list.length);
  549. let itemCount = 0;
  550. const fn = operation === 'add' ? this.malAdd : this.malEdit;
  551. for (let item of list) {
  552. await Util_1.sleep(500);
  553. try {
  554. await fn(item);
  555. itemCount++;
  556. this.Log.updateCountLog(operation, type, itemCount);
  557. }
  558. catch (e) {
  559. console.error(e);
  560. 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.`);
  561. }
  562. }
  563. }
  564. async syncType(type, anilistList) {
  565. this.Log.info(`Fetching MyAnimeList ${type} list...`);
  566. let list = await this.getEntriesList(anilistList, type);
  567. const addList = list.filter(entry => entry.shouldAdd());
  568. await this.syncList(type, addList, 'add');
  569. // Refresh list to get episode/chapter counts of new completed items
  570. if (addList.length) {
  571. this.Log.info(`Refreshing MyAnimeList ${type} list...`);
  572. list = await this.getEntriesList(anilistList, type);
  573. }
  574. const updateList = list.filter(entry => entry.shouldUpdate());
  575. await this.syncList(type, updateList, 'edit');
  576. }
  577. }
  578. exports.default = MAL;
  579.  
  580.  
  581. /***/ }),
  582. /* 7 */
  583. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  584.  
  585.  
  586. Object.defineProperty(exports, "__esModule", ({ value: true }));
  587. exports.MALEntryManga = exports.MALEntryAnime = exports.BaseMALEntry = exports.createMALEntry = void 0;
  588. const MALForm_1 = __webpack_require__(8);
  589. const Dom_1 = __webpack_require__(4);
  590. const Log_1 = __webpack_require__(1);
  591. const createMALEntry = (al, mal, csrfToken, dom) => al.type === 'anime' ?
  592. new MALEntryAnime(al, mal, csrfToken, dom) :
  593. new MALEntryManga(al, mal, csrfToken, dom);
  594. exports.createMALEntry = createMALEntry;
  595. const MALStatus = {
  596. Current: 1,
  597. Completed: 2,
  598. Paused: 3,
  599. Dropped: 4,
  600. Planning: 6
  601. };
  602. const getStatus = (status) => {
  603. // MAL status: 1/watching, 2/completed, 3/onhold, 4/dropped, 6/plantowatch
  604. // MAL handles REPEATING as a boolean, and keeps status as COMPLETE
  605. switch (status.trim()) {
  606. case 'CURRENT':
  607. return MALStatus.Current;
  608. case 'REPEATING':
  609. case 'COMPLETED':
  610. return MALStatus.Completed;
  611. case 'PAUSED':
  612. return MALStatus.Paused;
  613. case 'DROPPED':
  614. return MALStatus.Dropped;
  615. case 'PLANNING':
  616. return MALStatus.Planning;
  617. default:
  618. throw new Error(`unknown status "${status}"`);
  619. }
  620. };
  621. const createMALFormData = (malData) => {
  622. let formData = '';
  623. Object.keys(malData).forEach(key => {
  624. formData += `${encodeURIComponent(key)}=${encodeURIComponent(malData[key])}&`;
  625. });
  626. return formData.replace(/&$/, '');
  627. };
  628. class BaseMALEntry {
  629. constructor(al, mal, csrfToken = '', dom = Dom_1.default, log = Log_1.default) {
  630. this.alData = al;
  631. this.malData = mal;
  632. this.csrfToken = csrfToken;
  633. this._postData = this.createPostData();
  634. this.dom = dom;
  635. this.log = log;
  636. }
  637. createBaseMALPostItem() {
  638. return {
  639. status: getStatus(this.alData.status),
  640. csrf_token: this.csrfToken,
  641. score: this.alData.score || 0,
  642. finish_date: {
  643. year: this.alData.completedAt.year || 0,
  644. month: this.alData.completedAt.month || 0,
  645. day: this.alData.completedAt.day || 0
  646. },
  647. start_date: {
  648. year: this.alData.startedAt.year || 0,
  649. month: this.alData.startedAt.month || 0,
  650. day: this.alData.startedAt.day || 0
  651. }
  652. };
  653. }
  654. buildDateString(date) {
  655. if (date.month === 0 && date.day === 0 && date.year === 0)
  656. return null;
  657. const dateSetting = this.dom.getDateSetting();
  658. const month = `${String(date.month).length < 2 ? '0' : ''}${date.month}`;
  659. const day = `${String(date.day).length < 2 ? '0' : ''}${date.day}`;
  660. const year = `${date.year ? String(date.year).slice(-2) : 0}`;
  661. if (dateSetting === 'a') {
  662. return `${month}-${day}-${year}`;
  663. }
  664. return `${day}-${month}-${year}`;
  665. }
  666. shouldUpdate() {
  667. // If something went wrong or it didn't get added, update will not work
  668. if (!this.malData || !this._postData) {
  669. return false;
  670. }
  671. const debug = this.dom.getDebugSetting();
  672. return Object.keys(this._postData).some(key => {
  673. switch (key) {
  674. case 'csrf_token':
  675. case 'anime_id':
  676. case 'manga_id':
  677. // This data is not part of the load.json list and so can't be used as update test
  678. case 'num_watched_times':
  679. case 'num_read_times':
  680. return false;
  681. case 'start_date':
  682. case 'finish_date':
  683. {
  684. // @ts-ignore
  685. const dateString = this.buildDateString(this._postData[key]);
  686. if (dateString !== this.malData[`${key}_string`]) {
  687. if (debug) {
  688. this.log.debug(`${this.alData.title}: ${key} differs; MAL ${this.malData[`${key}_string`]} AL ${dateString}`);
  689. }
  690. return true;
  691. }
  692. return false;
  693. }
  694. case 'num_read_chapters':
  695. case 'num_read_volumes':
  696. case 'num_watched_episodes':
  697. // Anlist and MAL have different volume, episode, and chapter counts for some media;
  698. // If the item is marked as completed, ignore differences (Status 2 is COMPLETED)
  699. // EXCEPT when the count is 0, in which case this was newly added without a count and needs
  700. // to be updated now that the count is available
  701. {
  702. if (this.malData.status === MALStatus.Completed && this.malData[key] !== 0) {
  703. return false;
  704. }
  705. if (this._postData[key] !== this.malData[key]) {
  706. if (debug) {
  707. this.log.debug(`${this.alData.title} ${key} differs; MAL ${this.malData[key]} AL ${this._postData[key]}`);
  708. }
  709. return true;
  710. }
  711. return false;
  712. }
  713. default:
  714. {
  715. // Treat falsy values as equivalent (!= doesn't do the trick here)
  716. if (!this._postData[key] && !this.malData[key]) {
  717. return false;
  718. }
  719. if (this._postData[key] !== this.malData[key]) {
  720. if (debug) {
  721. this.log.debug(`${this.alData.title} ${key} differs; MAL ${this.malData[key]} AL ${this._postData[key]}`);
  722. }
  723. return true;
  724. }
  725. return false;
  726. }
  727. }
  728. });
  729. }
  730. shouldAdd() {
  731. return !this.malData;
  732. }
  733. formData() {
  734. throw new Error("Method not implemented.");
  735. }
  736. createPostData() {
  737. throw new Error("Method not implemented.");
  738. }
  739. get type() {
  740. return this.alData.type;
  741. }
  742. get id() {
  743. return this.alData.id;
  744. }
  745. get title() {
  746. return this.alData.title;
  747. }
  748. get postData() {
  749. return this._postData;
  750. }
  751. }
  752. exports.BaseMALEntry = BaseMALEntry;
  753. class MALEntryAnime extends BaseMALEntry {
  754. constructor(al, mal, csrfToken = '', dom = Dom_1.default) {
  755. super(al, mal, csrfToken, dom);
  756. }
  757. createPostData() {
  758. const result = this.createBaseMALPostItem();
  759. result.anime_id = this.alData.id;
  760. if (this.alData.repeat)
  761. result.num_watched_times = this.alData.repeat;
  762. /* Setting num_watched_episodes */
  763. // If this is a new item, malData is undefined, so set count to 0
  764. // When the list refreshes the count will be available and be set then
  765. if (!this.malData) {
  766. result.num_watched_episodes = 0;
  767. return result;
  768. }
  769. // If malData.anime_num_episodes is 0, the show is currently airing;
  770. // We're forced to use AL's count even though that might be wrong
  771. if (this.malData.anime_num_episodes === 0) {
  772. result.num_watched_episodes = this.alData.progress;
  773. return result;
  774. }
  775. // If the show is completed, use MAL's count in case AL's count is different;
  776. // We don't want MAL showing higher or lower than their own count
  777. if (result.status === MALStatus.Completed) {
  778. result.num_watched_episodes = this.malData.anime_num_episodes;
  779. return result;
  780. }
  781. // Othewrise, use MAL's count as a max
  782. result.num_watched_episodes = Math.min(this.alData.progress, this.malData.anime_num_episodes);
  783. return result;
  784. }
  785. async formData() {
  786. const malFormData = new MALForm_1.MALForm(this.alData.type, this.alData.id);
  787. await malFormData.get();
  788. const formData = {
  789. anime_id: this.malData.anime_id,
  790. aeps: this.malData.anime_num_episodes || 0,
  791. astatus: this.malData.anime_airing_status,
  792. 'add_anime[status]': this._postData.status,
  793. 'add_anime[num_watched_episodes]': this._postData.num_watched_episodes || 0,
  794. 'add_anime[score]': this._postData.score || '',
  795. 'add_anime[start_date][month]': this._postData.start_date && this._postData.start_date.month || '',
  796. 'add_anime[start_date][day]': this._postData.start_date && this._postData.start_date.day || '',
  797. 'add_anime[start_date][year]': this._postData.start_date && this._postData.start_date.year || '',
  798. 'add_anime[finish_date][month]': this._postData.finish_date && this._postData.finish_date.month || '',
  799. 'add_anime[finish_date][day]': this._postData.finish_date && this._postData.finish_date.day || '',
  800. 'add_anime[finish_date][year]': this._postData.finish_date && this._postData.finish_date.year || '',
  801. 'add_anime[tags]': this.malData.tags || '',
  802. 'add_anime[priority]': malFormData.priority,
  803. 'add_anime[storage_type]': malFormData.storageType,
  804. 'add_anime[storage_value]': malFormData.storageValue,
  805. 'add_anime[num_watched_times]': this._postData.num_watched_times || 0,
  806. 'add_anime[rewatch_value]': malFormData.rewatchValue,
  807. 'add_anime[comments]': malFormData.comments,
  808. 'add_anime[is_asked_to_discuss]': malFormData.discussionSetting,
  809. 'add_anime[sns_post_type]': malFormData.SNSSetting,
  810. submitIt: 0,
  811. csrf_token: this.csrfToken,
  812. };
  813. if (this.alData.status === 'REPEATING') {
  814. formData['add_anime[is_rewatching]'] = 1;
  815. }
  816. return createMALFormData(formData);
  817. }
  818. }
  819. exports.MALEntryAnime = MALEntryAnime;
  820. class MALEntryManga extends BaseMALEntry {
  821. constructor(al, mal, csrfToken = '', dom = Dom_1.default) {
  822. super(al, mal, csrfToken, dom);
  823. }
  824. createPostData() {
  825. const result = this.createBaseMALPostItem();
  826. result.manga_id = this.alData.id;
  827. if (this.alData.repeat)
  828. result.num_read_times = this.alData.repeat;
  829. /* Setting num_read_chapters and num_read_volumes */
  830. // If this is a new item, malData is undefined, so set count to 0
  831. // When the list refreshes the count will be available and be set then
  832. if (!this.malData) {
  833. result.num_read_chapters = 0;
  834. result.num_read_volumes = 0;
  835. return result;
  836. }
  837. // If malData.manga_num_chapters is 0, the manga is still publishing;
  838. // We're forced to use AL's count even though that might be wrong
  839. if (this.malData.manga_num_chapters === 0) {
  840. result.num_read_chapters = this.alData.progress;
  841. result.num_read_volumes = this.alData.progressVolumes;
  842. return result;
  843. }
  844. // If the manga is completed, use MAL's count in case AL's count is different;
  845. // We don't want MAL showing higher or lower than their own count
  846. if (result.status === MALStatus.Completed) {
  847. result.num_read_chapters = this.malData.manga_num_chapters;
  848. result.num_read_volumes = this.malData.manga_num_volumes;
  849. return result;
  850. }
  851. // Othewrise, use MAL's count as a max
  852. result.num_read_chapters = Math.min(this.alData.progress, this.malData.manga_num_chapters);
  853. result.num_read_volumes = Math.min(this.alData.progressVolumes, this.malData.manga_num_volumes);
  854. return result;
  855. }
  856. async formData() {
  857. const malFormData = new MALForm_1.MALForm(this.alData.type, this.alData.id);
  858. await malFormData.get();
  859. const formData = {
  860. entry_id: 0,
  861. manga_id: this.malData.manga_id,
  862. 'add_manga[status]': this._postData.status,
  863. 'add_manga[num_read_volumes]': this._postData.num_read_volumes || 0,
  864. last_completed_vol: '',
  865. 'add_manga[num_read_chapters]': this._postData.num_read_chapters || 0,
  866. 'add_manga[score]': this._postData.score || '',
  867. 'add_manga[start_date][month]': this._postData.start_date && this._postData.start_date.month || '',
  868. 'add_manga[start_date][day]': this._postData.start_date && this._postData.start_date.day || '',
  869. 'add_manga[start_date][year]': this._postData.start_date && this._postData.start_date.year || '',
  870. 'add_manga[finish_date][month]': this._postData.finish_date && this._postData.finish_date.month || '',
  871. 'add_manga[finish_date][day]': this._postData.finish_date && this._postData.finish_date.day || '',
  872. 'add_manga[finish_date][year]': this._postData.finish_date && this._postData.finish_date.year || '',
  873. 'add_manga[tags]': this.malData.tags || '',
  874. 'add_manga[priority]': malFormData.priority,
  875. 'add_manga[storage_type]': malFormData.storageType,
  876. 'add_manga[num_retail_volumes]': malFormData.numRetailVolumes,
  877. 'add_manga[num_read_times]': this._postData.num_read_times || 0,
  878. 'add_manga[reread_value]': malFormData.rereadValue,
  879. 'add_manga[comments]': malFormData.comments,
  880. 'add_manga[is_asked_to_discuss]': malFormData.discussionSetting,
  881. 'add_manga[sns_post_type]': malFormData.SNSSetting,
  882. csrf_token: this.csrfToken,
  883. submitIt: 0
  884. };
  885. if (this.alData.status === 'REPEATING') {
  886. formData['add_manga[is_rewatching]'] = 1;
  887. }
  888. return createMALFormData(formData);
  889. }
  890. }
  891. exports.MALEntryManga = MALEntryManga;
  892.  
  893.  
  894. /***/ }),
  895. /* 8 */
  896. /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
  897.  
  898.  
  899. Object.defineProperty(exports, "__esModule", ({ value: true }));
  900. exports.MALForm = void 0;
  901. const Util_1 = __webpack_require__(3);
  902. class MALForm {
  903. constructor(type, id) {
  904. this.document = null;
  905. this.type = type;
  906. this.id = id;
  907. }
  908. fetchDocument(type, id) {
  909. return new Promise((resolve, reject) => {
  910. const xhr = new XMLHttpRequest();
  911. xhr.onload = function () {
  912. return resolve(this.responseXML ? this.responseXML : null);
  913. };
  914. xhr.onerror = function (e) {
  915. reject(e);
  916. };
  917. xhr.open('GET', `https://myanimelist.net/ownlist/${type}/${id}/edit`);
  918. xhr.responseType = 'document';
  919. xhr.send();
  920. });
  921. }
  922. getElement(id) {
  923. if (!this.document)
  924. throw new Error('Document not loaded');
  925. return this.document.querySelector(`#add_${this.type}_${id}`);
  926. }
  927. async get() {
  928. await Util_1.sleep(500);
  929. const document = await this.fetchDocument(this.type, this.id);
  930. if (document) {
  931. this.document = document;
  932. }
  933. else {
  934. throw new Error('Unable to fetch form data');
  935. }
  936. }
  937. get priority() {
  938. const el = this.getElement('priority');
  939. if (!el)
  940. throw new Error('Unable to get priority');
  941. return el.value;
  942. }
  943. get storageType() {
  944. const el = this.getElement('storage_type');
  945. if (!el)
  946. throw new Error('Unable to get storage type');
  947. return el.value;
  948. }
  949. get storageValue() {
  950. const el = this.getElement('storage_value');
  951. if (!el)
  952. return '0';
  953. return el.value;
  954. }
  955. get numRetailVolumes() {
  956. const el = this.getElement('num_retail_volumes');
  957. if (!el)
  958. return '0';
  959. return el.value;
  960. }
  961. get rewatchValue() {
  962. const el = this.getElement('rewatch_value');
  963. if (!el)
  964. throw new Error('Unable to get rewatch value');
  965. return el.value;
  966. }
  967. get rereadValue() {
  968. const el = this.getElement('reread_value');
  969. if (!el)
  970. throw new Error('Unable to get reread value');
  971. return el.value;
  972. }
  973. get comments() {
  974. const el = this.getElement('comments');
  975. if (!el)
  976. throw new Error('Unable to get comments');
  977. return el.value;
  978. }
  979. get discussionSetting() {
  980. const el = this.getElement('is_asked_to_discuss');
  981. if (!el)
  982. throw new Error('Unable to get discussion value');
  983. return el.value;
  984. }
  985. get SNSSetting() {
  986. const el = this.getElement('sns_post_type');
  987. if (!el)
  988. throw new Error('Unable to get SNS setting');
  989. return el.value;
  990. }
  991. }
  992. exports.MALForm = MALForm;
  993.  
  994.  
  995. /***/ })
  996. /******/ ]);
  997. /************************************************************************/
  998. /******/ // The module cache
  999. /******/ var __webpack_module_cache__ = {};
  1000. /******/
  1001. /******/ // The require function
  1002. /******/ function __webpack_require__(moduleId) {
  1003. /******/ // Check if module is in cache
  1004. /******/ var cachedModule = __webpack_module_cache__[moduleId];
  1005. /******/ if (cachedModule !== undefined) {
  1006. /******/ return cachedModule.exports;
  1007. /******/ }
  1008. /******/ // Create a new module (and put it into the cache)
  1009. /******/ var module = __webpack_module_cache__[moduleId] = {
  1010. /******/ // no module.id needed
  1011. /******/ // no module.loaded needed
  1012. /******/ exports: {}
  1013. /******/ };
  1014. /******/
  1015. /******/ // Execute the module function
  1016. /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  1017. /******/
  1018. /******/ // Return the exports of the module
  1019. /******/ return module.exports;
  1020. /******/ }
  1021. /******/
  1022. /************************************************************************/
  1023. var __webpack_exports__ = {};
  1024. // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  1025. (() => {
  1026. var exports = __webpack_exports__;
  1027.  
  1028. Object.defineProperty(exports, "__esModule", ({ value: true }));
  1029. const Log_1 = __webpack_require__(1);
  1030. const Dom_1 = __webpack_require__(4);
  1031. const Anilist_1 = __webpack_require__(5);
  1032. const MAL_1 = __webpack_require__(6);
  1033. // Main business logic
  1034. const sync = async (e) => {
  1035. e.preventDefault();
  1036. const anilistUsername = Dom_1.default.getAnilistUsername();
  1037. if (!anilistUsername)
  1038. return;
  1039. const malUsername = Dom_1.default.getMALUsername();
  1040. if (!malUsername) {
  1041. Log_1.default.info('You must be logged in!');
  1042. return;
  1043. }
  1044. const csrfToken = Dom_1.default.getCSRFToken();
  1045. Log_1.default.clear();
  1046. Log_1.default.info(`Fetching data from Anilist...`);
  1047. const anilistList = await Anilist_1.getAnilistList(anilistUsername);
  1048. if (!anilistList) {
  1049. Log_1.default.info(`No data found for user ${anilistUsername}.`);
  1050. return;
  1051. }
  1052. Log_1.default.info(`Fetched Anilist data.`);
  1053. const mal = new MAL_1.default(malUsername, csrfToken);
  1054. await mal.syncType('anime', anilistList.anime);
  1055. await mal.syncType('manga', anilistList.manga);
  1056. Log_1.default.info('Import complete.');
  1057. };
  1058. // Entrypoint
  1059. (() => {
  1060. 'use strict';
  1061. Dom_1.default.addDropDownItem();
  1062. if (window.location.pathname === '/import.php') {
  1063. Dom_1.default.addImportForm(sync);
  1064. }
  1065. })();
  1066.  
  1067. })();
  1068.  
  1069. /******/ })()
  1070. ;

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址