您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Helpers library for AniList Edit Multiple Media Simultaneously
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://updategreasyfork.deno.dev/scripts/496875/1387743/helpers.js
// ==UserScript== // @name helpers // @license MIT // @namespace rtonne // @match https://anilist.co/* // @version 1.0 // @author Rtonne // @description Helpers library for AniList Edit Multiple Media Simultaneously // ==/UserScript== /** * @typedef {{message: string, status: number, locations: {line: number, column: number}[]}} AniListError * @typedef {{message: string} | AniListError} FetchError */ /** * Uses a MutationObserver to wait until the element we want exists. * This function is required because elements take a while to appear sometimes. * https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists * @param {string} selector A string for document.querySelector describing the elements we want. * @returns {Promise<HTMLElement[]>} The list of elements found. */ function waitForElements(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelectorAll(selector)); } const observer = new MutationObserver(() => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelectorAll(selector)); } }); observer.observe(document.body, { childList: true, subtree: true, }); }); } /** * Returns if anime or manga has advanced scoring enabled. * @returns {Promise<{data: {anime: boolean, manga: boolean}} | {data: {}, errors: FetchError[]}>} */ async function isAdvancedScoringEnabled() { const query = ` query { User(name: "${window.location.href.split("/")[4]}") { mediaListOptions { animeList { advancedScoringEnabled } mangaList { advancedScoringEnabled } } } } `; const { data, errors } = await anilistFetch( JSON.stringify({ query: query, variables: {} }) ); if (errors) { return { data: {}, errors }; } const return_data = { anime: data["User"]["mediaListOptions"]["animeList"]["advancedScoringEnabled"], manga: data["User"]["mediaListOptions"]["mangaList"]["advancedScoringEnabled"], }; return { data: return_data }; } /** * Get data from a group of entries. * @param {int[]} media_ids * @param {"id"|"isFavourite"|"customLists"|"advancedScores"} field * @returns {Promise<{data: any[]} | {data: any[], errors: FetchError[]}>} */ async function getDataFromEntries(media_ids, field) { const query = `query ($media_ids: [Int], $page: Int, $per_page: Int) { Page(page: $page, perPage: $per_page) { mediaList(mediaId_in: $media_ids, userName: "${ window.location.href.split("/")[4] }", compareWithAuthList: true) { ${field !== "isFavourite" ? field : "media{isFavourite}"} } } }`; const page_size = 50; let errors; let data = []; for (let i = 0; i < media_ids.length; i += page_size) { const page = media_ids.slice(i, i + page_size); const variables = { media_ids: page, page: 1, per_page: page_size, }; const response = await anilistFetch( JSON.stringify({ query: query, variables: variables, }) ); if (response.errors) { errors = response.errors; break; } data.push( ...response.data["Page"]["mediaList"].map((entry) => { if (field === "isFavourite") { return entry["media"]["isFavourite"]; } return entry[field]; }) ); } if (errors) { return { data, errors }; } return { data }; } /** * Make a GraphQL mutation to a single entry on AniList * @param {number} id The id of the entry to update. Not media_id (use turnMediaIdsIntoIds() to get the actual id). Should be an int. * @param {Object} values The values to update * @param {("CURRENT"|"PLANNING"|"COMPLETED"|"DROPPED"|"PAUSED"|"REPEATING")} [values.status] * @param {number} [values.score] * @param {number} [values.scoreRaw] Should be an int. * @param {number} [values.progress] Should be an int. * @param {number} [values.progressVolumes] Should be an int. * @param {number} [values.repeat] Should be an int. * @param {number} [values.priority] Should be an int. * @param {boolean} [values.private] * @param {string} [values.notes] * @param {boolean} [values.hiddenFromStatusLists] * @param {string[]} [values.customLists] * @param {number[]} [values.advancedScores] * @param {Object} [values.startedAt] * @param {number} [values.startedAt.year] Should be an int. * @param {number} [values.startedAt.month] Should be an int. * @param {number} [values.startedAt.day] Should be an int. * @param {Object} [values.completedAt] * @param {number} [values.completedAt.year] Should be an int. * @param {number} [values.completedAt.month] Should be an int. * @param {number} [values.completedAt.day] Should be an int. * @returns {Promise<{errors: FetchError[]} | {}>} */ async function updateEntry(id, values) { const query = ` mutation ( $id: Int $status: MediaListStatus $score: Float $scoreRaw: Int $progress: Int $progressVolumes: Int $repeat: Int $priority: Int $private: Boolean $notes: String $hiddenFromStatusLists: Boolean $customLists: [String] $advancedScores: [Float] $startedAt: FuzzyDateInput $completedAt: FuzzyDateInput ) {SaveMediaListEntry( id: $id status: $status score: $score scoreRaw: $scoreRaw progress: $progress progressVolumes: $progressVolumes repeat: $repeat priority: $priority private: $private notes: $notes hiddenFromStatusLists: $hiddenFromStatusLists customLists: $customLists advancedScores: $advancedScores startedAt: $startedAt completedAt: $completedAt ) { id } }`; const variables = { id, ...values, }; const { errors } = await anilistFetch( JSON.stringify({ query: query, variables: variables, }) ); //TODO maybe get all media fields on update to check if they're the same, as validation if (errors) { return { errors }; } // I'm returning empty object instead of void so that checking for errors outside is easier return {}; } /** * Make a GraphQL mutation to update multiple entries on AniList * @param {number[]} ids The ids of the entries to update. Not media_ids (use turnMediaIdsIntoIds() to get the actual ids). Should be ints. * @param {Object} values The values to update * @param {("CURRENT"|"PLANNING"|"COMPLETED"|"DROPPED"|"PAUSED"|"REPEATING")} [values.status] * @param {number} [values.score] * @param {number} [values.scoreRaw] Should be an int. * @param {number} [values.progress] Should be an int. * @param {number} [values.progressVolumes] Should be an int. * @param {number} [values.repeat] Should be an int. * @param {number} [values.priority] Should be an int. * @param {boolean} [values.private] * @param {string} [values.notes] * @param {boolean} [values.hiddenFromStatusLists] * @param {number[]} [values.advancedScores] * @param {Object} [values.startedAt] * @param {number} [values.startedAt.year] Should be an int. * @param {number} [values.startedAt.month] Should be an int. * @param {number} [values.startedAt.day] Should be an int. * @param {Object} [values.completedAt] * @param {number} [values.completedAt.year] Should be an int. * @param {number} [values.completedAt.month] Should be an int. * @param {number} [values.completedAt.day] Should be an int. * @returns {Promise<{errors: FetchError[]} | {}>} */ async function batchUpdateEntries(ids, values) { const query = ` mutation ( $ids: [Int] $status: MediaListStatus $score: Float $scoreRaw: Int $progress: Int $progressVolumes: Int $repeat: Int $priority: Int $private: Boolean $notes: String $hiddenFromStatusLists: Boolean $advancedScores: [Float] $startedAt: FuzzyDateInput $completedAt: FuzzyDateInput ) {UpdateMediaListEntries( ids: $ids status: $status score: $score scoreRaw: $scoreRaw progress: $progress progressVolumes: $progressVolumes repeat: $repeat priority: $priority private: $private notes: $notes hiddenFromStatusLists: $hiddenFromStatusLists advancedScores: $advancedScores startedAt: $startedAt completedAt: $completedAt ) { id } }`; const variables = { ids, ...values, }; const { errors } = await anilistFetch( JSON.stringify({ query: query, variables: variables, }) ); //TODO maybe get all media fields on update to check if they're the same, as validation if (errors) { return { errors }; } // I'm returning empty object instead of void so that checking for errors outside is easier return {}; } /** * Make a GraphQL mutation to toggle the favourite status for an entry on AniList * @param {{animeId: number} | {mangaId: number}} id Should be ints. * @returns {Promise<{errors: FetchError[]} | {}>} */ async function toggleFavouriteForEntry(id) { const query = ` mutation {ToggleFavourite( ${id.animeId ? "animeId: " + id.animeId : ""} ${id.mangaId ? "mangaId: " + id.mangaId : ""} ) { ${id.mangaId ? "manga" : "anime"} { nodes { id } } } } `; const { errors } = await anilistFetch( JSON.stringify({ query: query, variables: {}, }) ); // Not doing extra validation because the data returned depends on if its toggled on or off. // We could check if the entry id has been added/removed to the node list but if we have more // than 50 favourites I think we would need to query multiple pages, and I don't feel like doing it. if (errors) { return { errors }; } // I'm returning empty object instead of void so that checking for errors outside is easier return {}; } /** * Make a GraphQL mutation to delete an entry on AniList * @param {number} id Should be an int. * @returns {Promise<{errors: FetchError[]} | {}>} */ async function deleteEntry(id) { const query = ` mutation ( $id: Int ) {DeleteMediaListEntry( id: $id ) { deleted } }`; const variables = { id, }; const { data, errors } = await anilistFetch( JSON.stringify({ query: query, variables: variables, }) ); if (errors) { return { errors }; } else if (data && !data["DeleteMediaListEntry"]["deleted"]) { console.error( `The deletion request threw no errors but id ${id} was not deleted.` ); return { errors: [ { message: `The deletion request threw no errors but id ${id} was not deleted.`, }, ], }; } // I'm returning empty object instead of void so that checking for errors outside is easier return {}; } /** * Requests from the AniList GraphQL API. * Uses a url and token specific to the website for simplicity * (the user doesn't need to get a token) and for no rate limiting. * @param {string} body A GraphQL query string. * @returns A dict with the json data or the errors. */ async function anilistFetch(body) { const tokenScript = document .evaluate("/html/head/script[contains(., 'window.al_token')]", document) .iterateNext(); const token = tokenScript.innerText.substring( tokenScript.innerText.indexOf('"') + 1, tokenScript.innerText.lastIndexOf('"') ); let url = "https://anilist.co/graphql"; let options = { method: "POST", headers: { "X-Csrf-Token": token, "Content-Type": "application/json", Accept: "application/json", }, body, }; function handleResponse(response) { return response.json().then(function (json) { return response.ok ? json : Promise.reject(json); }); } /** * @param {{data: any}} response */ function handleData(response) { return response; } /** * @param {{data: {[_: string]: null}, errors: AniListError[]} | Error} e * @returns {{errors: FetchError[]}} */ function handleErrors(e) { // alert( // "An error ocurred when requesting from the AniList API. Check the console for more details." // ); console.error(e); if (e instanceof Error) { return { errors: [{ message: e.toString() }] }; } return { errors: e.errors }; } return await fetch(url, options) .then(handleResponse) .then(handleData) .catch(handleErrors); }