Greasy Fork is available in English.

IMDb with additional ratings

Adds additional ratings (TMDB, Douban, Metacritic, MyAnimeList). These can be deactivated individually in the configuration menu. And movie info can be copied by clicking unlinked elements below the title.

Versión del día 14/09/2024. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name           IMDb with additional ratings
// @description    Adds additional ratings (TMDB, Douban, Metacritic, MyAnimeList). These can be deactivated individually in the configuration menu. And movie info can be copied by clicking unlinked elements below the title.
// @version        20240914
// @author         mykarean
// @icon           https://icons.duckduckgo.com/ip2/imdb.com.ico
// @match          https://*.imdb.com/title/*
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @grant          GM_registerMenuCommand
// @run-at         document-start
// @compatible     chrome
// @license        GPL3
// @namespace https://greasyfork.org/users/1367334
// ==/UserScript==

"use strict";

// -----------------------------------------------------------------------------------------------------
// Config/Requirements
// -----------------------------------------------------------------------------------------------------

GM_registerMenuCommand("Configuration", openConfiguration, "c");

const ratingSources = ["TMDB", "Douban", "Metacritic", "MyAnimeList"];
const imdbId = window.location.pathname.match(/title\/(tt\d+)\//)[1];
const USER_AGENT = "Mozilla/5.0 (x64; rv) Gecko Firefox";
const undefinedValue = "X";
let local;

function getTitleElement() {
    return document.querySelector('[data-testid="hero__pageTitle"]');
}
function getMainTitle() {
    return getTitleElement()?.textContent;
}
function getOriginalTitle() {
    let originalTitle = document.querySelector('[data-testid="hero__pageTitle"] ~ div')?.textContent?.match(/^.*:\ (.*)/)?.[1];
    // Unicode normalisation and removal of diacritical characters to improve search on other pages
    return originalTitle?.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}

// -----------------------------------------------------------------------------------------------------
// General Functions
// -----------------------------------------------------------------------------------------------------

function addCss() {
    if (!document.getElementById("custom-css-style")) {
        GM_addStyle(`
            /* all Badges */
            [data-testid="hero-rating-bar__aggregate-rating"],
            .rating-bar__base-button > .ipc-btn {
                padding: 4px 3px;
                border-radius: 5px !important;
            }
            .rating-bar__base-button {
                margin-right: 0 !important;
            }

            /* added Badges */
            span[data-testid=hero-rating-bar__aggregate-rating] {
                margin: 0 3px;
                background-color: rgba(255, 255, 255, 0.08);
            }
            /* format rating content */
            span[data-testid=hero-rating-bar__aggregate-rating] .ipc-btn__text > div > div {
                 align-items: center;
            }
            /* remove /10 */
            span[data-testid=hero-rating-bar__aggregate-rating] div[data-testid=hero-rating-bar__aggregate-rating__score] > span:nth-child(2) {
                display: none;
            }
            span[data-testid=hero-rating-bar__aggregate-rating] div[data-testid=hero-rating-bar__aggregate-rating__score] > span:nth-child(1) {
                padding-right: 0;
            }

            /* IMDb Badge */
            div[data-testid=hero-rating-bar__aggregate-rating] {
                /* margin-left: 6px; */
            }

            /* Badge Header */
            .rating-bar__base-button > div {
                letter-spacing: unset;
            }
            span.rating-bar__base-button[myanimelist] > div {
                letter-spacing: normal;
            }
            
            /* for badges without rating data */
            .disable-anchor {
                cursor: default !important;
            }
            .disable-anchor:before {
                background: unset !important;
            }
        `).setAttribute("id", "custom-css-style");
    }
    const imdbRatingName = document.querySelector('div[data-testid="hero-rating-bar__aggregate-rating"] > div');
    if (imdbRatingName) {
        imdbRatingName.textContent = "IMDb";
    }
}

// create the initial rating template
function createRatingBadgeTemplate(ratingSource) {
    const ratingElementImdb = document.querySelector('div[data-testid="hero-rating-bar__aggregate-rating"]');

    // ignore if the rating badge has already been created
    if (!ratingElementImdb || document.querySelector(`span.rating-bar__base-button[${ratingSource}]`)) return null;

    let clonedRatingBadge = ratingElementImdb.cloneNode(true);
    clonedRatingBadge.setAttribute(ratingSource, "");
    clonedRatingBadge.childNodes[0].innerText = ratingSource;

    // disable link per default
    clonedRatingBadge.querySelector("a").removeAttribute("href");
    // clonedRatingBadge.querySelector("a").classList.add("disable-anchor");

    const updateRatingElement = (element, rating, voteCount) => {
        let imdbRatingElement = element.querySelector("div[data-testid=hero-rating-bar__aggregate-rating__score]");
        if (imdbRatingElement) {
            imdbRatingElement.querySelector("span").innerText = rating;
            imdbRatingElement.nextSibling.nextSibling.innerText = voteCount;
        }
    };

    if (ratingSource === "Metacritic") {
        const criticRatingElement = clonedRatingBadge.querySelector(
            "div[data-testid=hero-rating-bar__aggregate-rating__score]"
        )?.parentElement;

        if (!criticRatingElement) {
            console.error("Critic rating element not found");
            return;
        }

        // Critic rating: replace star svg element with critic rating by cloning the rating element
        let criticRating = clonedRatingBadge
            .querySelector("div[data-testid=hero-rating-bar__aggregate-rating__score]")
            .parentElement.cloneNode(true);
        criticRating.classList.add("critic-rating");
        clonedRatingBadge
            .querySelector("div[data-testid=hero-rating-bar__aggregate-rating__score]")
            .parentElement.classList.add("user-rating");

        //  critic rating
        updateRatingElement(criticRating, undefinedValue, undefinedValue.toLowerCase());
        criticRating.title = "Metascore";
        criticRating.style.cssText = `
        background-color: rgba(255, 255, 255, 0.1);
        padding-left: 4px;
        padding-right: 2px;
        margin-right: 4px;
        border-radius: 5px;
        `;
        clonedRatingBadge.querySelector("a > span > div > div").outerHTML = criticRating.outerHTML;

        // user rating
        updateRatingElement(clonedRatingBadge.querySelector(".user-rating"), undefinedValue, undefinedValue.toLowerCase());
        clonedRatingBadge.querySelector(".user-rating").title = "User Score";
        clonedRatingBadge.querySelector(".user-rating").style.paddingRight = "0";
    } else {
        updateRatingElement(clonedRatingBadge, undefinedValue, undefinedValue.toLowerCase());
        clonedRatingBadge.querySelector("a > span > div > div").remove();
        clonedRatingBadge.querySelector(".ipc-btn__text > div > div").style.paddingRight = "0";
    }

    // convert div to span element, otherwise it will be removed from IMDb scripts
    const ratingElement = document.createElement("span");
    // Transfer all attributes from the cloned div element to the new span element
    for (let attr of clonedRatingBadge.attributes) {
        ratingElement.setAttribute(attr.name, attr.value);
    }
    // transfer the content of the cloned IMDb rating element to the new span element
    ratingElement.innerHTML = clonedRatingBadge.innerHTML;

    ratingElementImdb.insertAdjacentElement("beforebegin", ratingElement);
    return ratingElement;
}

// update the rating template with actual data
function updateRatingTemplate(newRatingBadge, ratingData) {
    if (!newRatingBadge || !ratingData) return;

    const selectors = {
        url: "a",
        generalRating: "div[data-testid=hero-rating-bar__aggregate-rating__score] > span",
        generalVoteCount: "div[data-testid=hero-rating-bar__aggregate-rating__score] + * + *",
        criticRating: ".critic-rating div[data-testid=hero-rating-bar__aggregate-rating__score] > span",
        criticVoteCount: ".critic-rating div[data-testid=hero-rating-bar__aggregate-rating__score] + * + *",
        userRating: ".user-rating div[data-testid=hero-rating-bar__aggregate-rating__score] > span",
        userVoteCount: ".user-rating div[data-testid=hero-rating-bar__aggregate-rating__score] + * + *",
    };

    function updateElement(selector, value, voteCount = 0) {
        const element = newRatingBadge.querySelector(selector);

        if (!voteCount) {
            element.textContent = value !== undefined && value !== 0 ? value : undefinedValue;
        } else {
            element.textContent = value !== undefined && value !== 0 ? value : undefinedValue.toLowerCase();
        }
    }

    newRatingBadge.querySelector(selectors.url).href = ratingData.url;
    // newRatingBadge.querySelector(selectors.url).classList.remove("disable-anchor");

    if (ratingData.source === "Metacritic") {
        updateElement(selectors.criticRating, ratingData.criticRating);
        updateElement(selectors.userRating, ratingData.userRating);
        updateElement(selectors.criticVoteCount, ratingData.criticVoteCount, 1);
        updateElement(selectors.userVoteCount, ratingData.userVoteCount, 1);
    } else {
        updateElement(selectors.generalRating, ratingData.rating);
        updateElement(selectors.generalVoteCount, ratingData.voteCount, 1);
    }
}

// -----------------------------------------------------------------------------------------------------
// TMDB
// -----------------------------------------------------------------------------------------------------

let tmdbDataPromise = null;
async function getTmdbData() {
    const configured = await GM_getValue("TMDB", false);
    if (!configured) return;

    if (tmdbDataPromise) return tmdbDataPromise;

    if (!imdbId) {
        throw new Error("IMDb ID not found in URL.");
    }

    const options = {
        method: "GET",
        headers: {
            accept: "application/json",
            Authorization:
                "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyMzc1ZGIzOTYwYWVhMWI1OTA1NWMwZmM3ZDcwYjYwZiIsInN1YiI6IjYwYmNhZTk0NGE0YmY2MDA1OWJhNWE1ZSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.DU51juQWlAIIfZ2lK99b3zi-c5vgc4jAwVz5h2WjOP8",
        },
    };

    tmdbDataPromise = fetch(`https://api.themoviedb.org/3/find/${imdbId}?external_source=imdb_id`, options)
        .then((response) => {
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return response.json();
        })
        .then((data) => {
            const result = data.movie_results[0] || data.tv_results[0];
            if (!result) {
                throw new Error("No data found for the provided IMDb ID.");
            }

            console.log("TMDB: ", result);
            return {
                source: "TMDB",
                id: result.id,
                rating: (Math.round(result.vote_average * 10) / 10).toLocaleString(local, {
                    minimumFractionDigits: 1,
                    maximumFractionDigits: 1,
                }),
                voteCount: result.vote_count?.toLocaleString(local),
                url: `https://www.themoviedb.org/${result.media_type}/${result.id}`,
            };
        })
        .catch((error) => {
            console.error("Error fetching TMDb data:", error);
            return 0;
        });

    return tmdbDataPromise;
}

async function addTmdbRatingBadge() {
    const configured = await GM_getValue("TMDB", false);
    if (!configured) return;

    const newRatingBadge = createRatingBadgeTemplate("TMDB");

    // if the template for the rating badge was not created, it already exists
    if (!newRatingBadge) return;

    const ratingData = await getTmdbData();

    // Copy ratingData to avoid modifying the original object
    let finalRatingData = { ...ratingData };

    // Check if ratingData or ratingData.url is undefined and provide a default value
    if (!ratingData?.url) {
        const searchTitle = getOriginalTitle() ?? getMainTitle();
        const defaultUrl = `https://www.themoviedb.org/search?query=${searchTitle}`;
        finalRatingData.url = defaultUrl;
    }

    updateRatingTemplate(newRatingBadge, finalRatingData);
}

// -----------------------------------------------------------------------------------------------------
// Douban
// -----------------------------------------------------------------------------------------------------

let doubanDataPromise = null;
async function getDoubanData() {
    const configured = await GM_getValue("Douban", false);
    if (!configured) return;

    if (doubanDataPromise) return doubanDataPromise;

    if (!imdbId) {
        throw new Error("IMDb ID not found in URL.");
    }

    const fetchFromDouban = (url, method = "GET", data = null) =>
        new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method,
                url,
                data,
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded; charset=utf8",
                },
                onload: (response) => {
                    if (response.status >= 200 && response.status < 400) {
                        resolve(JSON.parse(response.responseText));
                    } else {
                        console.error(`Error getting ${url}:`, response.status, response.statusText, response.responseText);
                        resolve(null);
                    }
                },
                onerror: (error) => {
                    console.error(`Error during GM.xmlHttpRequest to ${url}:`, error.statusText);
                    reject(error);
                },
            });
        });

    const getDoubanInfo = async (imdbId) => {
        const data = await fetchFromDouban(
            `https://api.douban.com/v2/movie/imdb/${imdbId}`,
            "POST",
            "apikey=0ac44ae016490db2204ce0a042db2916"
        );
        if (data && data.alt && data.alt !== "N/A") {
            const url = data.alt.replace("/movie/", "/subject/") + "/";
            return { url, rating: data.rating, title: data.title };
        }
    };

    doubanDataPromise = (async function () {
        try {
            const result = await getDoubanInfo(imdbId);
            if (!result) {
                throw new Error("No data found for the provided IMDb ID.");
            }

            console.log("Douban: ", result);
            return {
                source: "Douban",
                id: result.id,
                rating: Number(result.rating.average).toLocaleString(local, { minimumFractionDigits: 1, maximumFractionDigits: 1 }),
                voteCount: result.rating.numRaters?.toLocaleString(local),
                url: result.url,
            };
        } catch (error) {
            console.error("Error fetching Douban data:", error);
            return 0;
        }
    })();

    return doubanDataPromise;
}

async function addDoubanRatingBadge() {
    const configured = await GM_getValue("Douban", false);
    if (!configured) return;

    const newRatingBadge = createRatingBadgeTemplate("Douban");

    // if the template for the rating badge was not created, it already exists
    if (!newRatingBadge) return;

    const ratingData = await getDoubanData();

    // Copy ratingData to avoid modifying the original object
    let finalRatingData = { ...ratingData };

    // Check if ratingData or ratingData.url is undefined and provide a default value
    if (!ratingData?.url) {
        const searchTitle = getOriginalTitle() ?? getMainTitle();
        const defaultUrl = `https://search.douban.com/movie/subject_search?search_text=${searchTitle}`;
        finalRatingData.url = defaultUrl;
    }

    updateRatingTemplate(newRatingBadge, finalRatingData);
}

// -----------------------------------------------------------------------------------------------------
// Metacritic
// -----------------------------------------------------------------------------------------------------
// wikidata solution inspired by IMDb Scout Mod

let metacriticDataPromise = null;
async function getMetacriticData() {
    const configured = await GM_getValue("Metacritic", false);
    if (!configured) return;

    if (metacriticDataPromise) return metacriticDataPromise;

    if (!imdbId) {
        throw new Error("IMDb ID not found in URL.");
    }

    async function getMetacriticId() {
        return new Promise((resolve) => {
            GM.xmlHttpRequest({
                method: "GET",
                timeout: 10000,
                headers: { "User-Agent": USER_AGENT },
                url: `https://query.wikidata.org/sparql?format=json&query=SELECT * WHERE {?s wdt:P345 "${imdbId}". OPTIONAL { ?s wdt:P1712 ?Metacritic_ID. }}`,
                onload: function (response) {
                    const result = JSON.parse(response.responseText);
                    const bindings = result.results.bindings[0];
                    const metacriticId = bindings && bindings.Metacritic_ID ? bindings.Metacritic_ID.value : "";
                    resolve(metacriticId);
                },
                onerror: function () {
                    console.error("getMetacriticId: Request Error.");
                    reject("Request Error");
                },
                onabort: function () {
                    console.error("getMetacriticId: Request Aborted.");
                    reject("Request Abort");
                },
                ontimeout: function () {
                    console.error("getMetacriticId: Request Timeout.");
                    reject("Request Timeout");
                },
            });
        });
    }

    function fetchMetacriticData(url) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: "GET",
                url: url,
                headers: { "User-Agent": USER_AGENT },
                onload: function (response) {
                    const parser = new DOMParser();
                    const result = parser.parseFromString(response.responseText, "text/html");

                    let criticRating;
                    let userRating;
                    let criticVoteCount;
                    let userVoteCount;

                    const criticRatingElement = result.querySelector(".c-siteReviewScore");
                    if (criticRatingElement) {
                        const ratingText = criticRatingElement.textContent.trim();
                        criticRating = ratingText.includes(".") ? "" : !isNaN(ratingText) ? ratingText : 0;

                        if (criticRating !== 0) {
                            let criticVoteCountText = result
                                .querySelector(".c-siteReviewScore")
                                .parentElement.parentElement.parentElement.querySelector("a > span")?.textContent;
                            criticVoteCount = criticVoteCountText.match(/\d+/)[0];
                        } else {
                            criticVoteCount = 0;
                        }
                    }

                    const userRatingElement = result.querySelector(".c-siteReviewScore_user");
                    if (userRatingElement) {
                        const ratingText = userRatingElement.textContent.trim();
                        userRating = Number(ratingText).toLocaleString(local, { minimumFractionDigits: 1, maximumFractionDigits: 1 });

                        // if (!isNaN(ratingText)) userRating = ratingText * 10;

                        if (userRating !== 0) {
                            let userVoteCountText = result
                                .querySelector(".c-siteReviewScore_user")
                                .parentElement.parentElement.parentElement.querySelector("a > span")?.textContent;
                            userVoteCount = userVoteCountText.match(/\d+/)[0];
                        } else {
                            userVoteCount = 0;
                        }
                    }

                    console.log(
                        "Critic rating: " +
                            criticRating +
                            ", User rating: " +
                            userRating +
                            ", Url: " +
                            url +
                            ", criticVoteCount: " +
                            criticVoteCount +
                            ", userVoteCount: " +
                            userVoteCount
                    );

                    // Resolve the promise with the ratings and URL
                    resolve({
                        source: "Metacritic",
                        criticRating: criticRating,
                        userRating: userRating,
                        criticVoteCount: criticVoteCount,
                        userVoteCount: userVoteCount,
                        url: url,
                    });
                },
                onerror: function () {
                    console.log("getMetacriticRatings: Request Error.");
                    reject("Request Error");
                },
                onabort: function () {
                    console.log("getMetacriticRatings: Request is aborted.");
                    reject("Request Aborted");
                },
                ontimeout: function () {
                    console.log("getMetacriticRatings: Request timed out.");
                    reject("Request Timed Out");
                },
            });
        });
    }

    metacriticDataPromise = (async () => {
        const metacriticId = await getMetacriticId();
        const url = encodeURI(`https://www.metacritic.com/${metacriticId}`);

        if (metacriticId !== "") {
            return fetchMetacriticData(url);
        }
    })();

    return 0;
}

async function addMetacriticRatingBadge() {
    const configured = await GM_getValue("Metacritic", false);
    if (!configured) return;

    const newRatingBadge = createRatingBadgeTemplate("Metacritic");

    // if the template for the rating badge was not created, it already exists
    if (!newRatingBadge) return;

    const ratingData = await getMetacriticData();

    // Copy ratingData to avoid modifying the original object
    let finalRatingData = { ...ratingData };

    // Check if ratingData or ratingData.url is undefined and provide a default value
    if (!ratingData?.url) {
        const searchTitle = getOriginalTitle() ?? getMainTitle();
        const defaultUrl = `https://www.metacritic.com/search/${searchTitle}`;
        finalRatingData.url = defaultUrl;
    }

    updateRatingTemplate(newRatingBadge, finalRatingData);
}

// -----------------------------------------------------------------------------------------------------
// MyAnimeList
// -----------------------------------------------------------------------------------------------------
// wikidata solution inspired by IMDb Scout Mod

let myAnimeListDataPromise = null;
async function getMyAnimeListDataByImdbId() {
    // only if genre is anime
    const genreAnime = document.querySelector(".ipc-chip-list__scroller")?.textContent.includes("Anime");
    if (!genreAnime) return;

    // only if enabled in settings
    const configured = await GM_getValue("MyAnimeList", false);
    if (!configured) return;

    if (myAnimeListDataPromise) return myAnimeListDataPromise;

    function getAnimeID() {
        const url = `https://query.wikidata.org/sparql?format=json&query=SELECT * WHERE {?s wdt:P345 "${imdbId}". OPTIONAL {?s wdt:P4086 ?MyAnimeList_ID.} OPTIONAL {?s wdt:P8729 ?AniList_ID.}}`;

        return new Promise((resolve) => {
            GM.xmlHttpRequest({
                method: "GET",
                timeout: 10000,
                url: url,
                headers: { "User-Agent": USER_AGENT },
                onload: function (response) {
                    const result = JSON.parse(response.responseText);
                    let myAnimeListId = "";
                    let aniListId = "";
                    if (result.results.bindings[0] !== undefined) {
                        if (result.results.bindings[0].MyAnimeList_ID !== undefined) {
                            myAnimeListId = result.results.bindings[0].MyAnimeList_ID.value;
                        } else {
                            console.log("getAnimeID: No MyAnimeList_ID found on wikidata.org");
                        }
                        if (result.results.bindings[0].AniList_ID !== undefined) {
                            aniListId = result.results.bindings[0].AniList_ID.value;
                        }
                        console.log("getAnimeID: ", result.results);
                        resolve([myAnimeListId, aniListId]);
                    }
                },
                onerror: function () {
                    console.log("getAnimeID: Request Error.");
                    reject("Request Error");
                },
                onabort: function () {
                    console.log("getAnimeID: Request Abort.");
                    reject("Request Abort");
                },
                ontimeout: function () {
                    console.log("getAnimeID: Request Timeout.");
                    reject("Request Timeout");
                },
            });
        });
    }

    function fetchMyAnimeListData(myAnimeListId) {
        return new Promise((resolve, reject) => {
            const url = "https://api.jikan.moe/v4/anime/" + myAnimeListId;
            GM.xmlHttpRequest({
                method: "GET",
                timeout: 10000,
                url: url,
                headers: { "User-Agent": USER_AGENT },
                onload: function (response) {
                    if (response.status === 200) {
                        const result = JSON.parse(response.responseText);
                        const rating = result.data.score;
                        if (!isNaN(rating) && rating > 0) {
                            console.log("fetchMyAnimeListData: ", result.data.mal_id, result);

                            resolve({
                                source: "MyAnimeList",
                                rating: parseFloat((rating || 0).toFixed(1)).toLocaleString(local),
                                voteCount: result.data.scored_by?.toLocaleString(local),
                                url: result.data.url,
                            });
                        } else {
                            reject("Invalid rating");
                        }
                    } else {
                        console.log("MyAnimeList: HTTP Error: " + response.status);
                        reject("HTTP Error");
                    }
                },
                onerror: function () {
                    console.log("MyAnimeList: Request Error.");
                    reject("Request Error");
                },
                onabort: function () {
                    console.log("MyAnimeList: Request is aborted.");
                    reject("Request Aborted");
                },
                ontimeout: function () {
                    console.log("MyAnimeList: Request timed out.");
                    reject("Request Timeout");
                },
            });
        });
    }

    myAnimeListDataPromise = (async () => {
        const id = await getAnimeID();
        const myAnimeListId = id[0];
        const aniListId = id[1];

        if (myAnimeListId !== "") {
            return fetchMyAnimeListData(myAnimeListId);
        }
    })();

    return 0;
}

async function getMyAnimeListDataByTitle() {
    const titleElement = getTitleElement();
    if (!titleElement) return;

    // only if genre is anime
    const genreAnime = document.querySelector(".ipc-chip-list__scroller")?.textContent.includes("Anime");
    if (!genreAnime) return;

    const mainTitle = getMainTitle();
    const originalTitle = getOriginalTitle();

    const metaData = titleElement?.parentElement?.querySelector("ul");
    const metaItems = metaData?.querySelectorAll("li");
    // If the text content type is an integer, it is a tv show, otherwise it is a movie.
    const type = isNaN(metaItems?.[0]?.textContent) ? "tv" : "movie";
    const yearIndex = type === "tv" ? 1 : 0;
    const yearText = metaItems?.[yearIndex]?.textContent;
    // Extract the first number up to the non-number sign and convert it into a integer (2018-2020)
    const year = parseInt(yearText);

    async function fetchAllPages(searchTitle) {
        let currentPage = 1;
        let allResults = [];
        const maxRetries = 3;
        const retryDelay = 1000; // 5 seconds

        async function fetchWithRetry(url, retries = 0) {
            try {
                const response = await fetch(url);
                if (response.status === 429) {
                    if (retries < maxRetries) {
                        console.log(`Rate limited. Retrying in ${retryDelay / 1000} seconds...`);
                        await new Promise((resolve) => setTimeout(resolve, retryDelay));
                        return fetchWithRetry(url, retries + 1);
                    } else {
                        throw new Error("Max retries reached. Please try again later.");
                    }
                }
                return response;
            } catch (error) {
                console.error("Fetch error:", error);
                throw error;
            }
        }

        while (true) {
            try {
                const response = await fetchWithRetry(
                    `https://api.jikan.moe/v4/anime?q=${encodeURIComponent(searchTitle)}&type=${type}&page=${currentPage}`
                );
                const data = await response.json();
                allResults = allResults.concat(data.data);
                if (!data.pagination.has_next_page) break;
                currentPage++;
            } catch (error) {
                console.error("Error fetching data:", error);
                break;
            }
        }
        // console.log(searchTitle, year, type, allResults);

        const result = allResults.find((anime, index) => {
            const titleMatch = anime.title.toLowerCase().includes(searchTitle.toLowerCase());
            const yearMatch = anime.aired.prop.from.year === year;

            if (titleMatch && yearMatch) {
                // console.log(`Title and year match found for anime[${index}] - ${anime.title}`);
                return true;
            }

            if (!titleMatch && anime.title_english) {
                const englishTitleMatch = anime.title_english.toLowerCase().includes(searchTitle.toLowerCase());
                // console.log(`English title match for "${anime.title_english}": ${englishTitleMatch}`);

                if (englishTitleMatch && yearMatch) {
                    // console.log(`English title and year match found for anime[${index}] - ${anime.title}`);
                    return true;
                }
            }

            if (!titleMatch && anime.title_synonyms && anime.title_synonyms.length > 0) {
                // console.log(`Checking synonyms for anime[${index}] - ${anime.title}, Synonyms: ${anime.title_synonyms}`);

                const synonymMatch = anime.title_synonyms.some((synonym) => synonym.toLowerCase().includes(searchTitle.toLowerCase()));

                // console.log(`Synonym match for anime[${index}] - ${anime.title}: ${synonymMatch}`);

                if (synonymMatch && yearMatch) {
                    // console.log(`Synonym and year match found for anime[${index}] - ${anime.title}`);
                    return true;
                }
            }

            // console.log(`No match found for anime[${index}] - ${anime.title}`);
            return false;
        });
        return result;
    }

    async function getAnimeData() {
        try {
            let result = await fetchAllPages(mainTitle);

            if (!result && originalTitle) {
                console.log(`No results found for "${mainTitle}", retrying with originalTitle "${originalTitle}"...`);
                result = await fetchAllPages(originalTitle);
            }

            if (result) {
                console.log("getMyAnimeListDataByTitle: ", result);
                return result;
            } else {
                console.log("No results found for either title.");
                return null;
            }
        } catch (error) {
            console.error("Error retrieving data:", error);
            return null;
        }
    }

    myAnimeListDataPromise = (async () => {
        const anime = await getAnimeData();

        if (anime) {
            const data = {
                source: "MyAnimeList",
                rating: Number(anime.score).toLocaleString(local, { minimumFractionDigits: 1, maximumFractionDigits: 1 }),
                voteCount: anime.scored_by?.toLocaleString(local),
                url: anime.url,
            };

            // console.log("myAnimeListDataPromise: ", anime);

            return data;
        } else {
            console.log("No anime data found.");
            return null;
        }
    })();
}

async function addMyAnimeListRatingBadge() {
    // only if genre is anime
    const genreAnime = document.querySelector(".ipc-chip-list__scroller")?.textContent.includes("Anime");
    if (!genreAnime) return;

    // only if enabled in settings
    const configured = await GM_getValue("MyAnimeList", false);
    if (!configured) return;

    const newRatingBadge = createRatingBadgeTemplate("MyAnimeList");

    // if the template for the rating badge was not created, it already exists
    if (!newRatingBadge) return;

    let ratingData = await getMyAnimeListDataByImdbId();
    if (ratingData === 0) {
        ratingData = await getMyAnimeListDataByTitle();
    }

    // Copy ratingData to avoid modifying the original object
    let finalRatingData = { ...ratingData };

    // Check if ratingData or ratingData.url is undefined and provide a default value
    if (!ratingData?.url) {
        const searchTitle = getOriginalTitle() ?? getMainTitle();
        const defaultUrl = `https://myanimelist.net/anime.php?q=${searchTitle}`; // Define your default URL here
        finalRatingData.url = defaultUrl;
    }

    updateRatingTemplate(newRatingBadge, finalRatingData);
}

// -----------------------------------------------------------------------------------------------------

async function addDdl() {
    const authorsMode = await GM_getValue("authorsMode", false);
    if (!authorsMode) return;

    const targetElement = document.querySelector("[data-testid=hero__pageTitle]");

    if (!document.querySelector("a#ddl-button") && targetElement) {
        let ddlElement = document.createElement("a");
        ddlElement.id = "ddl-button";
        ddlElement.href = `https://ddl-warez.cc/?s=${imdbId}`;
        ddlElement.style.float = "right";

        let imgElement = document.createElement("img");
        imgElement.src = "https://ddl-warez.cc/wp-content/uploads/logo.png";
        imgElement.style.height = "48px";
        imgElement.style.aspectRatio = "1/1";

        ddlElement.appendChild(imgElement);

        targetElement.insertAdjacentElement("afterend", ddlElement);
    }
}

let metadataAsText = "";
function collectMetadataForClipboard() {
    let title = document.querySelector("span.hero__primary-text")?.textContent;
    let genres = document.querySelector("div[data-testid='interests'] div.ipc-chip-list__scroller")?.childNodes;
    let additionalMetadata = document.querySelector('[data-testid="hero__pageTitle"]')?.parentElement?.querySelector("ul");

    // if click listener does not exist
    if (!document.querySelector("ul.collectMetadataForClipboardListener") && title && genres && additionalMetadata) {
        if (genres && additionalMetadata) {
            if (metadataAsText === "") {
                // add title
                metadataAsText += title + " | ";
                // collect additional metadata
                for (let element of additionalMetadata?.childNodes) metadataAsText += element.textContent + " | ";
                // collect genres
                let iteration = genres?.length;
                for (let genre of genres) {
                    metadataAsText += genre.textContent;

                    // append "," as long as not last iteration
                    if (--iteration) metadataAsText += ", ";
                }
            }

            additionalMetadata.style.cursor = "pointer";
            additionalMetadata.addEventListener("click", function () {
                navigator.clipboard.writeText(metadataAsText);
            });

            // to know if click listener is still there
            additionalMetadata.classList.add("collectMetadataForClipboardListener");
        }
    }
}

// Configuration Modal
function openConfiguration() {
    GM_addStyle(`
    .modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 9998;
        transition: background-color 0.5s ease;
    }
    
    .modal {
        font-family: var(--ipt-font-family);
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 300px;
        padding: 20px;
        background-color: #fff;
        border-radius: 10px;
        box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
        z-index: 9999;
        opacity: 0;
        transition: opacity 0.5s ease;
    }
    
    .modal-title {
        margin-bottom: 20px;
        font-size: 16px;
        font-weight: bold;
    }
    
    .checkbox-label {
        display: block;
        margin-bottom: 10px;
    }
    
    .close-button {
        display: block;
        margin: 20px auto 0;
    }
    `);

    // Darken background
    const overlay = document.createElement("div");
    overlay.className = "modal-overlay";
    overlay.style.backgroundColor = "rgba(0, 0, 0, 0)";
    setTimeout(() => {
        overlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
    }, 50);

    // Create modal
    const modal = document.createElement("div");
    modal.className = "modal";
    setTimeout(() => {
        modal.style.opacity = "1";
    }, 50);

    // Title of the modal
    const title = document.createElement("h3");
    title.innerText = "Which ratings should be displayed?";
    title.className = "modal-title";
    modal.appendChild(title);

    // Add checkboxes
    ratingSources.forEach((ratingSource) => {
        const label = document.createElement("label");
        label.className = "checkbox-label";

        const checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.checked = GM_getValue(ratingSource, false);

        checkbox.addEventListener("change", () => {
            GM_setValue(ratingSource, checkbox.checked);
            if (!checkbox.checked) {
                document.querySelector(`span.rating-bar__base-button[${ratingSource}]`).remove();
            } else {
                // trigger observer to add new badges
                const tempElement = document.createElement("div");
                document.body.appendChild(tempElement);
                document.body.removeChild(tempElement);
            }
        });

        label.appendChild(checkbox);
        label.appendChild(document.createTextNode(` ${ratingSource}`));
        modal.appendChild(label);
    });

    // Add button to close
    const closeButton = document.createElement("button");
    closeButton.innerText = "Close";
    closeButton.className =
        "close-button ipc-btn ipc-btn--half-padding ipc-btn--default-height ipc-btn--core-accent1 ipc-btn--theme-baseAlt ";

    closeButton.addEventListener("click", () => {
        document.body.removeChild(overlay);
        document.body.removeChild(modal);
    });

    modal.appendChild(closeButton);

    // Add modal and overlay to the DOM
    document.body.appendChild(overlay);
    document.body.appendChild(modal);

    // Close modal on click outside
    overlay.addEventListener("click", () => {
        document.body.removeChild(overlay);
        document.body.removeChild(modal);
    });
}

// add and keep elements in header container
async function main() {
    // set default configuration
    ratingSources.forEach((badge) => {
        // Query default value (if does not exist, 'null' is returned)
        const existingValue = GM_getValue(badge, null);
        if (existingValue === null) {
            // Value does not exist, set default value to true
            GM_setValue(badge, true);
        }
    });

    // ignore episode view
    if (!document.title.includes('"')) {
        addCss();
        getTmdbData();
        getDoubanData();
        getMetacriticData();
        getMyAnimeListDataByImdbId();
        getMyAnimeListDataByTitle();

        const observer = new MutationObserver(async () => {
            addCss();
            await addMyAnimeListRatingBadge();
            await addMetacriticRatingBadge();
            await addDoubanRatingBadge();
            await addTmdbRatingBadge();

            addDdl();
            // addGenresToTitle();
            collectMetadataForClipboard();
        });

        observer.observe(document.documentElement, { childList: true, subtree: true });
    }
}

// -----------------------------------------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------------------------------------

main();
// GM_setValue("authorsMode", true);