您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
For anilist.co, make manga and anime scores more prominent by moving them to the title.
- // ==UserScript==
- // @name AniList Unlimited - Score in Header
- // @namespace https://github.com/mysticflute
- // @version 1.0.3
- // @description For anilist.co, make manga and anime scores more prominent by moving them to the title.
- // @author mysticflute
- // @homepageURL https://github.com/mysticflute/ani-list-unlimited
- // @supportURL https://github.com/mysticflute/ani-list-unlimited/issues
- // @match https://anilist.co/*
- // @connect graphql.anilist.co
- // @connect api.jikan.moe
- // @connect kitsu.io
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM.xmlHttpRequest
- // @grant GM.setValue
- // @grant GM.getValue
- // @license MIT
- // ==/UserScript==
- // This user script was tested with the following user script managers:
- // - Violentmonkey (preferred): https://violentmonkey.github.io/
- // - TamperMonkey: https://www.tampermonkey.net/
- // - GreaseMonkey: https://www.greasespot.net/
- (async function () {
- 'use strict';
- /**
- * Default user configuration options.
- *
- * You can override these options if your user script runner supports it. Your
- * changes will persist across user script updates.
- *
- * In Violentmonkey:
- * 1. Install the user script.
- * 2. Let the script run at least once by loading an applicable url.
- * 3. Click the edit button for this script from the Violentmonkey menu.
- * 4. Click on the "Values" tab for this script.
- * 5. Click on the configuration option you want to change and edit the value
- * (change to true or false).
- * 6. Click the save button.
- * 7. Refresh or visit the page to see the changes.
- *
- * In TamperMonkey:
- * 1. Install the user script.
- * 2. Let the script run at least once by loading an applicable url.
- * 3. From the TamperMonkey dashboard, click the "Settings" tab.
- * 4. Change the "Config mode" mode to "Advanced".
- * 5. On the "Installed userscripts" tab (dashboard), click the edit button
- * for this script.
- * 6. Click the "Storage" tab. If you don't see this tab be sure the config
- * mode is set to "Advanced" as described above. Also be sure that you have
- * visited an applicable page with the user script enabled first.
- * 7. Change the value for any desired configuration options (change to true
- * or false).
- * 8. Click the "Save" button.
- * 9. Refresh or visit the page to see the changes. If it doesn't seem to be
- * working, refresh the TamperMonkey dashboard to double check your change
- * has stuck. If not try again and click the save button.
- *
- * Other user script managers:
- * 1. Change any of the options below directly in the code editor and save.
- * 2. Whenever you update this script or reinstall it you will have to make
- * your changes again.
- */
- const defaultConfig = {
- /** When true, adds the AniList average score to the header. */
- addAniListScoreToHeader: true,
- /** When true, adds the MyAnimeList score to the header. */
- addMyAnimeListScoreToHeader: true,
- /** When true, adds the Kitsu score to the header. */
- addKitsuScoreToHeader: false,
- /** When true, show the smile/neutral/frown icons next to the AniList score. */
- showIconWithAniListScore: true,
- /**
- * When true, show AniList's "Mean Score" instead of the "Average Score".
- * Regardless of this value, if the "Average Score" is not available
- * then the "Mean Score" will be shown.
- */
- preferAniListMeanScore: false,
- /** When true, shows loading indicators when scores are being retrieved. */
- showLoadingIndicators: true,
- };
- /**
- * Constants for this user script.
- */
- const constants = {
- /** Endpoint for the AniList API */
- ANI_LIST_API: 'https://graphql.anilist.co',
- /** Endpoint for the MyAnimeList API */
- MAL_API: 'https://api.jikan.moe/v4',
- /** Endpoint for the Kitsu API */
- KITSU_API: 'https://kitsu.io/api/edge',
- /** Regex to extract the page type and media id from a AniList url path */
- ANI_LIST_URL_PATH_REGEX: /(anime|manga)\/([0-9]+)/i,
- /** Prefix message for logs to the console */
- LOG_PREFIX: '[AniList Unlimited User Script]',
- /** Prefix for class names added to created elements (prevent conflicts) */
- CLASS_PREFIX: 'user-script-ani-list-unlimited',
- /** Title suffix added to created elements (for user information) */
- CUSTOM_ELEMENT_TITLE:
- '(this content was added by the ani-list-unlimited user script)',
- /** When true, output additional logs to the console */
- DEBUG: false,
- };
- /**
- * User script manager functions.
- *
- * Provides compatibility between Tampermonkey, Greasemonkey 4+, etc...
- */
- const userScriptAPI = (() => {
- const api = {};
- if (typeof GM_xmlhttpRequest !== 'undefined') {
- api.GM_xmlhttpRequest = GM_xmlhttpRequest;
- } else if (
- typeof GM !== 'undefined' &&
- typeof GM.xmlHttpRequest !== 'undefined'
- ) {
- api.GM_xmlhttpRequest = GM.xmlHttpRequest;
- }
- if (typeof GM_setValue !== 'undefined') {
- api.GM_setValue = GM_setValue;
- } else if (
- typeof GM !== 'undefined' &&
- typeof GM.setValue !== 'undefined'
- ) {
- api.GM_setValue = GM.setValue;
- }
- if (typeof GM_getValue !== 'undefined') {
- api.GM_getValue = GM_getValue;
- } else if (
- typeof GM !== 'undefined' &&
- typeof GM.getValue !== 'undefined'
- ) {
- api.GM_getValue = GM.getValue;
- }
- /** whether GM_xmlhttpRequest is supported. */
- api.supportsXHR = typeof api.GM_xmlhttpRequest !== 'undefined';
- /** whether GM_setValue and GM_getValue are supported. */
- api.supportsStorage =
- typeof api.GM_getValue !== 'undefined' &&
- typeof api.GM_setValue !== 'undefined';
- return api;
- })();
- /**
- * Utility functions.
- */
- const utils = {
- /**
- * Logs an error message to the console.
- *
- * @param {string} message - The error message.
- * @param {...any} additional - Additional values to log.
- */
- error(message, ...additional) {
- console.error(`${constants.LOG_PREFIX} Error: ${message}`, ...additional);
- },
- /**
- * Logs a group of related error messages to the console.
- *
- * @param {string} label - The group label.
- * @param {...any} additional - Additional error messages.
- */
- groupError(label, ...additional) {
- console.groupCollapsed(`${constants.LOG_PREFIX} Error: ${label}`);
- additional.forEach(entry => {
- console.log(entry);
- });
- console.groupEnd();
- },
- /**
- * Logs a debug message which only shows when constants.DEBUG = true.
- *
- * @param {string} message The message.
- * @param {...any} additional - ADditional values to log.
- */
- debug(message, ...additional) {
- if (constants.DEBUG) {
- console.debug(`${constants.LOG_PREFIX} ${message}`, ...additional);
- }
- },
- /**
- * Makes an XmlHttpRequest using the user script util.
- *
- * Common options include the following:
- *
- * - url (url endpoint, e.g., https://api.endpoint.com)
- * - method (e.g., GET or POST)
- * - headers (an object containing headers such as Content-Type)
- * - responseType (e.g., 'json')
- * - data (body data)
- *
- * See https://wiki.greasespot.net/GM.xmlHttpRequest for other options.
- *
- * If `options.responseType` is set then the response data is returned,
- * otherwise `responseText` is returned.
- *
- * @param {Object} options - The request options.
- *
- * @returns A Promise that resolves with the response or rejects on any
- * errors or status code other than 200.
- */
- xhr(options) {
- return new Promise((resolve, reject) => {
- const xhrOptions = Object.assign({}, options, {
- onabort: res => reject(res),
- ontimeout: res => reject(res),
- onerror: res => reject(res),
- onload: res => {
- if (res.status === 200) {
- if (options.responseType && res.response) {
- resolve(res.response);
- } else {
- resolve(res.responseText);
- }
- } else {
- reject(res);
- }
- },
- });
- userScriptAPI.GM_xmlhttpRequest(xhrOptions);
- });
- },
- /**
- * Waits for an element to load.
- *
- * @param {string} selector - Wait for the element matching this
- * selector to be found.
- * @param {Element} [container=document] - The root element for the
- * selector, defaults to `document`.
- * @param {number} [timeoutSecs=7] - The number of seconds to wait
- * before timing out.
- *
- * @returns {Promise<Element>} A Promise returning the DOM element, or a
- * rejection if a timeout occurred.
- */
- async waitForElement(selector, container = document, timeoutSecs = 7) {
- const element = container.querySelector(selector);
- if (element) {
- return Promise.resolve(element);
- }
- return new Promise((resolve, reject) => {
- const timeoutTime = Date.now() + timeoutSecs * 1000;
- const handler = () => {
- const element = document.querySelector(selector);
- if (element) {
- resolve(element);
- } else if (Date.now() > timeoutTime) {
- reject(new Error(`Timed out waiting for selector '${selector}'`));
- } else {
- setTimeout(handler, 100);
- }
- };
- setTimeout(handler, 1);
- });
- },
- /**
- * Loads user configuration from storage.
- *
- * @param {Object} defaultConfiguration - An object containing all of
- * the user configuration keys mapped to their default values. This
- * object will be used to set an initial value for any keys not currently
- * in storage.
- *
- * @param {Boolean} [setDefault=true] - When true, save the value from
- * defaultConfiguration for keys not present in storage for next time.
- * This lets the user edit the configuration more easily.
- *
- * @returns {Promise<Object>} A Promise returning an object that has the
- * config from storage, or an empty object if the storage APIs are not
- * defined.
- */
- async loadUserConfiguration(defaultConfiguration, setDefault = true) {
- if (!userScriptAPI.supportsStorage) {
- utils.debug('User configuration is not enabled');
- return {};
- }
- const userConfig = {};
- for (let [key, value] of Object.entries(defaultConfiguration)) {
- const userValue = await userScriptAPI.GM_getValue(key);
- // initialize any config values that haven't been set
- if (setDefault && userValue === undefined) {
- utils.debug(`setting default config value for ${key}: ${value}`);
- userScriptAPI.GM_setValue(key, value);
- } else {
- userConfig[key] = userValue;
- }
- }
- utils.debug('loaded user configuration from storage', userConfig);
- return userConfig;
- },
- };
- /**
- * Functions to make API calls.
- */
- const api = {
- /**
- * Loads data from the AniList API.
- *
- * @param {('anime'|'manga')} type - The type of media content.
- * @param {string} aniListId - The AniList media id.
- *
- * @returns {Promise<Object>} A Promise returning the media's data, or a
- * rejection if there was a problem calling the API.
- */
- async loadAniListData(type, aniListId) {
- var query = `
- query ($id: Int, $type: MediaType) {
- Media (id: $id, type: $type) {
- idMal
- averageScore
- meanScore
- title {
- english
- romaji
- }
- }
- }
- `;
- const variables = {
- id: aniListId,
- type: type.toUpperCase(),
- };
- try {
- const response = await utils.xhr({
- url: constants.ANI_LIST_API,
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- },
- responseType: 'json',
- data: JSON.stringify({
- query,
- variables,
- }),
- });
- utils.debug('AniList API response:', response);
- return response.data.Media;
- } catch (res) {
- const message = `AniList API request failed for media with ID '${aniListId}'`;
- utils.groupError(
- message,
- `Request failed with status ${res.status}`,
- ...(res.response ? res.response.errors : [res])
- );
- const error = new Error(message);
- error.response = res;
- throw error;
- }
- },
- /**
- * Loads data from the MyAnimeList API.
- *
- * @param {('anime'|'manga')} type - The type of media content.
- * @param {string} myAnimeListId - The MyAnimeList media id.
- *
- * @returns {Promise<Object>} A Promise returning the media's data, or a
- * rejection if there was a problem calling the API.
- */
- async loadMyAnimeListData(type, myAnimeListId) {
- try {
- const response = await utils.xhr({
- url: `${constants.MAL_API}/${type}/${myAnimeListId}`,
- method: 'GET',
- responseType: 'json',
- });
- utils.debug('MyAnimeList API response:', response);
- return response.data;
- } catch (res) {
- const message = `MyAnimeList API request failed for mapped MyAnimeList ID '${myAnimeListId}'`;
- utils.groupError(
- message,
- `Request failed with status ${res.status}`,
- res.response ? res.response.error || res.response.message : res
- );
- const error = new Error(message);
- error.response = res;
- throw error;
- }
- },
- /**
- * Loads data from the Kitsu API.
- *
- * @param {('anime'|'manga')} type - The type of media content.
- * @param {string} englishTitle - Search for media with this title.
- * @param {string} romajiTitle - Search for media with this title.
- *
- * @returns {Promise<Object>} A Promise returning the media's data, or a
- * rejection if there was a problem calling the API.
- */
- async loadKitsuData(type, englishTitle, romajiTitle) {
- try {
- const fields = 'slug,averageRating,userCount,titles';
- const response = await utils.xhr({
- url: encodeURI(
- `${
- constants.KITSU_API
- }/${type}?page[limit]=3&fields[${type}]=${fields}&filter[text]=${
- englishTitle || romajiTitle
- }`
- ),
- method: 'GET',
- headers: {
- Accept: 'application/vnd.api+json',
- 'Content-Type': 'application/vnd.api+json',
- },
- responseType: 'json',
- });
- utils.debug('Kitsu API response:', response);
- if (response.data && response.data.length) {
- let index = 0;
- let isExactMatch = false;
- const collator = new Intl.Collator({
- usage: 'search',
- sensitivity: 'base',
- ignorePunctuation: true,
- });
- const matchedIndex = response.data.findIndex(result => {
- return Object.values(result.attributes.titles).find(kitsuTitle => {
- return (
- collator.compare(englishTitle, kitsuTitle) === 0 ||
- collator.compare(romajiTitle, kitsuTitle) === 0
- );
- });
- });
- if (matchedIndex > -1) {
- utils.debug(
- `matched title for Kitsu result at index ${matchedIndex}`,
- response.data[index]
- );
- index = matchedIndex;
- isExactMatch = true;
- } else {
- utils.debug('exact title match not found in Kitsu results');
- }
- return {
- isExactMatch,
- data: response.data[index].attributes,
- };
- } else {
- utils.debug(`Kitsu API returned 0 results for '${englishTitle}'`);
- return {};
- }
- } catch (res) {
- const message = `Kitsu API request failed for text '${englishTitle}'`;
- utils.groupError(
- message,
- `Request failed with status ${res.status}`,
- ...(res.response ? res.response.errors : [])
- );
- const error = new Error(message);
- error.response = res;
- throw error;
- }
- },
- };
- /**
- * AniList SVGs.
- */
- const svg = {
- /** from AniList */
- smile:
- '<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="smile" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" color="rgb(var(--color-green))" class="icon svg-inline--fa fa-smile fa-w-16"><path fill="currentColor" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z" class=""></path></svg>',
- /** from AniList */
- straight:
- '<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="meh" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" color="rgb(var(--color-orange))" class="icon svg-inline--fa fa-meh fa-w-16"><path fill="currentColor" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm8 144H160c-13.2 0-24 10.8-24 24s10.8 24 24 24h176c13.2 0 24-10.8 24-24s-10.8-24-24-24z" class=""></path></svg>',
- /** from AniList */
- frown:
- '<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="frown" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" color="rgb(var(--color-red))" class="icon svg-inline--fa fa-frown fa-w-16"><path fill="currentColor" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm-80 128c-40.2 0-78 17.7-103.8 48.6-8.5 10.2-7.1 25.3 3.1 33.8 10.2 8.4 25.3 7.1 33.8-3.1 16.6-19.9 41-31.4 66.9-31.4s50.3 11.4 66.9 31.4c8.1 9.7 23.1 11.9 33.8 3.1 10.2-8.5 11.5-23.6 3.1-33.8C326 321.7 288.2 304 248 304z" class=""></path></svg>',
- /** From https://github.com/SamHerbert/SVG-Loaders */
- // License/accreditation https://github.com/SamHerbert/SVG-Loaders/blob/master/LICENSE.md
- loading:
- '<svg width="60" height="8" viewbox="0 0 130 32" style="fill: rgb(var(--color-text-light, 80%, 80%, 80%))" xmlns="http://www.w3.org/2000/svg" fill="#fff"><circle cx="15" cy="15" r="15"><animate attributeName="r" from="15" to="15" begin="0s" dur="0.8s" values="15;9;15" calcMode="linear" repeatCount="indefinite"/><animate attributeName="fill-opacity" from="1" to="1" begin="0s" dur="0.8s" values="1;.5;1" calcMode="linear" repeatCount="indefinite"/></circle><circle cx="60" cy="15" r="9" fill-opacity=".3"><animate attributeName="r" from="9" to="9" begin="0s" dur="0.8s" values="9;15;9" calcMode="linear" repeatCount="indefinite"/><animate attributeName="fill-opacity" from=".5" to=".5" begin="0s" dur="0.8s" values=".5;1;.5" calcMode="linear" repeatCount="indefinite"/></circle><circle cx="105" cy="15" r="15"><animate attributeName="r" from="15" to="15" begin="0s" dur="0.8s" values="15;9;15" calcMode="linear" repeatCount="indefinite"/><animate attributeName="fill-opacity" from="1" to="1" begin="0s" dur="0.8s" values="1;.5;1" calcMode="linear" repeatCount="indefinite"/></circle></svg>',
- };
- /**
- * Handles manipulating the current AniList page.
- */
- class AniListPage {
- /**
- * @param {Object} config - The user script configuration.
- */
- constructor(config) {
- this.selectors = {
- pageTitle: 'head > title',
- header: '.page-content .header .content',
- };
- this.config = config;
- this.lastCheckedUrlPath = null;
- }
- /**
- * Initialize the page and apply page modifications.
- */
- initialize() {
- utils.debug('initializing page');
- this.applyPageModifications().catch(e =>
- utils.error(`Unable to apply modifications to the page - ${e.message}`)
- );
- // eslint-disable-next-line no-unused-vars
- const observer = new MutationObserver((mutations, observer) => {
- utils.debug('mutation observer', mutations);
- this.applyPageModifications().catch(e =>
- utils.error(
- `Unable to apply modifications to the page - ${e.message}`
- )
- );
- });
- const target = document.querySelector(this.selectors.pageTitle);
- observer.observe(target, { childList: true, characterData: true });
- }
- /**
- * Applies modifications to the page based on config settings.
- *
- * This will only add content if we are on a relevant page in the app.
- */
- async applyPageModifications() {
- const pathname = window.location.pathname;
- utils.debug('checking page url', pathname);
- if (this.lastCheckedUrlPath === pathname) {
- utils.debug('url path did not change, skipping');
- return;
- }
- this.lastCheckedUrlPath = pathname;
- const matches = constants.ANI_LIST_URL_PATH_REGEX.exec(pathname);
- if (!matches) {
- utils.debug('url did not match');
- return;
- }
- const pageType = matches[1];
- const mediaId = matches[2];
- utils.debug('pageType:', pageType, 'mediaId:', mediaId);
- const aniListData = await api.loadAniListData(pageType, mediaId);
- if (this.config.addAniListScoreToHeader) {
- this.addAniListScoreToHeader(pageType, mediaId, aniListData);
- }
- if (this.config.addMyAnimeListScoreToHeader) {
- this.addMyAnimeListScoreToHeader(pageType, mediaId, aniListData);
- }
- if (this.config.addKitsuScoreToHeader) {
- this.addKitsuScoreToHeader(pageType, mediaId, aniListData);
- }
- }
- /**
- * Adds the AniList score to the header.
- *
- * @param {('anime'|'manga')} type - The type of media content.
- * @param {string} mediaId - The AniList media id.
- * @param {Object} aniListData - The data from the AniList api.
- */
- async addAniListScoreToHeader(pageType, mediaId, aniListData) {
- const slot = 1;
- const source = 'AniList';
- let rawScore, info;
- if (
- aniListData.meanScore &&
- (this.config.preferAniListMeanScore || !aniListData.averageScore)
- ) {
- rawScore = aniListData.meanScore;
- info = ' (mean)';
- } else if (aniListData.averageScore) {
- rawScore = aniListData.averageScore;
- info = ' (average)';
- }
- const score = rawScore ? `${rawScore}%` : '(N/A)';
- let iconMarkup;
- if (this.config.showIconWithAniListScore) {
- if (rawScore === null || rawScore == undefined) {
- iconMarkup = svg.straight;
- } else if (rawScore >= 75) {
- iconMarkup = svg.smile;
- } else if (rawScore >= 60) {
- iconMarkup = svg.straight;
- } else {
- iconMarkup = svg.frown;
- }
- }
- this.addToHeader({ slot, source, score, iconMarkup, info }).catch(e => {
- utils.error(
- `Unable to add the ${source} score to the header: ${e.message}`
- );
- });
- }
- /**
- * Adds the MyAnimeList score to the header.
- *
- * @param {('anime'|'manga')} type - The type of media content.
- * @param {string} mediaId - The AniList media id.
- * @param {Object} aniListData - The data from the AniList api.
- */
- async addMyAnimeListScoreToHeader(pageType, mediaId, aniListData) {
- const slot = 2;
- const source = 'MyAnimeList';
- if (!aniListData.idMal) {
- utils.error(`no ${source} id found for media ${mediaId}`);
- return this.clearHeaderSlot(slot);
- }
- if (this.config.showLoadingIndicators) {
- await this.showSlotLoading(slot);
- }
- api
- .loadMyAnimeListData(pageType, aniListData.idMal)
- .then(data => {
- const score = data.score;
- const href = data.url;
- return this.addToHeader({ slot, source, score, href });
- })
- .catch(e => {
- utils.error(
- `Unable to add the ${source} score to the header: ${e.message}`
- );
- // https://github.com/jikan-me/jikan-rest/issues/102
- if (e.response && e.response.status === 503) {
- return this.addToHeader({
- slot,
- source,
- score: 'Unavailable',
- info: ': The Jikan API is temporarily unavailable. Please try again later',
- });
- } else if (e.response && e.response.status === 429) {
- // rate limited
- return this.addToHeader({
- slot,
- source,
- score: 'Unavailable*',
- info: ': Temporarily unavailable due to rate-limiting, since you made too many requests to the MyAnimeList API. Reload in a few seconds to try again',
- });
- }
- });
- }
- /**
- * Adds the Kitsu score to the header.
- *
- * @param {('anime'|'manga')} type - The type of media content.
- * @param {string} mediaId - The AniList media id.
- * @param {Object} aniListData - The data from the AniList api.
- */
- async addKitsuScoreToHeader(pageType, mediaId, aniListData) {
- const slot = 3;
- const source = 'Kitsu';
- const englishTitle = aniListData.title.english;
- const romajiTitle = aniListData.title.romaji;
- if (!englishTitle && !romajiTitle) {
- utils.error(
- `Unable to search ${source} - no media title found for ${mediaId}`
- );
- return this.clearHeaderSlot(slot);
- }
- if (this.config.showLoadingIndicators) {
- await this.showSlotLoading(slot);
- }
- api
- .loadKitsuData(pageType, englishTitle, romajiTitle)
- .then(entry => {
- if (!entry.data) {
- utils.error(`no ${source} matches found for media ${mediaId}`);
- return this.clearHeaderSlot(slot);
- }
- const data = entry.data;
- let score = null;
- if (data.averageRating !== undefined && data.averageRating !== null) {
- score = `${data.averageRating}%`;
- if (!entry.isExactMatch) {
- score += '*';
- }
- }
- const href = `https://kitsu.io/${pageType}/${data.slug}`;
- let info = '';
- if (!entry.isExactMatch) {
- info += ', *exact match not found';
- }
- const kitsuTitles = Object.values(data.titles).join(', ');
- info += `, matched on "${kitsuTitles}"`;
- return this.addToHeader({ slot, source, score, href, info });
- })
- .catch(e => {
- utils.error(
- `Unable to add the ${source} score to the header: ${e.message}`
- );
- });
- }
- /**
- * Shows a loading indicator in the given slot position.
- *
- * @param {number} slot - The slot position.
- */
- async showSlotLoading(slot) {
- const slotEl = await this.getSlotElement(slot);
- if (slotEl) {
- slotEl.innerHTML = svg.loading;
- }
- }
- /**
- * Removes markup from the header for the given slot position.
- *
- * @param {number} slot - The slot position.
- */
- async clearHeaderSlot(slot) {
- const slotEl = await this.getSlotElement(slot);
- if (slotEl) {
- while (slotEl.lastChild) {
- slotEl.removeChild(slotEl.lastChild);
- }
- slotEl.style.marginRight = '0';
- }
- }
- /**
- * Add score data to a slot in the header section.
- *
- * @param {Object} info - Data about the score.
- * @param {number} info.slot - The ordering position within the header.
- * @param {string} info.source - The source of the data.
- * @param {string} [info.score] - The score text.
- * @param {string} [info.href] - The link for the media from the source.
- * @param {string} [info.iconMarkup] - Icon markup representing the score.
- * @param {string} [info=''] - Additional info about the score.
- */
- async addToHeader({ slot, source, score, href, iconMarkup, info = '' }) {
- const slotEl = await this.getSlotElement(slot);
- if (slotEl) {
- const newSlotEl = slotEl.cloneNode(false);
- newSlotEl.title = `${source} Score${info} ${constants.CUSTOM_ELEMENT_TITLE}`;
- newSlotEl.style.marginRight = '1rem';
- if (slot > 1) {
- newSlotEl.style.fontSize = '.875em';
- }
- if (iconMarkup) {
- newSlotEl.insertAdjacentHTML('afterbegin', iconMarkup);
- newSlotEl.firstElementChild.style.marginRight = '6px';
- }
- const scoreEl = document.createElement('span');
- if (slot > 1) {
- scoreEl.style.fontWeight = 'bold';
- }
- scoreEl.append(document.createTextNode(score || 'No Score'));
- newSlotEl.appendChild(scoreEl);
- if (href) {
- newSlotEl.appendChild(document.createTextNode(' on '));
- const link = document.createElement('a');
- link.href = href;
- link.title = `View this entry on ${source} ${constants.CUSTOM_ELEMENT_TITLE}`;
- link.textContent = source;
- newSlotEl.appendChild(link);
- }
- slotEl.replaceWith(newSlotEl);
- } else {
- throw new Error(`Unable to find element to place ${source} score`);
- }
- }
- /**
- * Gets the slot element at the given position.
- *
- * @param {number} slot - Get the slot element at this ordering position.
- */
- async getSlotElement(slot) {
- const containerEl = await this.getContainerElement();
- const slotClass = `${constants.CLASS_PREFIX}-slot${slot}`;
- return containerEl.querySelector(`.${slotClass}`);
- }
- /**
- * Gets the container for new content, adding it to the DOM if
- * necessary.
- */
- async getContainerElement() {
- const headerEl = await utils.waitForElement(this.selectors.header);
- const insertionPoint =
- headerEl.querySelector('h1') || headerEl.firstElementChild;
- const containerClass = `${constants.CLASS_PREFIX}-scores`;
- let containerEl = headerEl.querySelector(`.${containerClass}`);
- if (!containerEl) {
- containerEl = document.createElement('div');
- containerEl.className = containerClass;
- containerEl.style.display = 'flex';
- containerEl.style.marginTop = '1em';
- containerEl.style.alignItems = 'center';
- const numSlots = 3;
- for (let i = 0; i < numSlots; i++) {
- const slotEl = document.createElement('div');
- slotEl.className = `${constants.CLASS_PREFIX}-slot${i + 1}`;
- containerEl.appendChild(slotEl);
- }
- insertionPoint.insertAdjacentElement('afterend', containerEl);
- }
- return containerEl;
- }
- }
- // execution:
- // check for compatibility
- if (!userScriptAPI.supportsXHR) {
- utils.error(
- 'The current version of your user script manager ' +
- 'does not support required features. Please update ' +
- 'it to the latest version and try again.'
- );
- return;
- }
- // setup configuration
- const userConfig = await utils.loadUserConfiguration(defaultConfig);
- const config = Object.assign({}, defaultConfig, userConfig);
- utils.debug('configuration values:', config);
- const page = new AniListPage(config);
- page.initialize();
- })();