MyAnimeList (MAL) Tags Updater

Adds type, genres and other info to entries tags. Can also delete all current tags.

  1. // ==UserScript==
  2. // @name MyAnimeList (MAL) Tags Updater
  3. // @namespace https://gf.qytechs.cn/users/7517
  4. // @description Adds type, genres and other info to entries tags. Can also delete all current tags.
  5. // @icon http://i.imgur.com/b7Fw8oH.png
  6. // @version 6.1.3
  7. // @author akarin
  8. // @include /^https?:\/\/myanimelist\.net\/(anime|manga)list\//
  9. // @include /^https?:\/\/myanimelist\.net\/panel\.php\?go=(add|edit)/
  10. // @include /^https?:\/\/myanimelist\.net\/editlist\.php\?type=anime/
  11. // @include /^https?:\/\/myanimelist\.net\/ownlist\/(anime|manga)\//
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function ($) {
  16. 'use strict';
  17.  
  18. const T_PAGE = {
  19. M_LIST: 1, M_POPUP: 2
  20. };
  21.  
  22. const T_RUN = {
  23. M_FULL: 1, M_EMPTY: 2, M_CLEAR: 3
  24. };
  25.  
  26. const T_STATUS = {
  27. ALL: 7, IN_PROGRESS: 1, COMPLETED: 2, ON_HOLD: 3, DROPPED: 4, PLAN_TO: 6
  28. };
  29.  
  30. const TAGS_CHAR_MAX = 255;
  31.  
  32. const mal = {
  33. version: '6.0', // cache
  34. page: document.URL.match(/^https?:\/\/myanimelist\.net\/(anime|manga)list\//) ? T_PAGE.M_LIST : T_PAGE.M_POPUP,
  35. type: '', // anime or manga
  36. status: '',
  37. entries: {
  38. updating: false,
  39. total: 0,
  40. done: 0,
  41. fail: 0
  42. },
  43. content: {
  44. stage: $('<span id="tu_stage">'),
  45. done: $('<span id="tu_status_done">'),
  46. fail: $('<span id="tu_status_fail">')
  47. }
  48. };
  49.  
  50. const sleep = (ms) => {
  51. return new Promise(resolve => setTimeout(resolve, ms));
  52. };
  53.  
  54. $.fn.myfancybox = function (onstart) {
  55. return $(this).click(() => {
  56. mal.fancybox.start(onstart);
  57. });
  58. };
  59.  
  60. mal.fancybox = {
  61. body: $('<div id="tu_fancybox_inner">'),
  62. outer: $('<div id="tu_fancybox_outer">'),
  63. wrapper: $('<div id="tu_fancybox_wrapper">'),
  64.  
  65. init: (el) => {
  66. mal.fancybox.outer.hide()
  67. .append(mal.fancybox.body)
  68. .insertAfter(el);
  69.  
  70. mal.fancybox.wrapper.hide()
  71. .insertAfter(el);
  72.  
  73. mal.fancybox.wrapper.click(() => {
  74. mal.fancybox.close();
  75. });
  76. },
  77.  
  78. start: (onstart) => {
  79. mal.fancybox.body.children().hide();
  80. if (onstart()) {
  81. mal.fancybox.wrapper.show();
  82. mal.fancybox.outer.show();
  83. } else {
  84. mal.fancybox.close();
  85. }
  86. },
  87.  
  88. close: () => {
  89. mal.fancybox.outer.hide();
  90. mal.fancybox.wrapper.hide();
  91. }
  92. };
  93.  
  94. const T_ = {
  95. TYPE: 1,
  96. STATUS: 2,
  97. AIRED: 3,
  98. PRODUCERS: 4,
  99. LICENSORS: 5,
  100. STUDIOS: 6,
  101. AUTHORS: 7,
  102. SERIALIZATION: 8,
  103. GENRES: 9,
  104. JRATING: 10,
  105. RATING: 11,
  106. BROADCAST: 12,
  107. SOURCE: 13,
  108. SCORE: 14,
  109. RANK: 15,
  110. POPULARITY: 16,
  111. MEMBERS: 17,
  112. FAVORITES: 18,
  113. JAPANESE: 19,
  114. ENGLISH: 20,
  115. DURATION: 101,
  116. YEAR: 102,
  117. SEASON: 103,
  118. PERIOD: 104,
  119. RND_SCORE: 105,
  120. YEAR_SHORT: 106,
  121. SEASON_SHORT: 107,
  122. PERIOD_SHORT: 108
  123. };
  124.  
  125. const TAGS_ARRAY = {
  126. anime: [
  127. { id: T_.TYPE, text: 'Type', has_prefix: true, prefix: '', def: true },
  128. { id: T_.GENRES, text: 'Genres', has_prefix: false, prefix: '', def: true },
  129. { id: T_.STUDIOS, text: 'Studios', has_prefix: false, prefix: '', def: true },
  130. { id: T_.LICENSORS, text: 'Licensors', has_prefix: false, prefix: '', def: true },
  131. { id: T_.PRODUCERS, text: 'Producers', has_prefix: false, prefix: '', def: false },
  132. { id: T_.DURATION, text: 'Episode Length', has_prefix: true, prefix: '', def: true },
  133. { id: T_.RATING, text: 'Rating', has_prefix: true, prefix: 'Rating: ', def: false },
  134. { id: T_.JRATING, text: 'Japanese Rating', has_prefix: true, prefix: 'Rating: ', def: false },
  135. { id: T_.BROADCAST, text: 'Broadcast', has_prefix: true, prefix: 'Broadcast: ', def: false },
  136. { id: T_.SOURCE, text: 'Source', has_prefix: true, prefix: 'Source: ', def: false },
  137. { id: T_.STATUS, text: 'Status', has_prefix: true, prefix: 'Status: ', def: false },
  138. { id: T_.YEAR, text: 'Year', has_prefix: true, prefix: '', def: false },
  139. { id: T_.YEAR_SHORT, text: 'Year (Short)', has_prefix: true, prefix: '`', def: false },
  140. { id: T_.SEASON, text: 'Season', has_prefix: true, prefix: '', def: true },
  141. { id: T_.SEASON_SHORT, text: 'Season (Short)', has_prefix: true, prefix: '', def: false },
  142. { id: T_.PERIOD, text: 'Time Period', has_prefix: true, prefix: '', def: false },
  143. { id: T_.PERIOD_SHORT, text: 'Time Period (Short)', has_prefix: true, prefix: '`', def: false },
  144. { id: T_.JAPANESE, text: 'Japanese Title', has_prefix: true, prefix: '', def: false },
  145. { id: T_.ENGLISH, text: 'English Title', has_prefix: true, prefix: '', def: false },
  146. { id: T_.SCORE, text: 'Score', has_prefix: true, prefix: 'Score: ', def: false },
  147. { id: T_.RND_SCORE, text: 'Rounded Score', has_prefix: true, prefix: 'Score: ', def: false },
  148. { id: T_.RANK, text: 'Rank', has_prefix: true, prefix: 'Ranked: ', def: false },
  149. { id: T_.POPULARITY, text: 'Popularity', has_prefix: true, prefix: 'Popularity: ', def: false },
  150. { id: T_.MEMBERS, text: 'Members', has_prefix: true, prefix: 'Members: ', def: false },
  151. { id: T_.FAVORITES, text: 'Favorites', has_prefix: true, prefix: 'Favorites: ', def: false }
  152. ],
  153. manga: [
  154. { id: T_.TYPE, text: 'Type', has_prefix: true, prefix: '', def: true },
  155. { id: T_.GENRES, text: 'Genres', has_prefix: false, prefix: '', def: true },
  156. { id: T_.AUTHORS, text: 'Authors', has_prefix: false, prefix: '', def: true },
  157. { id: T_.SERIALIZATION, text: 'Serialization', has_prefix: true, prefix: '', def: true },
  158. { id: T_.STATUS, text: 'Status', has_prefix: true, prefix: 'Status: ', def: false },
  159. { id: T_.YEAR, text: 'Year', has_prefix: true, prefix: '', def: false },
  160. { id: T_.YEAR_SHORT, text: 'Year (Short)', has_prefix: true, prefix: '`', def: false },
  161. { id: T_.SEASON, text: 'Season', has_prefix: true, prefix: '', def: false },
  162. { id: T_.SEASON_SHORT, text: 'Season (Short)', has_prefix: true, prefix: '', def: false },
  163. { id: T_.PERIOD, text: 'Time Period', has_prefix: true, prefix: '', def: false },
  164. { id: T_.PERIOD_SHORT, text: 'Time Period (Short)', has_prefix: true, prefix: '`', def: false },
  165. { id: T_.JAPANESE, text: 'Japanese Title', has_prefix: true, prefix: '', def: false },
  166. { id: T_.ENGLISH, text: 'English Title', has_prefix: true, prefix: '', def: false },
  167. { id: T_.SCORE, text: 'Score', has_prefix: true, prefix: 'Score: ', def: false },
  168. { id: T_.RND_SCORE, text: 'Rounded Score', has_prefix: true, prefix: 'Score: ', def: false },
  169. { id: T_.RANK, text: 'Rank', has_prefix: true, prefix: 'Ranked: ', def: false },
  170. { id: T_.POPULARITY, text: 'Popularity', has_prefix: true, prefix: 'Popularity: ', def: false },
  171. { id: T_.MEMBERS, text: 'Members', has_prefix: true, prefix: 'Members: ', def: false },
  172. { id: T_.FAVORITES, text: 'Favorites', has_prefix: true, prefix: 'Favorites: ', def: false }
  173. ]
  174. };
  175.  
  176. const TAGS_ARRAY_SORTED = {
  177. anime: [], manga: []
  178. };
  179.  
  180. TAGS_ARRAY_SORTED.update = () => {
  181. ['anime', 'manga'].forEach((type) => {
  182. const map = mal.settings.order[type];
  183. TAGS_ARRAY_SORTED[type] = TAGS_ARRAY[type].slice().sort((a, b) => {
  184. const a1 = map.hasOwnProperty(a.id) ? map[a.id] : 0;
  185. const b1 = map.hasOwnProperty(b.id) ? map[b.id] : 0;
  186. return a1 - b1;
  187. });
  188. });
  189. };
  190.  
  191. const AJAX = {
  192. delay: 3000
  193. };
  194.  
  195. class Cache {
  196. constructor (name) {
  197. this.name = name;
  198. }
  199.  
  200. encodeKey (key) {
  201. return this.name + '#' + mal.version + '#' + key;
  202. }
  203.  
  204. loadValue (key, value) {
  205. try {
  206. return JSON.parse(localStorage.getItem(this.encodeKey(key))) || value;
  207. } catch (e) {
  208. console.log(e.name + ': ' + e.message);
  209. return value;
  210. }
  211. }
  212.  
  213. saveValue (key, value) {
  214. localStorage.setItem(this.encodeKey(key), JSON.stringify(value));
  215. }
  216. }
  217.  
  218. class MalData {
  219. constructor (username, type, offset, delay) {
  220. this.username = username;
  221. this.type = type;
  222. this.offset = parseInt(offset) || 300;
  223. this.delay = parseInt(delay) || AJAX.delay;
  224. this.running = false;
  225. this.data = {};
  226. this.size = 0;
  227. }
  228.  
  229. clear () {
  230. this.running = false;
  231. this.data = {};
  232. this.size = 0;
  233. }
  234.  
  235. async load (status, callbacks, filter, offset = 0, trycnt = 0) {
  236. if (!this.running) {
  237. return;
  238. }
  239.  
  240. const hasFilter = Array.isArray(filter) && filter.length > 0;
  241.  
  242. try {
  243. const response = await fetch('/' + this.type + 'list/' + this.username + '/load.json?offset=' + offset + '&status=' + status);
  244. if (!response.ok) {
  245. throw false;
  246. }
  247. const data = await response.json();
  248. if (Array.isArray(data) && data.length > 0) {
  249. data.forEach((entry) => {
  250. this.data[entry[this.type + '_id']] = hasFilter ? Object.keys(entry)
  251. .filter(key => filter.includes(key))
  252. .reduce((obj, key) => {
  253. obj[key] = entry[key];
  254. return obj;
  255. }, {}) : entry;
  256. });
  257.  
  258. this.size = this.size + data.length;
  259. if (callbacks.hasOwnProperty('onNext')) {
  260. await callbacks.onNext(this.size);
  261. }
  262. } else {
  263. if (callbacks.hasOwnProperty('onFinish')) {
  264. await callbacks.onFinish(Object.assign({}, this.data));
  265. }
  266. this.clear();
  267. }
  268. } catch (e) {
  269. if (trycnt >= 10) {
  270. this.clear();
  271. if (callbacks.hasOwnProperty('onError')) {
  272. await callbacks.onError();
  273. }
  274. } else {
  275. await sleep(this.delay * 2);
  276. return this.load(status, callbacks, filter, offset, trycnt + 1);
  277. }
  278. }
  279. }
  280.  
  281. async populate (status, callbacks, filter) {
  282. if (this.running) {
  283. return;
  284. }
  285.  
  286. this.clear();
  287. this.running = true;
  288.  
  289. for (let offset = 0; this.running; offset += this.offset) {
  290. for (let trycnt = 10; trycnt > 0; trycnt -= 1) {
  291. try {
  292. await sleep(this.delay);
  293. await this.load(parseInt(status) || T_STATUS.ALL, callbacks, filter, offset);
  294. break;
  295. } catch (e) {
  296. if (trycnt <= 1) {
  297. this.running = false;
  298. return;
  299. }
  300. }
  301. }
  302. }
  303.  
  304. this.running = false;
  305. }
  306. }
  307.  
  308. mal.settings = {
  309. cache: new Cache('mal_tags_updater'),
  310. body: $('<div id="tu_settings">'),
  311. ajax: { delay: AJAX.delay },
  312. tags: { anime: [], manga: [] },
  313. order: { anime: {}, manga: {} },
  314. prefix: { anime: {}, manga: {} },
  315. status: { anime: T_STATUS.ALL, manga: T_STATUS.ALL },
  316.  
  317. load: () => {
  318. mal.settings.reset();
  319. mal.settings.ajax.delay = mal.settings.cache.loadValue('mal.settings.ajax.delay', mal.settings.ajax.delay);
  320.  
  321. ['anime', 'manga'].forEach((type) => {
  322. mal.settings.tags[type] = mal.settings.cache.loadValue('mal.settings.tags.' + type, mal.settings.tags[type]);
  323. mal.settings.order[type] = mal.settings.cache.loadValue('mal.settings.order.' + type, mal.settings.order[type]);
  324. mal.settings.prefix[type] = mal.settings.cache.loadValue('mal.settings.prefix.' + type, mal.settings.prefix[type]);
  325. mal.settings.status[type] = mal.settings.cache.loadValue('mal.settings.status.' + type, mal.settings.status[type]);
  326. });
  327.  
  328. TAGS_ARRAY_SORTED.update();
  329. },
  330.  
  331. save: () => {
  332. mal.settings.cache.saveValue('mal.settings.ajax.delay', mal.settings.ajax.delay);
  333.  
  334. ['anime', 'manga'].forEach((type) => {
  335. mal.settings.cache.saveValue('mal.settings.tags.' + type, mal.settings.tags[type]);
  336. mal.settings.cache.saveValue('mal.settings.order.' + type, mal.settings.order[type]);
  337. mal.settings.cache.saveValue('mal.settings.prefix.' + type, mal.settings.prefix[type]);
  338. mal.settings.cache.saveValue('mal.settings.status.' + type, mal.settings.status[type]);
  339. });
  340.  
  341. TAGS_ARRAY_SORTED.update();
  342. },
  343.  
  344. reset: () => {
  345. mal.settings.ajax.delay = AJAX.delay;
  346.  
  347. ['anime', 'manga'].forEach((type) => {
  348. mal.settings.tags[type] = [];
  349. mal.settings.order[type] = {};
  350. mal.settings.prefix[type] = {};
  351. mal.settings.status[type] = T_STATUS.ALL;
  352. TAGS_ARRAY[type].forEach((tag, index) => {
  353. if (tag.def) {
  354. mal.settings.tags[type].push(tag.id);
  355. }
  356. if (tag.has_prefix) {
  357. mal.settings.prefix[type][tag.id] = tag.prefix;
  358. }
  359. mal.settings.order[type][tag.id] = index + 1;
  360. });
  361. });
  362. },
  363.  
  364. update: () => {
  365. mal.settings.body.empty();
  366.  
  367. const table = $('<table class="tu_table" border="0" cellpadding="0" cellspacing="0" width="100%">' +
  368. '<thead><tr>' +
  369. '<th>Anime Tags <span>(Order / Prefix / Status)</span></th>' +
  370. '<th>Manga Tags <span>(Order / Prefix / Status)</span></th>' +
  371. '</tr></thead></table>');
  372. const tbody = $('<tbody>').appendTo(table);
  373.  
  374. const reTags = {
  375. anime: new RegExp('^(' + mal.settings.tags.anime.join('|') + ')$'),
  376. manga: new RegExp('^(' + mal.settings.tags.manga.join('|') + ')$')
  377. };
  378.  
  379. const maxLength = Math.max(TAGS_ARRAY.anime.length, TAGS_ARRAY.manga.length);
  380. for (let i = 0; i < maxLength; i += 1) {
  381. const tr = $('<tr>').appendTo(tbody);
  382.  
  383. ['anime', 'manga'].forEach((type) => {
  384. if (i < TAGS_ARRAY[type].length) {
  385. const tag = TAGS_ARRAY[type][i];
  386. const mapOrder = mal.settings.order[type];
  387. const mapPrefix = mal.settings.prefix[type];
  388.  
  389. const el = $('<div class="tu_checkbox">')
  390. .append('<input type="number" min="0" max="999">')
  391. .append('<input type="text" value="">')
  392. .append('<input name="tu_cb' + type[0] + '_' + tag.id +
  393. '" id="tu_cb' + type[0] + '_' + tag.id + '" type="checkbox">')
  394. .append('<label for="tu_cb' + type[0] + '_' + tag.id + '">' + tag.text + '</label>');
  395.  
  396. $('input[type=number]', el).val(mapOrder.hasOwnProperty(tag.id) ? mapOrder[tag.id] : 0);
  397. $('input[type=checkbox]', el).prop('checked', tag.id.toString().match(reTags[type]));
  398.  
  399. const prefix = $('input[type=text]', el);
  400. if (tag.has_prefix) {
  401. prefix.val(mapPrefix.hasOwnProperty(tag.id) ? mapPrefix[tag.id] : '');
  402. } else {
  403. prefix.prop('disabled', true);
  404. }
  405.  
  406. $('<td>').append(el).appendTo(tr);
  407. } else {
  408. $('<td>').appendTo(tr);
  409. }
  410. });
  411. }
  412.  
  413. const ajax = $('<div class="tu_ajax">')
  414. .append('<label>Requests Delay (ms):</label>')
  415. .append('<input id="tu_ajax_delay" type="number" min="100" max="99999">');
  416.  
  417. $('input[id^="tu_ajax_"]', ajax).each(function () {
  418. const id = this.id.match(/[^_]+$/)[0];
  419. $(this).val(mal.settings.ajax[id] || AJAX[id]);
  420. $(this).attr('placeholder', AJAX[id]);
  421. });
  422.  
  423. const status = $('<div class="tu_status">');
  424. ['anime', 'manga'].forEach((type) => {
  425. status.append('<label>Filter Entries:</label>');
  426. $('<select id="tu_status_' + type + '">')
  427. .append('<option value="' + T_STATUS.ALL + '">All ' + type.replace(/^a/, 'A').replace(/^m/, 'M') + '</option>')
  428. .append('<option value="' + T_STATUS.IN_PROGRESS + '">' + (type === 'anime' ? 'Watching' : 'Reading') + '</option>')
  429. .append('<option value="' + T_STATUS.COMPLETED + '">Completed</option>')
  430. .append('<option value="' + T_STATUS.ON_HOLD + '">On-Hold</option>')
  431. .append('<option value="' + T_STATUS.DROPPED + '">Dropped</option>')
  432. .append('<option value="' + T_STATUS.PLAN_TO + '">Plan to ' + (type === 'anime' ? 'Watch' : 'Read') + '</option>')
  433. .val(mal.settings.status[type])
  434. .change(function () {
  435. mal.settings.status[type] = parseInt($(this).val() || T_STATUS.ALL);
  436. })
  437. .appendTo(status);
  438. });
  439.  
  440. const buttons = $('<div class="tu_buttons">')
  441. .append($('<input class="tu_button" value="Save" type="button">').click(() => {
  442. ['anime', 'manga'].forEach((type) => {
  443. mal.settings.tags[type] = [];
  444. mal.settings.order[type] = {};
  445.  
  446. $('input[type=checkbox][id^="tu_cb' + type[0] + '_"]', mal.settings.body).each(function () {
  447. const id = this.id.match(/\d+/)[0];
  448. if ($(this).prop('checked')) {
  449. mal.settings.tags[type].push(id);
  450. }
  451.  
  452. let order = parseInt($(this).parent().find('input[type=number]').val()) || 0;
  453. order = Math.max(order, 0);
  454. order = Math.min(order, 999);
  455. mal.settings.order[type][id] = order;
  456.  
  457. const prefix = $(this).parent().find('input[type=text]').val();
  458. mal.settings.prefix[type][id] = prefix.replace(/^\s+$/, '');
  459. });
  460.  
  461. $('input[id^="tu_ajax_"]', mal.settings.body).each(function () {
  462. const id = this.id.match(/[^_]+$/)[0];
  463. mal.settings.ajax[id] = parseInt($(this).val()) || AJAX[id];
  464. });
  465. });
  466.  
  467. mal.settings.save();
  468. mal.fancybox.close();
  469. }))
  470. .append($('<input class="tu_button" value="Cancel" type="button">').click(() => {
  471. mal.fancybox.close();
  472. }))
  473. .append($('<input class="tu_button" value="Reset" type="button">').click(() => {
  474. mal.settings.reset();
  475. mal.settings.save();
  476. mal.fancybox.close();
  477. }));
  478.  
  479. mal.settings.body
  480. .append('<div class="tu_title">Tags Settings</div>')
  481. .append($('<div class="tu_table_div">')
  482. .append(table)
  483. .append(ajax)
  484. .append(status)
  485. )
  486. .append(buttons);
  487. }
  488. };
  489.  
  490. const formatProducers = (str) => {
  491. return String(str)
  492. .replace(/None\sfound,\s<a\shref="[^"]*?\/dbchanges\.php\?[^>]*?>add\ssome<\/a>\.?/i, '')
  493. .replace(/<sup>[\s\S]*?<\/sup>/g, '')
  494. .replace(/,/g, '')
  495. .replace(/<\/a>\s*?<a/g, '</a>, <a');
  496. };
  497.  
  498. const getTagsFromDuration = (type, duration) => {
  499. const reSec = duration.match(/(\d+)\ssec./)
  500. const reMin = duration.match(/(\d+)\smin./);
  501. const reHour = duration.match(/(\d+)\shr./);
  502.  
  503. duration = reHour ? (parseInt(reHour[1]) * 60 * 60) : 0;
  504. duration += reMin ? (parseInt(reMin[1]) * 60) : 0;
  505. duration += reSec ? (parseInt(reSec[1])) : 0;
  506.  
  507. if (type.match(/(Music|Unknown)/) || duration <= 0) {
  508. return '';
  509. }
  510. if (duration > (32*60) && !type.match('Movie')) {
  511. return 'Long-ep';
  512. }
  513. if (duration < (10*60)) {
  514. return 'Short-ep';
  515. }
  516. if (duration <= (16*60)) {
  517. return 'Half-ep';
  518. }
  519.  
  520. return '';
  521. };
  522.  
  523. const getDateFromString = (str) => {
  524. const result = {
  525. year: '',
  526. year_short: '',
  527. season: '',
  528. season_short: '',
  529. period: '',
  530. period_short: ''
  531. };
  532. const date = str.replace(/to(.*)$/, '').trim();
  533. const mYear = date.match(/\d{4}/);
  534. const mMonth = date.match(/^[a-zA-Z]{3}/);
  535.  
  536. if (!mYear) {
  537. return result;
  538. }
  539.  
  540. result.year = mYear[0];
  541. result.year_short = result.year.replace(/(\d\d)(\d\d)$/, '$2');
  542.  
  543. if (mMonth) {
  544. result.season = mMonth[0]
  545. .replace(/^(Jan|Feb|Mar)$/i, 'Winter')
  546. .replace(/^(Apr|May|Jun)$/i, 'Spring')
  547. .replace(/^(Jul|Aug|Sep)$/i, 'Summer')
  548. .replace(/^(Oct|Nov|Dec)$/i, 'Fall');
  549. result.season += ' ' + result.year;
  550. result.season_short = result.season.replace(/(\d\d)(\d\d)$/, '$2');
  551. }
  552.  
  553. const years = [
  554. [1917, 1959], [1960, 1979], [1980, 1989], [1990, 1999], [2000, 2004],
  555. [2005, 2009], [2010, 2014], [2015, 2019], [2020, 2024], [2025, 2029]
  556. ];
  557. for (let i = years.length - 1; i >= 0; i -= 1) {
  558. if (result.year >= years[i][0] && result.year <= years[i][1]) {
  559. result.period = years[i][0] + '-' + years[i][1];
  560. result.period_short = result.period.replace(/(\d\d)(\d\d)/g, '$2');
  561. break;
  562. }
  563. }
  564.  
  565. return result;
  566. };
  567.  
  568. const getTags = (data) => {
  569. const result = [];
  570. const reTags = new RegExp('^(' + mal.settings.tags[mal.type].join('|') + ')$');
  571.  
  572. let re = data.match(/<div\sid="editdiv"([\s\S]*?)<h2>Information<\/h2>([\s\S]*?)<h2>Statistics<\/h2>([\s\S]*?)<\/td>/);
  573. if (!re) {
  574. return null;
  575. }
  576.  
  577. const titles = re[1];
  578. const info = re[2];
  579. const stats = re[3];
  580.  
  581. re = info.match(/[\s\S]*?>(Aired|Published):<\/span>([\s\S]*?)<\/div>/);
  582. const date = re ? getDateFromString(re[2]) : null;
  583. const textarea = $('<textarea>');
  584. const mapPrefix = mal.settings.prefix[mal.type];
  585.  
  586. TAGS_ARRAY_SORTED[mal.type].forEach((tag) => {
  587. if (!tag.id.toString().match(reTags)) {
  588. return;
  589. }
  590.  
  591. const prefix = tag.has_prefix ? (mapPrefix.hasOwnProperty(tag.id) ? mapPrefix[tag.id] : '') : '';
  592.  
  593. switch (tag.id) {
  594. case T_.JAPANESE:
  595. case T_.ENGLISH:
  596. if (tag.id === T_.JAPANESE) {
  597. re = titles.match(/[\s\S]*?>Japanese:<\/span>([\s\S]*?)<\/div>/);
  598. } else {
  599. re = titles.match(/[\s\S]*?>English:<\/span>([\s\S]*?)<\/div>/);
  600. }
  601. if (re) {
  602. textarea.html(re[1].trim());
  603. re = textarea.val();
  604. if (re.length > 0) {
  605. result.push(prefix + re);
  606. }
  607. }
  608. break;
  609.  
  610. case T_.TYPE:
  611. re = info.match(/[\s\S]*?>Type:<\/span>([\s\S]*?)<\/div>/);
  612. textarea.html(re ? re[1].trim() : '<a>N/A</a>');
  613. re = textarea.val().trim()
  614. .replace('Unknown', 'N/A')
  615. .replace(/<[^>]*?>/g, '');
  616. if (re !== 'N/A' || prefix.length > 0) {
  617. result.push(prefix + re);
  618. }
  619. break;
  620.  
  621. case T_.GENRES:
  622. if (mal.type === 'anime') {
  623. re = info.match(/[\s\S]*?>Genres:<\/span>([\s\S]*?)<\/div>[\s\S]*?>Rating:<\/span>([\s\S]*?)(\s-|None)/);
  624. } else {
  625. re = info.match(/[\s\S]*?>Genres:<\/span>([\s\S]*?)<\/div>/);
  626. }
  627. if (re) {
  628. $(re[1].replace('No genres have been added yet.', '')).filter('a[title]').each(function () {
  629. result.push($(this).text());
  630. });
  631. if (mal.type === 'anime' && re[2].match('Rx')) {
  632. result.push('Hentai');
  633. }
  634. }
  635. break;
  636.  
  637. case T_.STUDIOS:
  638. re = info.match(/[\s\S]*?>Studios:<\/span>([\s\S]*?)<\/div>/);
  639. if (re) {
  640. result.push($(formatProducers(re[1])).text());
  641. }
  642. break;
  643.  
  644. case T_.LICENSORS:
  645. re = info.match(/[\s\S]*?>Licensors:<\/span>([\s\S]*?)<\/div>/);
  646. if (re) {
  647. result.push($(formatProducers(re[1])).text());
  648. }
  649. break;
  650.  
  651. case T_.PRODUCERS:
  652. re = info.match(/[\s\S]*?>Producers:<\/span>([\s\S]*?)<\/div>/);
  653. if (re) {
  654. result.push($(formatProducers(re[1])).text());
  655. }
  656. break;
  657.  
  658. case T_.AUTHORS:
  659. re = info.match(/[\s\S]*?>Authors:<\/span>([\s\S]*?)<\/div>/);
  660. if (re) {
  661. re = $(re[1]
  662. .replace(/,/g, '')
  663. .replace(/\((Art|Story|Story\s&\sArt)\)/g, '')
  664. .replace(/<\/a>\s*?<a/g, '</a>, <a')
  665. ).text();
  666. if (re.length > 0) {
  667. result.push(prefix + re);
  668. }
  669. }
  670. break;
  671.  
  672. case T_.SERIALIZATION:
  673. re = info.match(/[\s\S]*?>Serialization:<\/span>([\s\S]*?)<\/div>/);
  674. if (re) {
  675. re = $(re[1].replace(/None\s*?$/, '<a>N/A</a>')).text().trim();
  676. if (re !== 'N/A' || prefix.length > 0) {
  677. result.push(prefix + re);
  678. }
  679. }
  680. break;
  681.  
  682. case T_.DURATION:
  683. re = info.match(/[\s\S]*?>Type:<\/span>([\s\S]*?)<\/div>[\s\S]*?>Duration:<\/span>([\s\S]*?)<\/div>/);
  684. if (re) {
  685. re = getTagsFromDuration(re[1], re[2]);
  686. if (re.length > 0) {
  687. result.push(prefix + re);
  688. }
  689. }
  690. break;
  691.  
  692. case T_.RATING:
  693. case T_.JRATING:
  694. re = info.match(/[\s\S]*?>Rating:<\/span>([\s\S]*?)(\s-|None)/);
  695. re = re ? re[1].trim().replace(/^\s*?$/, 'N/A') : 'N/A';
  696. if (tag.id === T_.JRATING) {
  697. re = re
  698. .replace(/^PG$/, 'G')
  699. .replace(/^PG-13$/, 'PG-12')
  700. .replace(/^(R|R\+)$/, 'R-15')
  701. .replace(/^Rx$/, 'R-18');
  702. }
  703. if (re !== 'N/A' || prefix.length > 0) {
  704. result.push(prefix + re);
  705. }
  706. break;
  707.  
  708. case T_.STATUS:
  709. re = info.match(/[\s\S]*?>Status:<\/span>([\s\S]*?)<\/div>/);
  710. re = re ? re[1].trim()
  711. .replace('Finished Airing', 'Finished')
  712. .replace('Currently Airing', 'Airing') : 'N/A';
  713. if (re !== 'N/A' || prefix.length > 0) {
  714. result.push(prefix + re);
  715. }
  716. break;
  717.  
  718. case T_.BROADCAST:
  719. re = info.match(/[\s\S]*?>Broadcast:<\/span>([\s\S]*?)<\/div>/);
  720. if (re) {
  721. re = re[1].trim()
  722. .replace(/s\s[\s\S]*?$/, '')
  723. .replace('Unknown', 'N/A');
  724. if (re !== 'N/A' || prefix.length > 0) {
  725. result.push(prefix + re);
  726. }
  727. }
  728. break;
  729.  
  730. case T_.SOURCE:
  731. re = info.match(/[\s\S]*?>Source:<\/span>([\s\S]*?)<\/div>/);
  732. if (re) {
  733. re = re[1].trim().replace('Unknown', 'N/A');
  734. if (re !== 'N/A' || prefix.length > 0) {
  735. result.push(prefix + re);
  736. }
  737. }
  738. break;
  739.  
  740. case T_.YEAR:
  741. case T_.YEAR_SHORT:
  742. case T_.PERIOD:
  743. case T_.PERIOD_SHORT:
  744. if (date) {
  745. switch (tag.id) {
  746. case T_.YEAR:
  747. re = date.year;
  748. break;
  749. case T_.YEAR_SHORT:
  750. re = date.year_short;
  751. break;
  752. case T_.PERIOD:
  753. re = date.period;
  754. break;
  755. case T_.PERIOD_SHORT:
  756. re = date.period_short;
  757. break;
  758. }
  759. if (re.length > 0) {
  760. result.push(prefix + re);
  761. }
  762. }
  763. break;
  764.  
  765. case T_.SEASON:
  766. case T_.SEASON_SHORT:
  767. re = info.match(/[\s\S]*?>Premiered:<\/span>[^<]*?<a\shref=[^>]+?>([\s\S]*?)<\/a>[^<]*?<\/div>/);
  768. if (re && re[1].match(/(Winter|Spring|Summer|Fall)\s\d{4}/)) {
  769. re = tag.id === T_.SEASON ? re[1].trim() : re[1].trim().replace(/(\d\d)(\d\d)$/, '$2');
  770. result.push(prefix + re);
  771. } else if (date) {
  772. re = tag.id === T_.SEASON ? date.season : date.season_short;
  773. if (re.length > 0) {
  774. result.push(prefix + re);
  775. }
  776. }
  777. break;
  778.  
  779. case T_.SCORE:
  780. case T_.RND_SCORE:
  781. re = stats.match(/itemprop="ratingValue">([\d.]+?)<\/span>/);
  782. re = re ? (tag.id === T_.RND_SCORE ? Math.round(re[1]) : re[1]) : 'N/A';
  783. if (re !== 'N/A' || prefix.length > 0) {
  784. result.push(prefix + re);
  785. }
  786. break;
  787.  
  788. case T_.RANK:
  789. re = stats.match(/[\s\S]*?>Ranked:<\/span>\s*?#(\d+?)\s*?</);
  790. re = re ? re[1] : 'N/A';
  791. if (re !== 'N/A' || prefix.length > 0) {
  792. result.push(prefix + re);
  793. }
  794. break;
  795.  
  796. case T_.POPULARITY:
  797. re = stats.match(/[\s\S]*?>Popularity:<\/span>\s*?#(\d+?)\s*?</);
  798. re = re ? re[1] : 'N/A';
  799. if (re !== 'N/A' || prefix.length > 0) {
  800. result.push(prefix + re);
  801. }
  802. break;
  803.  
  804. case T_.MEMBERS:
  805. re = stats.match(/[\s\S]*?>Members:<\/span>\s*?([\d,]+?)\s*?</);
  806. re = re ? re[1].replace(',', '') : 'N/A';
  807. if (re !== 'N/A' || prefix.length > 0) {
  808. result.push(prefix + re);
  809. }
  810. break;
  811.  
  812. case T_.FAVORITES:
  813. re = stats.match(/[\s\S]*?>Favorites:<\/span>\s*?([\d,]+?)\s*?</);
  814. re = re ? re[1].replace(',', '') : 'N/A';
  815. if (re !== 'N/A' || prefix.length > 0) {
  816. result.push(prefix + re);
  817. }
  818. break;
  819. }
  820. });
  821.  
  822. return result;
  823. };
  824.  
  825. const setTags = async (id, tags) => {
  826. if (!tags) {
  827. throw id;
  828. }
  829.  
  830. const cache = {};
  831. tags = $.map(tags.join(',').split(','), (tag) => {
  832. return tag.trim().replace(/'/g, '’').replace(/\s+/g, ' ');
  833. }).filter((tag) => {
  834. if (tag.length === 0 || cache.hasOwnProperty(tag)) {
  835. return false;
  836. } else {
  837. cache[tag] = true;
  838. return true;
  839. }
  840. }).join(', ');
  841.  
  842. while (tags.length > TAGS_CHAR_MAX) {
  843. tags = tags.replace(/,(?!.*,).*$/, '');
  844. }
  845.  
  846. if (mal.page === T_PAGE.M_POPUP) {
  847. $('textarea#add_' + mal.type + '_tags').prop('value', tags);
  848. } else {
  849. if (tags === '') {
  850. try {
  851. await $.when($.ajax({
  852. type: 'POST',
  853. url: mal.tagsUrl,
  854. data: mal.type[0] + 'id=' + id,
  855. dataType: 'text'
  856. }));
  857. } catch (e) {
  858. return Promise.reject(id);
  859. }
  860. } else {
  861. mal.tags[id] = tags;
  862. }
  863. }
  864. };
  865.  
  866. const updateTags = async (id, mode) => {
  867. try {
  868. let tags = [];
  869. if (mode !== T_RUN.M_CLEAR) {
  870. const response = await fetch('/' + mal.type + '/' + id + '/_/news');
  871. if (!response.ok) {
  872. throw id;
  873. }
  874. tags = getTags(await response.text());
  875. }
  876. return await setTags(id, tags);
  877. } catch (e) {
  878. return Promise.reject(id);
  879. }
  880. };
  881.  
  882. const updateAllTags = async (username, mode) => {
  883. if (mal.page !== T_PAGE.M_LIST || mal.entries.updating) {
  884. return;
  885. }
  886.  
  887. mal.tags = {};
  888.  
  889. if (mal.page === T_PAGE.M_LIST) {
  890. mal.content.stage.html('&nbsp;&nbsp;[1/3]');
  891. mal.content.done.html('&nbsp;&nbsp;Loading...');
  892. mal.content.fail.empty();
  893. }
  894.  
  895. mal.entries.updating = true;
  896. mal.entries.total = 0;
  897. mal.entries.done = 0;
  898. mal.entries.fail = 0;
  899.  
  900. if (mal.settings.tags[mal.type].length === 0) {
  901. mode = T_RUN.M_CLEAR;
  902. }
  903.  
  904. await (new MalData(username, mal.type, 300, mal.settings.ajax.delay)).populate(mal.settings.status[mal.type], {
  905. onFinish: async (data) => {
  906. const keys = Object.keys(data);
  907. mal.entries.total = keys.length;
  908.  
  909. if (mal.entries.total === 0) {
  910. if (mal.page === T_PAGE.M_LIST) {
  911. mal.content.stage.html('&nbsp;&nbsp;[3/3]&nbsp;&nbsp;Finished');
  912. mal.content.done.empty();
  913. }
  914. return;
  915. } else {
  916. if (mal.page === T_PAGE.M_LIST) {
  917. mal.content.stage.html('&nbsp;&nbsp;[2/3]');
  918. mal.content.done.html('&nbsp;&nbsp;Done: ' + mal.entries.done + '/' + mal.entries.total);
  919. }
  920. }
  921.  
  922. const ids = [];
  923. keys.forEach((id) => {
  924. const entry = data[id];
  925. if ((mode === T_RUN.M_EMPTY && entry.tags !== '') ||
  926. (mode === T_RUN.M_CLEAR && entry.tags === '')) {
  927. mal.entries.done += 1;
  928. } else {
  929. ids.push(id);
  930. }
  931. delete data[id];
  932. });
  933.  
  934. while (ids.length > 0) {
  935. const id = ids.shift();
  936. try {
  937. for (let trycnt = 10; trycnt > 0; trycnt -= 1) {
  938. try {
  939. await sleep(mal.settings.ajax.delay);
  940. await updateTags(id, mode);
  941. break;
  942. } catch (e) {
  943. if (trycnt <= 1) {
  944. throw e;
  945. }
  946. }
  947. }
  948.  
  949. if (mal.page === T_PAGE.M_LIST) {
  950. mal.entries.done += 1;
  951. mal.content.done.html('&nbsp;&nbsp;Done: ' + mal.entries.done + '/' + mal.entries.total);
  952. }
  953. } catch (e) {
  954. await sleep(mal.settings.ajax.delay);
  955. await setTags(id, []).catch(() => {});
  956.  
  957. if (mal.page === T_PAGE.M_LIST) {
  958. mal.entries.fail += 1;
  959. mal.content.fail.html('&nbsp;&nbsp;Failed: ' + mal.entries.fail);
  960. console.log('[2/3] failed ' + mal.type + ' id: ' + id);
  961. }
  962. }
  963. }
  964.  
  965. mal.entries.updating = false;
  966. if (mal.page !== T_PAGE.M_LIST) {
  967. return;
  968. }
  969.  
  970. const tags = Object.keys(mal.tags);
  971. mal.entries.total = tags.length + mal.entries.fail;
  972. mal.entries.done = 0;
  973.  
  974. if (tags.length === 0) {
  975. mal.content.stage.html('&nbsp;&nbsp;[3/3]&nbsp;&nbsp;Finished');
  976. mal.content.done.empty();
  977. return;
  978. } else {
  979. mal.content.stage.html('&nbsp;&nbsp;[3/3]');
  980. mal.content.done.html('&nbsp;&nbsp;Done: ' + mal.entries.done + '/' + mal.entries.total);
  981. }
  982.  
  983. while (tags.length > 0) {
  984. const id = tags.shift();
  985. try {
  986. await sleep(mal.settings.ajax.delay);
  987. let data = await $.when($.ajax({
  988. type: 'POST',
  989. url: mal.tagsUrl + encodeURIComponent(mal.tags[id]),
  990. data: mal.type[0] + 'id=' + id,
  991. dataType: 'text'
  992. }));
  993.  
  994. if (!mal.modern && $('#list_surround .table_header[width="125"]').length > 0) {
  995. data = data.replace(/[?&]status=\d/g, '').replace(/&tag=/g, mal.status + '&tag=');
  996. $('#list_surround #tagLinks' + id).html(data);
  997. $('#list_surround #tagRow' + id).text($(data).text());
  998. }
  999. mal.entries.done += 1;
  1000. mal.content.done.html('&nbsp;&nbsp;Done: ' + mal.entries.done + '/' + mal.entries.total);
  1001. } catch (e) {
  1002. await sleep(mal.settings.ajax.delay);
  1003. await setTags(id, []).catch(() => {});
  1004.  
  1005. mal.entries.fail += 1;
  1006. mal.content.fail.html('&nbsp;&nbsp;Failed: ' + mal.entries.fail);
  1007. console.log('[3/3] failed ' + mal.type + ' id: ' + id);
  1008. }
  1009. }
  1010. },
  1011.  
  1012. onNext: (count) => {
  1013. mal.content.done.html('&nbsp;&nbsp;Loading: ' + count);
  1014. },
  1015.  
  1016. onError: () => {
  1017. mal.content.done.empty();
  1018. mal.content.fail.html('&nbsp;&nbsp;Failed');
  1019. mal.entries.total = 0;
  1020. mal.entries.done = 0;
  1021. mal.entries.fail = 0;
  1022. }
  1023. }, [ 'tags' ]);
  1024.  
  1025. mal.entries.updating = false;
  1026. };
  1027.  
  1028. if ($('#malLogin').length === 0 && $('a[href$="/login.php"]').length === 0) {
  1029. mal.settings.load();
  1030. mal.modern = false;
  1031.  
  1032. if (mal.page === T_PAGE.M_LIST) {
  1033. mal.type = document.URL.match(/^https?:\/\/myanimelist\.net\/(anime|manga)list\//)[1];
  1034. mal.modern = $('.header .header-menu .btn-menu > span.username').length > 0;
  1035.  
  1036. let el;
  1037. let username;
  1038.  
  1039. if (mal.modern) {
  1040. if ($('.header .header-info').length === 0) {
  1041. $('.header .header-menu').addClass('other').append('<div class="header-info">');
  1042. }
  1043. el = $('.header .header-info');
  1044. username = $('.list-menu-float .icon-menu.profile').prop('href').match(/\/profile\/(.*)$/)[1];
  1045. mal.status = $('.status-menu-container .status-menu .status-button.on').prop('href').match(/[?&]status=\d/)[0];
  1046. mal.fancybox.init('.list-container');
  1047. } else {
  1048. if (!$('#mal_cs_otherlinks div:first strong').text().match('You are viewing your')) {
  1049. return;
  1050. }
  1051. el = $('<span id="tu_links">').appendTo('#mal_cs_otherlinks div:last');
  1052. username = $('#mal_cs_listinfo strong a strong').text();
  1053. mal.status = $('.status_selected a').prop('href').match(/[?&]status=\d/)[0];
  1054. mal.fancybox.init('#list_surround');
  1055. }
  1056.  
  1057. mal.fancybox.body.append(mal.settings.body);
  1058.  
  1059. el.append((mal.modern ? '' : '&nbsp;|') + '&nbsp;&nbsp;Update Tags: ')
  1060. .append($('<a href="javascript:void(0);" title="Update all tags">All</a>').click(() => {
  1061. if (mal.entries.updating || mal.entries.done + mal.entries.fail < mal.entries.total) {
  1062. alert('Updating in process!');
  1063. } else if (confirm('Are you sure you want to update all tags?')) {
  1064. updateAllTags(username, T_RUN.M_FULL);
  1065. }
  1066. }))
  1067. .append(',&nbsp;')
  1068. .append($('<a href="javascript:void(0);" title="Update only empty tags">Empty</a>').click(() => {
  1069. if (mal.entries.updating || mal.entries.done + mal.entries.fail < mal.entries.total) {
  1070. alert('Updating in process!');
  1071. } else if (confirm('Are you sure you want to update empty tags?')) {
  1072. updateAllTags(username, T_RUN.M_EMPTY);
  1073. }
  1074. }))
  1075. .append('&nbsp;-&nbsp;')
  1076. .append($('<a href="javascript:void(0);" title="Clear all tags">Clear</a>').click(() => {
  1077. if (mal.entries.updating || mal.entries.done + mal.entries.fail < mal.entries.total) {
  1078. alert('Updating in process!');
  1079. } else if (confirm('Are you sure you want to clear all tags?')) {
  1080. updateAllTags(username, T_RUN.M_CLEAR);
  1081. }
  1082. }))
  1083. .append('&nbsp;-&nbsp;')
  1084. .append($('<a href="javascript:void(0);" title="Change Tags Updater settings">Settings</a>').myfancybox(() => {
  1085. mal.settings.update();
  1086. mal.settings.body.show();
  1087. return true;
  1088. }))
  1089. .append(mal.content.stage)
  1090. .append(mal.content.done)
  1091. .append(mal.content.fail);
  1092.  
  1093. $('<style type="text/css">').html(
  1094. 'div#tu_fancybox_wrapper { position: fixed; width: 100%; height: 100%; top: 0; left: 0; background: rgba(102, 102, 102, 0.3); z-index: 99990; }' +
  1095. 'div#tu_fancybox_inner { width: 600px !important; height: 730px !important; overflow: hidden; color: #000; }' +
  1096. 'div#tu_fancybox_outer { position: absolute; display: block; width: auto; height: auto; padding: 10px; border-radius: 8px; top: 80px; left: 50%; margin-top: 0 !important; margin-left: -310px !important; background: #fff; box-shadow: 0 0 15px rgba(32, 32, 32, 0.4); z-index: 99991; }' +
  1097. 'div#tu_settings { width: 100%; height: 100%; text-align: center; padding: 40px 0 35px; box-sizing: border-box; }' +
  1098. 'div#tu_settings .tu_title { position: absolute; top: 10px; left: 10px; width: 600px; font-size: 16px; font-weight: normal; text-align: center; margin: 0; border: 0; }' +
  1099. 'div#tu_settings .tu_title:after { content: ""; display: block; position: relative; width: 100%; height: 8px; margin: 0.5em 0 0; padding: 0; border-top: 1px solid #ebebeb; background: center bottom no-repeat radial-gradient(#f6f6f6, #fff 70%); background-size: 100% 16px; }' +
  1100. 'div#tu_settings .tu_table_div { width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; border: 1px solid #eee; box-sizing: border-box; }' +
  1101. 'div#tu_settings .tu_table thead { background-color: #f5f5f5; }' +
  1102. 'div#tu_settings .tu_table th { background-color: transparent; width: 50%; padding: 5px 0 5px 5px; color: #222; font-size: 13px; font-weight: bold; text-align: left; line-height: 20px !important; box-shadow: none; }' +
  1103. 'div#tu_settings .tu_table th > span { font-size: 11px; font-weight: normal; }' +
  1104. 'div#tu_settings .tu_table tbody { background-color: #fff; }' +
  1105. 'div#tu_settings .tu_table td { text-align: left !important; }' +
  1106. 'div#tu_settings .tu_table .tu_checkbox { font-size: 12px; }' +
  1107. 'div#tu_settings .tu_table .tu_checkbox > * { vertical-align: middle; }' +
  1108. 'div#tu_settings .tu_table .tu_checkbox > input[type=number], div#tu_settings .tu_table .tu_checkbox > input[type=text] { width: 40px !important; margin: 1px 2px 1px 5px !important; padding: 2px 0 1px 2px !important; border: 1px solid #bbb !important; font-size: 11px !important; }' +
  1109. 'div#tu_settings .tu_table .tu_checkbox > input[type=text] { width: 70px !important; margin: 1px 2px !important; text-align: right; }' +
  1110. 'div#tu_settings .tu_table .tu_checkbox > input[type=checkbox] + label { font-weight: normal; color: #666; }' +
  1111. 'div#tu_settings .tu_table .tu_checkbox > input[type=checkbox]:checked + label { font-weight: bold; color: #222; }' +
  1112. 'div#tu_settings .tu_ajax, div#tu_settings .tu_status { width: 100%; text-align: center; margin: 8px 0 4px; border: 0; }' +
  1113. 'div#tu_settings .tu_ajax > *, div#tu_settings .tu_status > * { vertical-align: middle; font-size: 12px; font-weight: normal; margin: 0 6px; }' +
  1114. 'div#tu_settings .tu_ajax > label, div#tu_settings .tu_status > label { padding-top: 1px !important; }' +
  1115. 'div#tu_settings .tu_ajax > input, div#tu_settings .tu_status > select { width: 70px !important; margin-left: 0 !important; padding: 2px 0 1px 2px !important; border: 1px solid #bbb !important; font-size: 11px !important; }' +
  1116. 'div#tu_settings .tu_status > select { width: 100px !important; }' +
  1117. 'div#tu_settings .tu_buttons { position: absolute; bottom: 10px; width: 600px; text-align: center; padding: 0; }' +
  1118. 'div#tu_settings .tu_buttons > .tu_button { margin: 2px 5px !important; font-size: 12px; }'
  1119. ).appendTo('head');
  1120. } else {
  1121. mal.type = document.URL.match(/(\?go=(add|edit)&|\?type=anime&|ownlist\/anime\/)/) ? 'anime' : 'manga';
  1122.  
  1123. const id = $('#main-form > table td.borderClass:contains(Title) + td > strong > a').prop('href').match(/\d+/)[0];
  1124. $('#main-form > table.advanced td.borderClass:contains(Tags)').append('&nbsp;').append(
  1125. $('<a href="javascript:void(0)">').click(() => {
  1126. updateTags(id, T_RUN.M_FULL);
  1127. })
  1128. .append('<small>update</small>')
  1129. );
  1130. }
  1131.  
  1132. mal.tagsUrl = '/includes/ajax.inc.php?' + (mal.type === 'anime' ? 't=22' : 't=30') + '&tags=';
  1133. }
  1134. }(jQuery));

QingJ © 2025

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