// ==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);