Greasy Fork is available in English.

helpers

Helpers library for AniList Edit Multiple Media Simultaneously

Dette scriptet burde ikke installeres direkte. Det er et bibliotek for andre script å inkludere med det nye metadirektivet // @require https://update.greasyfork.org/scripts/496875/1387743/helpers.js

  1. // ==UserScript==
  2. // @name helpers
  3. // @license MIT
  4. // @namespace rtonne
  5. // @match https://anilist.co/*
  6. // @version 1.0
  7. // @author Rtonne
  8. // @description Helpers library for AniList Edit Multiple Media Simultaneously
  9. // ==/UserScript==
  10.  
  11. /**
  12. * @typedef {{message: string, status: number, locations: {line: number, column: number}[]}} AniListError
  13. * @typedef {{message: string} | AniListError} FetchError
  14. */
  15.  
  16. /**
  17. * Uses a MutationObserver to wait until the element we want exists.
  18. * This function is required because elements take a while to appear sometimes.
  19. * https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
  20. * @param {string} selector A string for document.querySelector describing the elements we want.
  21. * @returns {Promise<HTMLElement[]>} The list of elements found.
  22. */
  23. function waitForElements(selector) {
  24. return new Promise((resolve) => {
  25. if (document.querySelector(selector)) {
  26. return resolve(document.querySelectorAll(selector));
  27. }
  28.  
  29. const observer = new MutationObserver(() => {
  30. if (document.querySelector(selector)) {
  31. observer.disconnect();
  32. resolve(document.querySelectorAll(selector));
  33. }
  34. });
  35.  
  36. observer.observe(document.body, {
  37. childList: true,
  38. subtree: true,
  39. });
  40. });
  41. }
  42.  
  43. /**
  44. * Returns if anime or manga has advanced scoring enabled.
  45. * @returns {Promise<{data: {anime: boolean, manga: boolean}} | {data: {}, errors: FetchError[]}>}
  46. */
  47. async function isAdvancedScoringEnabled() {
  48. const query = `
  49. query {
  50. User(name: "${window.location.href.split("/")[4]}") {
  51. mediaListOptions {
  52. animeList {
  53. advancedScoringEnabled
  54. }
  55. mangaList {
  56. advancedScoringEnabled
  57. }
  58. }
  59. }
  60. }
  61. `;
  62.  
  63. const { data, errors } = await anilistFetch(
  64. JSON.stringify({ query: query, variables: {} })
  65. );
  66. if (errors) {
  67. return { data: {}, errors };
  68. }
  69. const return_data = {
  70. anime:
  71. data["User"]["mediaListOptions"]["animeList"]["advancedScoringEnabled"],
  72. manga:
  73. data["User"]["mediaListOptions"]["mangaList"]["advancedScoringEnabled"],
  74. };
  75. return { data: return_data };
  76. }
  77.  
  78. /**
  79. * Get data from a group of entries.
  80. * @param {int[]} media_ids
  81. * @param {"id"|"isFavourite"|"customLists"|"advancedScores"} field
  82. * @returns {Promise<{data: any[]} | {data: any[], errors: FetchError[]}>}
  83. */
  84. async function getDataFromEntries(media_ids, field) {
  85. const query = `query ($media_ids: [Int], $page: Int, $per_page: Int) {
  86. Page(page: $page, perPage: $per_page) {
  87. mediaList(mediaId_in: $media_ids, userName: "${
  88. window.location.href.split("/")[4]
  89. }", compareWithAuthList: true) {
  90. ${field !== "isFavourite" ? field : "media{isFavourite}"}
  91. }
  92. }
  93. }`;
  94. const page_size = 50;
  95.  
  96. let errors;
  97. let data = [];
  98. for (let i = 0; i < media_ids.length; i += page_size) {
  99. const page = media_ids.slice(i, i + page_size);
  100. const variables = {
  101. media_ids: page,
  102. page: 1,
  103. per_page: page_size,
  104. };
  105. const response = await anilistFetch(
  106. JSON.stringify({
  107. query: query,
  108. variables: variables,
  109. })
  110. );
  111. if (response.errors) {
  112. errors = response.errors;
  113. break;
  114. }
  115. data.push(
  116. ...response.data["Page"]["mediaList"].map((entry) => {
  117. if (field === "isFavourite") {
  118. return entry["media"]["isFavourite"];
  119. }
  120. return entry[field];
  121. })
  122. );
  123. }
  124. if (errors) {
  125. return { data, errors };
  126. }
  127. return { data };
  128. }
  129.  
  130. /**
  131. * Make a GraphQL mutation to a single entry on AniList
  132. * @param {number} id The id of the entry to update. Not media_id (use turnMediaIdsIntoIds() to get the actual id). Should be an int.
  133. * @param {Object} values The values to update
  134. * @param {("CURRENT"|"PLANNING"|"COMPLETED"|"DROPPED"|"PAUSED"|"REPEATING")} [values.status]
  135. * @param {number} [values.score]
  136. * @param {number} [values.scoreRaw] Should be an int.
  137. * @param {number} [values.progress] Should be an int.
  138. * @param {number} [values.progressVolumes] Should be an int.
  139. * @param {number} [values.repeat] Should be an int.
  140. * @param {number} [values.priority] Should be an int.
  141. * @param {boolean} [values.private]
  142. * @param {string} [values.notes]
  143. * @param {boolean} [values.hiddenFromStatusLists]
  144. * @param {string[]} [values.customLists]
  145. * @param {number[]} [values.advancedScores]
  146. * @param {Object} [values.startedAt]
  147. * @param {number} [values.startedAt.year] Should be an int.
  148. * @param {number} [values.startedAt.month] Should be an int.
  149. * @param {number} [values.startedAt.day] Should be an int.
  150. * @param {Object} [values.completedAt]
  151. * @param {number} [values.completedAt.year] Should be an int.
  152. * @param {number} [values.completedAt.month] Should be an int.
  153. * @param {number} [values.completedAt.day] Should be an int.
  154. * @returns {Promise<{errors: FetchError[]} | {}>}
  155. */
  156. async function updateEntry(id, values) {
  157. const query = `
  158. mutation (
  159. $id: Int
  160. $status: MediaListStatus
  161. $score: Float
  162. $scoreRaw: Int
  163. $progress: Int
  164. $progressVolumes: Int
  165. $repeat: Int
  166. $priority: Int
  167. $private: Boolean
  168. $notes: String
  169. $hiddenFromStatusLists: Boolean
  170. $customLists: [String]
  171. $advancedScores: [Float]
  172. $startedAt: FuzzyDateInput
  173. $completedAt: FuzzyDateInput
  174. ) {SaveMediaListEntry(
  175. id: $id
  176. status: $status
  177. score: $score
  178. scoreRaw: $scoreRaw
  179. progress: $progress
  180. progressVolumes: $progressVolumes
  181. repeat: $repeat
  182. priority: $priority
  183. private: $private
  184. notes: $notes
  185. hiddenFromStatusLists: $hiddenFromStatusLists
  186. customLists: $customLists
  187. advancedScores: $advancedScores
  188. startedAt: $startedAt
  189. completedAt: $completedAt
  190. ) {
  191. id
  192. }
  193. }`;
  194.  
  195. const variables = {
  196. id,
  197. ...values,
  198. };
  199.  
  200. const { errors } = await anilistFetch(
  201. JSON.stringify({
  202. query: query,
  203. variables: variables,
  204. })
  205. );
  206. //TODO maybe get all media fields on update to check if they're the same, as validation
  207. if (errors) {
  208. return { errors };
  209. }
  210. // I'm returning empty object instead of void so that checking for errors outside is easier
  211. return {};
  212. }
  213.  
  214. /**
  215. * Make a GraphQL mutation to update multiple entries on AniList
  216. * @param {number[]} ids The ids of the entries to update. Not media_ids (use turnMediaIdsIntoIds() to get the actual ids). Should be ints.
  217. * @param {Object} values The values to update
  218. * @param {("CURRENT"|"PLANNING"|"COMPLETED"|"DROPPED"|"PAUSED"|"REPEATING")} [values.status]
  219. * @param {number} [values.score]
  220. * @param {number} [values.scoreRaw] Should be an int.
  221. * @param {number} [values.progress] Should be an int.
  222. * @param {number} [values.progressVolumes] Should be an int.
  223. * @param {number} [values.repeat] Should be an int.
  224. * @param {number} [values.priority] Should be an int.
  225. * @param {boolean} [values.private]
  226. * @param {string} [values.notes]
  227. * @param {boolean} [values.hiddenFromStatusLists]
  228. * @param {number[]} [values.advancedScores]
  229. * @param {Object} [values.startedAt]
  230. * @param {number} [values.startedAt.year] Should be an int.
  231. * @param {number} [values.startedAt.month] Should be an int.
  232. * @param {number} [values.startedAt.day] Should be an int.
  233. * @param {Object} [values.completedAt]
  234. * @param {number} [values.completedAt.year] Should be an int.
  235. * @param {number} [values.completedAt.month] Should be an int.
  236. * @param {number} [values.completedAt.day] Should be an int.
  237. * @returns {Promise<{errors: FetchError[]} | {}>}
  238. */
  239. async function batchUpdateEntries(ids, values) {
  240. const query = `
  241. mutation (
  242. $ids: [Int]
  243. $status: MediaListStatus
  244. $score: Float
  245. $scoreRaw: Int
  246. $progress: Int
  247. $progressVolumes: Int
  248. $repeat: Int
  249. $priority: Int
  250. $private: Boolean
  251. $notes: String
  252. $hiddenFromStatusLists: Boolean
  253. $advancedScores: [Float]
  254. $startedAt: FuzzyDateInput
  255. $completedAt: FuzzyDateInput
  256. ) {UpdateMediaListEntries(
  257. ids: $ids
  258. status: $status
  259. score: $score
  260. scoreRaw: $scoreRaw
  261. progress: $progress
  262. progressVolumes: $progressVolumes
  263. repeat: $repeat
  264. priority: $priority
  265. private: $private
  266. notes: $notes
  267. hiddenFromStatusLists: $hiddenFromStatusLists
  268. advancedScores: $advancedScores
  269. startedAt: $startedAt
  270. completedAt: $completedAt
  271. ) {
  272. id
  273. }
  274. }`;
  275.  
  276. const variables = {
  277. ids,
  278. ...values,
  279. };
  280.  
  281. const { errors } = await anilistFetch(
  282. JSON.stringify({
  283. query: query,
  284. variables: variables,
  285. })
  286. );
  287. //TODO maybe get all media fields on update to check if they're the same, as validation
  288. if (errors) {
  289. return { errors };
  290. }
  291. // I'm returning empty object instead of void so that checking for errors outside is easier
  292. return {};
  293. }
  294.  
  295. /**
  296. * Make a GraphQL mutation to toggle the favourite status for an entry on AniList
  297. * @param {{animeId: number} | {mangaId: number}} id Should be ints.
  298. * @returns {Promise<{errors: FetchError[]} | {}>}
  299. */
  300. async function toggleFavouriteForEntry(id) {
  301. const query = `
  302. mutation {ToggleFavourite(
  303. ${id.animeId ? "animeId: " + id.animeId : ""}
  304. ${id.mangaId ? "mangaId: " + id.mangaId : ""}
  305. ) {
  306. ${id.mangaId ? "manga" : "anime"} {
  307. nodes {
  308. id
  309. }
  310. }
  311. }
  312. }
  313. `;
  314.  
  315. const { errors } = await anilistFetch(
  316. JSON.stringify({
  317. query: query,
  318. variables: {},
  319. })
  320. );
  321. // Not doing extra validation because the data returned depends on if its toggled on or off.
  322. // We could check if the entry id has been added/removed to the node list but if we have more
  323. // than 50 favourites I think we would need to query multiple pages, and I don't feel like doing it.
  324. if (errors) {
  325. return { errors };
  326. }
  327. // I'm returning empty object instead of void so that checking for errors outside is easier
  328. return {};
  329. }
  330.  
  331. /**
  332. * Make a GraphQL mutation to delete an entry on AniList
  333. * @param {number} id Should be an int.
  334. * @returns {Promise<{errors: FetchError[]} | {}>}
  335. */
  336. async function deleteEntry(id) {
  337. const query = `
  338. mutation (
  339. $id: Int
  340. ) {DeleteMediaListEntry(
  341. id: $id
  342. ) {
  343. deleted
  344. }
  345. }`;
  346.  
  347. const variables = {
  348. id,
  349. };
  350.  
  351. const { data, errors } = await anilistFetch(
  352. JSON.stringify({
  353. query: query,
  354. variables: variables,
  355. })
  356. );
  357.  
  358. if (errors) {
  359. return { errors };
  360. } else if (data && !data["DeleteMediaListEntry"]["deleted"]) {
  361. console.error(
  362. `The deletion request threw no errors but id ${id} was not deleted.`
  363. );
  364. return {
  365. errors: [
  366. {
  367. message: `The deletion request threw no errors but id ${id} was not deleted.`,
  368. },
  369. ],
  370. };
  371. }
  372. // I'm returning empty object instead of void so that checking for errors outside is easier
  373. return {};
  374. }
  375.  
  376. /**
  377. * Requests from the AniList GraphQL API.
  378. * Uses a url and token specific to the website for simplicity
  379. * (the user doesn't need to get a token) and for no rate limiting.
  380. * @param {string} body A GraphQL query string.
  381. * @returns A dict with the json data or the errors.
  382. */
  383. async function anilistFetch(body) {
  384. const tokenScript = document
  385. .evaluate("/html/head/script[contains(., 'window.al_token')]", document)
  386. .iterateNext();
  387. const token = tokenScript.innerText.substring(
  388. tokenScript.innerText.indexOf('"') + 1,
  389. tokenScript.innerText.lastIndexOf('"')
  390. );
  391.  
  392. let url = "https://anilist.co/graphql";
  393. let options = {
  394. method: "POST",
  395. headers: {
  396. "X-Csrf-Token": token,
  397. "Content-Type": "application/json",
  398. Accept: "application/json",
  399. },
  400. body,
  401. };
  402.  
  403. function handleResponse(response) {
  404. return response.json().then(function (json) {
  405. return response.ok ? json : Promise.reject(json);
  406. });
  407. }
  408.  
  409. /**
  410. * @param {{data: any}} response
  411. */
  412. function handleData(response) {
  413. return response;
  414. }
  415.  
  416. /**
  417. * @param {{data: {[_: string]: null}, errors: AniListError[]} | Error} e
  418. * @returns {{errors: FetchError[]}}
  419. */
  420. function handleErrors(e) {
  421. // alert(
  422. // "An error ocurred when requesting from the AniList API. Check the console for more details."
  423. // );
  424. console.error(e);
  425. if (e instanceof Error) {
  426. return { errors: [{ message: e.toString() }] };
  427. }
  428. return { errors: e.errors };
  429. }
  430.  
  431. return await fetch(url, options)
  432. .then(handleResponse)
  433. .then(handleData)
  434. .catch(handleErrors);
  435. }