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.

Από την 14/09/2024. Δείτε την τελευταία έκδοση.

  1. // ==UserScript==
  2. // @name IMDb with additional ratings
  3. // @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.
  4. // @version 20240914
  5. // @author mykarean
  6. // @icon https://icons.duckduckgo.com/ip2/imdb.com.ico
  7. // @match https://*.imdb.com/title/*
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_addStyle
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_registerMenuCommand
  13. // @run-at document-start
  14. // @compatible chrome
  15. // @license GPL3
  16. // @namespace https://greasyfork.org/users/1367334
  17. // ==/UserScript==
  18.  
  19. "use strict";
  20.  
  21. // -----------------------------------------------------------------------------------------------------
  22. // Config/Requirements
  23. // -----------------------------------------------------------------------------------------------------
  24.  
  25. GM_registerMenuCommand("Configuration", openConfiguration, "c");
  26.  
  27. const ratingSources = ["TMDB", "Douban", "Metacritic", "MyAnimeList"];
  28. const imdbId = window.location.pathname.match(/title\/(tt\d+)\//)[1];
  29. const USER_AGENT = "Mozilla/5.0 (x64; rv) Gecko Firefox";
  30. const undefinedValue = "X";
  31. let local;
  32.  
  33. function getTitleElement() {
  34. return document.querySelector('[data-testid="hero__pageTitle"]');
  35. }
  36. function getMainTitle() {
  37. return getTitleElement()?.textContent;
  38. }
  39. function getOriginalTitle() {
  40. let originalTitle = document.querySelector('[data-testid="hero__pageTitle"] ~ div')?.textContent?.match(/^.*:\ (.*)/)?.[1];
  41. // Unicode normalisation and removal of diacritical characters to improve search on other pages
  42. return originalTitle?.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
  43. }
  44.  
  45. // -----------------------------------------------------------------------------------------------------
  46. // General Functions
  47. // -----------------------------------------------------------------------------------------------------
  48.  
  49. function addCss() {
  50. if (!document.getElementById("custom-css-style")) {
  51. GM_addStyle(`
  52. /* all Badges */
  53. [data-testid="hero-rating-bar__aggregate-rating"],
  54. .rating-bar__base-button > .ipc-btn {
  55. padding: 4px 3px;
  56. border-radius: 5px !important;
  57. }
  58. .rating-bar__base-button {
  59. margin-right: 0 !important;
  60. }
  61.  
  62. /* added Badges */
  63. span[data-testid=hero-rating-bar__aggregate-rating] {
  64. margin: 0 3px;
  65. background-color: rgba(255, 255, 255, 0.08);
  66. }
  67. /* format rating content */
  68. span[data-testid=hero-rating-bar__aggregate-rating] .ipc-btn__text > div > div {
  69. align-items: center;
  70. }
  71. /* remove /10 */
  72. span[data-testid=hero-rating-bar__aggregate-rating] div[data-testid=hero-rating-bar__aggregate-rating__score] > span:nth-child(2) {
  73. display: none;
  74. }
  75. span[data-testid=hero-rating-bar__aggregate-rating] div[data-testid=hero-rating-bar__aggregate-rating__score] > span:nth-child(1) {
  76. padding-right: 0;
  77. }
  78.  
  79. /* IMDb Badge */
  80. div[data-testid=hero-rating-bar__aggregate-rating] {
  81. /* margin-left: 6px; */
  82. }
  83.  
  84. /* Badge Header */
  85. .rating-bar__base-button > div {
  86. letter-spacing: unset;
  87. }
  88. span.rating-bar__base-button[myanimelist] > div {
  89. letter-spacing: normal;
  90. }
  91. /* for badges without rating data */
  92. .disable-anchor {
  93. cursor: default !important;
  94. }
  95. .disable-anchor:before {
  96. background: unset !important;
  97. }
  98. `).setAttribute("id", "custom-css-style");
  99. }
  100. const imdbRatingName = document.querySelector('div[data-testid="hero-rating-bar__aggregate-rating"] > div');
  101. if (imdbRatingName) {
  102. imdbRatingName.textContent = "IMDb";
  103. }
  104. }
  105.  
  106. // create the initial rating template
  107. function createRatingBadgeTemplate(ratingSource) {
  108. const ratingElementImdb = document.querySelector('div[data-testid="hero-rating-bar__aggregate-rating"]');
  109.  
  110. // ignore if the rating badge has already been created
  111. if (!ratingElementImdb || document.querySelector(`span.rating-bar__base-button[${ratingSource}]`)) return null;
  112.  
  113. let clonedRatingBadge = ratingElementImdb.cloneNode(true);
  114. clonedRatingBadge.setAttribute(ratingSource, "");
  115. clonedRatingBadge.childNodes[0].innerText = ratingSource;
  116.  
  117. // disable link per default
  118. clonedRatingBadge.querySelector("a").removeAttribute("href");
  119. // clonedRatingBadge.querySelector("a").classList.add("disable-anchor");
  120.  
  121. const updateRatingElement = (element, rating, voteCount) => {
  122. let imdbRatingElement = element.querySelector("div[data-testid=hero-rating-bar__aggregate-rating__score]");
  123. if (imdbRatingElement) {
  124. imdbRatingElement.querySelector("span").innerText = rating;
  125. imdbRatingElement.nextSibling.nextSibling.innerText = voteCount;
  126. }
  127. };
  128.  
  129. if (ratingSource === "Metacritic") {
  130. const criticRatingElement = clonedRatingBadge.querySelector(
  131. "div[data-testid=hero-rating-bar__aggregate-rating__score]"
  132. )?.parentElement;
  133.  
  134. if (!criticRatingElement) {
  135. console.error("Critic rating element not found");
  136. return;
  137. }
  138.  
  139. // Critic rating: replace star svg element with critic rating by cloning the rating element
  140. let criticRating = clonedRatingBadge
  141. .querySelector("div[data-testid=hero-rating-bar__aggregate-rating__score]")
  142. .parentElement.cloneNode(true);
  143. criticRating.classList.add("critic-rating");
  144. clonedRatingBadge
  145. .querySelector("div[data-testid=hero-rating-bar__aggregate-rating__score]")
  146. .parentElement.classList.add("user-rating");
  147.  
  148. // critic rating
  149. updateRatingElement(criticRating, undefinedValue, undefinedValue.toLowerCase());
  150. criticRating.title = "Metascore";
  151. criticRating.style.cssText = `
  152. background-color: rgba(255, 255, 255, 0.1);
  153. padding-left: 4px;
  154. padding-right: 2px;
  155. margin-right: 4px;
  156. border-radius: 5px;
  157. `;
  158. clonedRatingBadge.querySelector("a > span > div > div").outerHTML = criticRating.outerHTML;
  159.  
  160. // user rating
  161. updateRatingElement(clonedRatingBadge.querySelector(".user-rating"), undefinedValue, undefinedValue.toLowerCase());
  162. clonedRatingBadge.querySelector(".user-rating").title = "User Score";
  163. clonedRatingBadge.querySelector(".user-rating").style.paddingRight = "0";
  164. } else {
  165. updateRatingElement(clonedRatingBadge, undefinedValue, undefinedValue.toLowerCase());
  166. clonedRatingBadge.querySelector("a > span > div > div").remove();
  167. clonedRatingBadge.querySelector(".ipc-btn__text > div > div").style.paddingRight = "0";
  168. }
  169.  
  170. // convert div to span element, otherwise it will be removed from IMDb scripts
  171. const ratingElement = document.createElement("span");
  172. // Transfer all attributes from the cloned div element to the new span element
  173. for (let attr of clonedRatingBadge.attributes) {
  174. ratingElement.setAttribute(attr.name, attr.value);
  175. }
  176. // transfer the content of the cloned IMDb rating element to the new span element
  177. ratingElement.innerHTML = clonedRatingBadge.innerHTML;
  178.  
  179. ratingElementImdb.insertAdjacentElement("beforebegin", ratingElement);
  180. return ratingElement;
  181. }
  182.  
  183. // update the rating template with actual data
  184. function updateRatingTemplate(newRatingBadge, ratingData) {
  185. if (!newRatingBadge || !ratingData) return;
  186.  
  187. const selectors = {
  188. url: "a",
  189. generalRating: "div[data-testid=hero-rating-bar__aggregate-rating__score] > span",
  190. generalVoteCount: "div[data-testid=hero-rating-bar__aggregate-rating__score] + * + *",
  191. criticRating: ".critic-rating div[data-testid=hero-rating-bar__aggregate-rating__score] > span",
  192. criticVoteCount: ".critic-rating div[data-testid=hero-rating-bar__aggregate-rating__score] + * + *",
  193. userRating: ".user-rating div[data-testid=hero-rating-bar__aggregate-rating__score] > span",
  194. userVoteCount: ".user-rating div[data-testid=hero-rating-bar__aggregate-rating__score] + * + *",
  195. };
  196.  
  197. function updateElement(selector, value, voteCount = 0) {
  198. const element = newRatingBadge.querySelector(selector);
  199.  
  200. if (!voteCount) {
  201. element.textContent = value !== undefined && value !== 0 ? value : undefinedValue;
  202. } else {
  203. element.textContent = value !== undefined && value !== 0 ? value : undefinedValue.toLowerCase();
  204. }
  205. }
  206.  
  207. newRatingBadge.querySelector(selectors.url).href = ratingData.url;
  208. // newRatingBadge.querySelector(selectors.url).classList.remove("disable-anchor");
  209.  
  210. if (ratingData.source === "Metacritic") {
  211. updateElement(selectors.criticRating, ratingData.criticRating);
  212. updateElement(selectors.userRating, ratingData.userRating);
  213. updateElement(selectors.criticVoteCount, ratingData.criticVoteCount, 1);
  214. updateElement(selectors.userVoteCount, ratingData.userVoteCount, 1);
  215. } else {
  216. updateElement(selectors.generalRating, ratingData.rating);
  217. updateElement(selectors.generalVoteCount, ratingData.voteCount, 1);
  218. }
  219. }
  220.  
  221. // -----------------------------------------------------------------------------------------------------
  222. // TMDB
  223. // -----------------------------------------------------------------------------------------------------
  224.  
  225. let tmdbDataPromise = null;
  226. async function getTmdbData() {
  227. const configured = await GM_getValue("TMDB", false);
  228. if (!configured) return;
  229.  
  230. if (tmdbDataPromise) return tmdbDataPromise;
  231.  
  232. if (!imdbId) {
  233. throw new Error("IMDb ID not found in URL.");
  234. }
  235.  
  236. const options = {
  237. method: "GET",
  238. headers: {
  239. accept: "application/json",
  240. Authorization:
  241. "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyMzc1ZGIzOTYwYWVhMWI1OTA1NWMwZmM3ZDcwYjYwZiIsInN1YiI6IjYwYmNhZTk0NGE0YmY2MDA1OWJhNWE1ZSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.DU51juQWlAIIfZ2lK99b3zi-c5vgc4jAwVz5h2WjOP8",
  242. },
  243. };
  244.  
  245. tmdbDataPromise = fetch(`https://api.themoviedb.org/3/find/${imdbId}?external_source=imdb_id`, options)
  246. .then((response) => {
  247. if (!response.ok) {
  248. throw new Error(`HTTP error! status: ${response.status}`);
  249. }
  250. return response.json();
  251. })
  252. .then((data) => {
  253. const result = data.movie_results[0] || data.tv_results[0];
  254. if (!result) {
  255. throw new Error("No data found for the provided IMDb ID.");
  256. }
  257.  
  258. console.log("TMDB: ", result);
  259. return {
  260. source: "TMDB",
  261. id: result.id,
  262. rating: (Math.round(result.vote_average * 10) / 10).toLocaleString(local, {
  263. minimumFractionDigits: 1,
  264. maximumFractionDigits: 1,
  265. }),
  266. voteCount: result.vote_count?.toLocaleString(local),
  267. url: `https://www.themoviedb.org/${result.media_type}/${result.id}`,
  268. };
  269. })
  270. .catch((error) => {
  271. console.error("Error fetching TMDb data:", error);
  272. return 0;
  273. });
  274.  
  275. return tmdbDataPromise;
  276. }
  277.  
  278. async function addTmdbRatingBadge() {
  279. const configured = await GM_getValue("TMDB", false);
  280. if (!configured) return;
  281.  
  282. const newRatingBadge = createRatingBadgeTemplate("TMDB");
  283.  
  284. // if the template for the rating badge was not created, it already exists
  285. if (!newRatingBadge) return;
  286.  
  287. const ratingData = await getTmdbData();
  288.  
  289. // Copy ratingData to avoid modifying the original object
  290. let finalRatingData = { ...ratingData };
  291.  
  292. // Check if ratingData or ratingData.url is undefined and provide a default value
  293. if (!ratingData?.url) {
  294. const searchTitle = getOriginalTitle() ?? getMainTitle();
  295. const defaultUrl = `https://www.themoviedb.org/search?query=${searchTitle}`;
  296. finalRatingData.url = defaultUrl;
  297. }
  298.  
  299. updateRatingTemplate(newRatingBadge, finalRatingData);
  300. }
  301.  
  302. // -----------------------------------------------------------------------------------------------------
  303. // Douban
  304. // -----------------------------------------------------------------------------------------------------
  305.  
  306. let doubanDataPromise = null;
  307. async function getDoubanData() {
  308. const configured = await GM_getValue("Douban", false);
  309. if (!configured) return;
  310.  
  311. if (doubanDataPromise) return doubanDataPromise;
  312.  
  313. if (!imdbId) {
  314. throw new Error("IMDb ID not found in URL.");
  315. }
  316.  
  317. const fetchFromDouban = (url, method = "GET", data = null) =>
  318. new Promise((resolve, reject) => {
  319. GM.xmlHttpRequest({
  320. method,
  321. url,
  322. data,
  323. headers: {
  324. "Content-Type": "application/x-www-form-urlencoded; charset=utf8",
  325. },
  326. onload: (response) => {
  327. if (response.status >= 200 && response.status < 400) {
  328. resolve(JSON.parse(response.responseText));
  329. } else {
  330. console.error(`Error getting ${url}:`, response.status, response.statusText, response.responseText);
  331. resolve(null);
  332. }
  333. },
  334. onerror: (error) => {
  335. console.error(`Error during GM.xmlHttpRequest to ${url}:`, error.statusText);
  336. reject(error);
  337. },
  338. });
  339. });
  340.  
  341. const getDoubanInfo = async (imdbId) => {
  342. const data = await fetchFromDouban(
  343. `https://api.douban.com/v2/movie/imdb/${imdbId}`,
  344. "POST",
  345. "apikey=0ac44ae016490db2204ce0a042db2916"
  346. );
  347. if (data && data.alt && data.alt !== "N/A") {
  348. const url = data.alt.replace("/movie/", "/subject/") + "/";
  349. return { url, rating: data.rating, title: data.title };
  350. }
  351. };
  352.  
  353. doubanDataPromise = (async function () {
  354. try {
  355. const result = await getDoubanInfo(imdbId);
  356. if (!result) {
  357. throw new Error("No data found for the provided IMDb ID.");
  358. }
  359.  
  360. console.log("Douban: ", result);
  361. return {
  362. source: "Douban",
  363. id: result.id,
  364. rating: Number(result.rating.average).toLocaleString(local, { minimumFractionDigits: 1, maximumFractionDigits: 1 }),
  365. voteCount: result.rating.numRaters?.toLocaleString(local),
  366. url: result.url,
  367. };
  368. } catch (error) {
  369. console.error("Error fetching Douban data:", error);
  370. return 0;
  371. }
  372. })();
  373.  
  374. return doubanDataPromise;
  375. }
  376.  
  377. async function addDoubanRatingBadge() {
  378. const configured = await GM_getValue("Douban", false);
  379. if (!configured) return;
  380.  
  381. const newRatingBadge = createRatingBadgeTemplate("Douban");
  382.  
  383. // if the template for the rating badge was not created, it already exists
  384. if (!newRatingBadge) return;
  385.  
  386. const ratingData = await getDoubanData();
  387.  
  388. // Copy ratingData to avoid modifying the original object
  389. let finalRatingData = { ...ratingData };
  390.  
  391. // Check if ratingData or ratingData.url is undefined and provide a default value
  392. if (!ratingData?.url) {
  393. const searchTitle = getOriginalTitle() ?? getMainTitle();
  394. const defaultUrl = `https://search.douban.com/movie/subject_search?search_text=${searchTitle}`;
  395. finalRatingData.url = defaultUrl;
  396. }
  397.  
  398. updateRatingTemplate(newRatingBadge, finalRatingData);
  399. }
  400.  
  401. // -----------------------------------------------------------------------------------------------------
  402. // Metacritic
  403. // -----------------------------------------------------------------------------------------------------
  404. // wikidata solution inspired by IMDb Scout Mod
  405.  
  406. let metacriticDataPromise = null;
  407. async function getMetacriticData() {
  408. const configured = await GM_getValue("Metacritic", false);
  409. if (!configured) return;
  410.  
  411. if (metacriticDataPromise) return metacriticDataPromise;
  412.  
  413. if (!imdbId) {
  414. throw new Error("IMDb ID not found in URL.");
  415. }
  416.  
  417. async function getMetacriticId() {
  418. return new Promise((resolve) => {
  419. GM.xmlHttpRequest({
  420. method: "GET",
  421. timeout: 10000,
  422. headers: { "User-Agent": USER_AGENT },
  423. url: `https://query.wikidata.org/sparql?format=json&query=SELECT * WHERE {?s wdt:P345 "${imdbId}". OPTIONAL { ?s wdt:P1712 ?Metacritic_ID. }}`,
  424. onload: function (response) {
  425. const result = JSON.parse(response.responseText);
  426. const bindings = result.results.bindings[0];
  427. const metacriticId = bindings && bindings.Metacritic_ID ? bindings.Metacritic_ID.value : "";
  428. resolve(metacriticId);
  429. },
  430. onerror: function () {
  431. console.error("getMetacriticId: Request Error.");
  432. reject("Request Error");
  433. },
  434. onabort: function () {
  435. console.error("getMetacriticId: Request Aborted.");
  436. reject("Request Abort");
  437. },
  438. ontimeout: function () {
  439. console.error("getMetacriticId: Request Timeout.");
  440. reject("Request Timeout");
  441. },
  442. });
  443. });
  444. }
  445.  
  446. function fetchMetacriticData(url) {
  447. return new Promise((resolve, reject) => {
  448. GM.xmlHttpRequest({
  449. method: "GET",
  450. url: url,
  451. headers: { "User-Agent": USER_AGENT },
  452. onload: function (response) {
  453. const parser = new DOMParser();
  454. const result = parser.parseFromString(response.responseText, "text/html");
  455.  
  456. let criticRating;
  457. let userRating;
  458. let criticVoteCount;
  459. let userVoteCount;
  460.  
  461. const criticRatingElement = result.querySelector(".c-siteReviewScore");
  462. if (criticRatingElement) {
  463. const ratingText = criticRatingElement.textContent.trim();
  464. criticRating = ratingText.includes(".") ? "" : !isNaN(ratingText) ? ratingText : 0;
  465.  
  466. if (criticRating !== 0) {
  467. let criticVoteCountText = result
  468. .querySelector(".c-siteReviewScore")
  469. .parentElement.parentElement.parentElement.querySelector("a > span")?.textContent;
  470. criticVoteCount = criticVoteCountText.match(/\d+/)[0];
  471. } else {
  472. criticVoteCount = 0;
  473. }
  474. }
  475.  
  476. const userRatingElement = result.querySelector(".c-siteReviewScore_user");
  477. if (userRatingElement) {
  478. const ratingText = userRatingElement.textContent.trim();
  479. userRating = Number(ratingText).toLocaleString(local, { minimumFractionDigits: 1, maximumFractionDigits: 1 });
  480.  
  481. // if (!isNaN(ratingText)) userRating = ratingText * 10;
  482.  
  483. if (userRating !== 0) {
  484. let userVoteCountText = result
  485. .querySelector(".c-siteReviewScore_user")
  486. .parentElement.parentElement.parentElement.querySelector("a > span")?.textContent;
  487. userVoteCount = userVoteCountText.match(/\d+/)[0];
  488. } else {
  489. userVoteCount = 0;
  490. }
  491. }
  492.  
  493. console.log(
  494. "Critic rating: " +
  495. criticRating +
  496. ", User rating: " +
  497. userRating +
  498. ", Url: " +
  499. url +
  500. ", criticVoteCount: " +
  501. criticVoteCount +
  502. ", userVoteCount: " +
  503. userVoteCount
  504. );
  505.  
  506. // Resolve the promise with the ratings and URL
  507. resolve({
  508. source: "Metacritic",
  509. criticRating: criticRating,
  510. userRating: userRating,
  511. criticVoteCount: criticVoteCount,
  512. userVoteCount: userVoteCount,
  513. url: url,
  514. });
  515. },
  516. onerror: function () {
  517. console.log("getMetacriticRatings: Request Error.");
  518. reject("Request Error");
  519. },
  520. onabort: function () {
  521. console.log("getMetacriticRatings: Request is aborted.");
  522. reject("Request Aborted");
  523. },
  524. ontimeout: function () {
  525. console.log("getMetacriticRatings: Request timed out.");
  526. reject("Request Timed Out");
  527. },
  528. });
  529. });
  530. }
  531.  
  532. metacriticDataPromise = (async () => {
  533. const metacriticId = await getMetacriticId();
  534. const url = encodeURI(`https://www.metacritic.com/${metacriticId}`);
  535.  
  536. if (metacriticId !== "") {
  537. return fetchMetacriticData(url);
  538. }
  539. })();
  540.  
  541. return 0;
  542. }
  543.  
  544. async function addMetacriticRatingBadge() {
  545. const configured = await GM_getValue("Metacritic", false);
  546. if (!configured) return;
  547.  
  548. const newRatingBadge = createRatingBadgeTemplate("Metacritic");
  549.  
  550. // if the template for the rating badge was not created, it already exists
  551. if (!newRatingBadge) return;
  552.  
  553. const ratingData = await getMetacriticData();
  554.  
  555. // Copy ratingData to avoid modifying the original object
  556. let finalRatingData = { ...ratingData };
  557.  
  558. // Check if ratingData or ratingData.url is undefined and provide a default value
  559. if (!ratingData?.url) {
  560. const searchTitle = getOriginalTitle() ?? getMainTitle();
  561. const defaultUrl = `https://www.metacritic.com/search/${searchTitle}`;
  562. finalRatingData.url = defaultUrl;
  563. }
  564.  
  565. updateRatingTemplate(newRatingBadge, finalRatingData);
  566. }
  567.  
  568. // -----------------------------------------------------------------------------------------------------
  569. // MyAnimeList
  570. // -----------------------------------------------------------------------------------------------------
  571. // wikidata solution inspired by IMDb Scout Mod
  572.  
  573. let myAnimeListDataPromise = null;
  574. async function getMyAnimeListDataByImdbId() {
  575. // only if genre is anime
  576. const genreAnime = document.querySelector(".ipc-chip-list__scroller")?.textContent.includes("Anime");
  577. if (!genreAnime) return;
  578.  
  579. // only if enabled in settings
  580. const configured = await GM_getValue("MyAnimeList", false);
  581. if (!configured) return;
  582.  
  583. if (myAnimeListDataPromise) return myAnimeListDataPromise;
  584.  
  585. function getAnimeID() {
  586. 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.}}`;
  587.  
  588. return new Promise((resolve) => {
  589. GM.xmlHttpRequest({
  590. method: "GET",
  591. timeout: 10000,
  592. url: url,
  593. headers: { "User-Agent": USER_AGENT },
  594. onload: function (response) {
  595. const result = JSON.parse(response.responseText);
  596. let myAnimeListId = "";
  597. let aniListId = "";
  598. if (result.results.bindings[0] !== undefined) {
  599. if (result.results.bindings[0].MyAnimeList_ID !== undefined) {
  600. myAnimeListId = result.results.bindings[0].MyAnimeList_ID.value;
  601. } else {
  602. console.log("getAnimeID: No MyAnimeList_ID found on wikidata.org");
  603. }
  604. if (result.results.bindings[0].AniList_ID !== undefined) {
  605. aniListId = result.results.bindings[0].AniList_ID.value;
  606. }
  607. console.log("getAnimeID: ", result.results);
  608. resolve([myAnimeListId, aniListId]);
  609. }
  610. },
  611. onerror: function () {
  612. console.log("getAnimeID: Request Error.");
  613. reject("Request Error");
  614. },
  615. onabort: function () {
  616. console.log("getAnimeID: Request Abort.");
  617. reject("Request Abort");
  618. },
  619. ontimeout: function () {
  620. console.log("getAnimeID: Request Timeout.");
  621. reject("Request Timeout");
  622. },
  623. });
  624. });
  625. }
  626.  
  627. function fetchMyAnimeListData(myAnimeListId) {
  628. return new Promise((resolve, reject) => {
  629. const url = "https://api.jikan.moe/v4/anime/" + myAnimeListId;
  630. GM.xmlHttpRequest({
  631. method: "GET",
  632. timeout: 10000,
  633. url: url,
  634. headers: { "User-Agent": USER_AGENT },
  635. onload: function (response) {
  636. if (response.status === 200) {
  637. const result = JSON.parse(response.responseText);
  638. const rating = result.data.score;
  639. if (!isNaN(rating) && rating > 0) {
  640. console.log("fetchMyAnimeListData: ", result.data.mal_id, result);
  641.  
  642. resolve({
  643. source: "MyAnimeList",
  644. rating: parseFloat((rating || 0).toFixed(1)).toLocaleString(local),
  645. voteCount: result.data.scored_by?.toLocaleString(local),
  646. url: result.data.url,
  647. });
  648. } else {
  649. reject("Invalid rating");
  650. }
  651. } else {
  652. console.log("MyAnimeList: HTTP Error: " + response.status);
  653. reject("HTTP Error");
  654. }
  655. },
  656. onerror: function () {
  657. console.log("MyAnimeList: Request Error.");
  658. reject("Request Error");
  659. },
  660. onabort: function () {
  661. console.log("MyAnimeList: Request is aborted.");
  662. reject("Request Aborted");
  663. },
  664. ontimeout: function () {
  665. console.log("MyAnimeList: Request timed out.");
  666. reject("Request Timeout");
  667. },
  668. });
  669. });
  670. }
  671.  
  672. myAnimeListDataPromise = (async () => {
  673. const id = await getAnimeID();
  674. const myAnimeListId = id[0];
  675. const aniListId = id[1];
  676.  
  677. if (myAnimeListId !== "") {
  678. return fetchMyAnimeListData(myAnimeListId);
  679. }
  680. })();
  681.  
  682. return 0;
  683. }
  684.  
  685. async function getMyAnimeListDataByTitle() {
  686. const titleElement = getTitleElement();
  687. if (!titleElement) return;
  688.  
  689. // only if genre is anime
  690. const genreAnime = document.querySelector(".ipc-chip-list__scroller")?.textContent.includes("Anime");
  691. if (!genreAnime) return;
  692.  
  693. const mainTitle = getMainTitle();
  694. const originalTitle = getOriginalTitle();
  695.  
  696. const metaData = titleElement?.parentElement?.querySelector("ul");
  697. const metaItems = metaData?.querySelectorAll("li");
  698. // If the text content type is an integer, it is a tv show, otherwise it is a movie.
  699. const type = isNaN(metaItems?.[0]?.textContent) ? "tv" : "movie";
  700. const yearIndex = type === "tv" ? 1 : 0;
  701. const yearText = metaItems?.[yearIndex]?.textContent;
  702. // Extract the first number up to the non-number sign and convert it into a integer (2018-2020)
  703. const year = parseInt(yearText);
  704.  
  705. async function fetchAllPages(searchTitle) {
  706. let currentPage = 1;
  707. let allResults = [];
  708. const maxRetries = 3;
  709. const retryDelay = 1000; // 5 seconds
  710.  
  711. async function fetchWithRetry(url, retries = 0) {
  712. try {
  713. const response = await fetch(url);
  714. if (response.status === 429) {
  715. if (retries < maxRetries) {
  716. console.log(`Rate limited. Retrying in ${retryDelay / 1000} seconds...`);
  717. await new Promise((resolve) => setTimeout(resolve, retryDelay));
  718. return fetchWithRetry(url, retries + 1);
  719. } else {
  720. throw new Error("Max retries reached. Please try again later.");
  721. }
  722. }
  723. return response;
  724. } catch (error) {
  725. console.error("Fetch error:", error);
  726. throw error;
  727. }
  728. }
  729.  
  730. while (true) {
  731. try {
  732. const response = await fetchWithRetry(
  733. `https://api.jikan.moe/v4/anime?q=${encodeURIComponent(searchTitle)}&type=${type}&page=${currentPage}`
  734. );
  735. const data = await response.json();
  736. allResults = allResults.concat(data.data);
  737. if (!data.pagination.has_next_page) break;
  738. currentPage++;
  739. } catch (error) {
  740. console.error("Error fetching data:", error);
  741. break;
  742. }
  743. }
  744. // console.log(searchTitle, year, type, allResults);
  745.  
  746. const result = allResults.find((anime, index) => {
  747. const titleMatch = anime.title.toLowerCase().includes(searchTitle.toLowerCase());
  748. const yearMatch = anime.aired.prop.from.year === year;
  749.  
  750. if (titleMatch && yearMatch) {
  751. // console.log(`Title and year match found for anime[${index}] - ${anime.title}`);
  752. return true;
  753. }
  754.  
  755. if (!titleMatch && anime.title_english) {
  756. const englishTitleMatch = anime.title_english.toLowerCase().includes(searchTitle.toLowerCase());
  757. // console.log(`English title match for "${anime.title_english}": ${englishTitleMatch}`);
  758.  
  759. if (englishTitleMatch && yearMatch) {
  760. // console.log(`English title and year match found for anime[${index}] - ${anime.title}`);
  761. return true;
  762. }
  763. }
  764.  
  765. if (!titleMatch && anime.title_synonyms && anime.title_synonyms.length > 0) {
  766. // console.log(`Checking synonyms for anime[${index}] - ${anime.title}, Synonyms: ${anime.title_synonyms}`);
  767.  
  768. const synonymMatch = anime.title_synonyms.some((synonym) => synonym.toLowerCase().includes(searchTitle.toLowerCase()));
  769.  
  770. // console.log(`Synonym match for anime[${index}] - ${anime.title}: ${synonymMatch}`);
  771.  
  772. if (synonymMatch && yearMatch) {
  773. // console.log(`Synonym and year match found for anime[${index}] - ${anime.title}`);
  774. return true;
  775. }
  776. }
  777.  
  778. // console.log(`No match found for anime[${index}] - ${anime.title}`);
  779. return false;
  780. });
  781. return result;
  782. }
  783.  
  784. async function getAnimeData() {
  785. try {
  786. let result = await fetchAllPages(mainTitle);
  787.  
  788. if (!result && originalTitle) {
  789. console.log(`No results found for "${mainTitle}", retrying with originalTitle "${originalTitle}"...`);
  790. result = await fetchAllPages(originalTitle);
  791. }
  792.  
  793. if (result) {
  794. console.log("getMyAnimeListDataByTitle: ", result);
  795. return result;
  796. } else {
  797. console.log("No results found for either title.");
  798. return null;
  799. }
  800. } catch (error) {
  801. console.error("Error retrieving data:", error);
  802. return null;
  803. }
  804. }
  805.  
  806. myAnimeListDataPromise = (async () => {
  807. const anime = await getAnimeData();
  808.  
  809. if (anime) {
  810. const data = {
  811. source: "MyAnimeList",
  812. rating: Number(anime.score).toLocaleString(local, { minimumFractionDigits: 1, maximumFractionDigits: 1 }),
  813. voteCount: anime.scored_by?.toLocaleString(local),
  814. url: anime.url,
  815. };
  816.  
  817. // console.log("myAnimeListDataPromise: ", anime);
  818.  
  819. return data;
  820. } else {
  821. console.log("No anime data found.");
  822. return null;
  823. }
  824. })();
  825. }
  826.  
  827. async function addMyAnimeListRatingBadge() {
  828. // only if genre is anime
  829. const genreAnime = document.querySelector(".ipc-chip-list__scroller")?.textContent.includes("Anime");
  830. if (!genreAnime) return;
  831.  
  832. // only if enabled in settings
  833. const configured = await GM_getValue("MyAnimeList", false);
  834. if (!configured) return;
  835.  
  836. const newRatingBadge = createRatingBadgeTemplate("MyAnimeList");
  837.  
  838. // if the template for the rating badge was not created, it already exists
  839. if (!newRatingBadge) return;
  840.  
  841. let ratingData = await getMyAnimeListDataByImdbId();
  842. if (ratingData === 0) {
  843. ratingData = await getMyAnimeListDataByTitle();
  844. }
  845.  
  846. // Copy ratingData to avoid modifying the original object
  847. let finalRatingData = { ...ratingData };
  848.  
  849. // Check if ratingData or ratingData.url is undefined and provide a default value
  850. if (!ratingData?.url) {
  851. const searchTitle = getOriginalTitle() ?? getMainTitle();
  852. const defaultUrl = `https://myanimelist.net/anime.php?q=${searchTitle}`; // Define your default URL here
  853. finalRatingData.url = defaultUrl;
  854. }
  855.  
  856. updateRatingTemplate(newRatingBadge, finalRatingData);
  857. }
  858.  
  859. // -----------------------------------------------------------------------------------------------------
  860.  
  861. async function addDdl() {
  862. const authorsMode = await GM_getValue("authorsMode", false);
  863. if (!authorsMode) return;
  864.  
  865. const targetElement = document.querySelector("[data-testid=hero__pageTitle]");
  866.  
  867. if (!document.querySelector("a#ddl-button") && targetElement) {
  868. let ddlElement = document.createElement("a");
  869. ddlElement.id = "ddl-button";
  870. ddlElement.href = `https://ddl-warez.cc/?s=${imdbId}`;
  871. ddlElement.style.float = "right";
  872.  
  873. let imgElement = document.createElement("img");
  874. imgElement.src = "https://ddl-warez.cc/wp-content/uploads/logo.png";
  875. imgElement.style.height = "48px";
  876. imgElement.style.aspectRatio = "1/1";
  877.  
  878. ddlElement.appendChild(imgElement);
  879.  
  880. targetElement.insertAdjacentElement("afterend", ddlElement);
  881. }
  882. }
  883.  
  884. let metadataAsText = "";
  885. function collectMetadataForClipboard() {
  886. let title = document.querySelector("span.hero__primary-text")?.textContent;
  887. let genres = document.querySelector("div[data-testid='interests'] div.ipc-chip-list__scroller")?.childNodes;
  888. let additionalMetadata = document.querySelector('[data-testid="hero__pageTitle"]')?.parentElement?.querySelector("ul");
  889.  
  890. // if click listener does not exist
  891. if (!document.querySelector("ul.collectMetadataForClipboardListener") && title && genres && additionalMetadata) {
  892. if (genres && additionalMetadata) {
  893. if (metadataAsText === "") {
  894. // add title
  895. metadataAsText += title + " | ";
  896. // collect additional metadata
  897. for (let element of additionalMetadata?.childNodes) metadataAsText += element.textContent + " | ";
  898. // collect genres
  899. let iteration = genres?.length;
  900. for (let genre of genres) {
  901. metadataAsText += genre.textContent;
  902.  
  903. // append "," as long as not last iteration
  904. if (--iteration) metadataAsText += ", ";
  905. }
  906. }
  907.  
  908. additionalMetadata.style.cursor = "pointer";
  909. additionalMetadata.addEventListener("click", function () {
  910. navigator.clipboard.writeText(metadataAsText);
  911. });
  912.  
  913. // to know if click listener is still there
  914. additionalMetadata.classList.add("collectMetadataForClipboardListener");
  915. }
  916. }
  917. }
  918.  
  919. // Configuration Modal
  920. function openConfiguration() {
  921. GM_addStyle(`
  922. .modal-overlay {
  923. position: fixed;
  924. top: 0;
  925. left: 0;
  926. width: 100vw;
  927. height: 100vh;
  928. background-color: rgba(0, 0, 0, 0.5);
  929. z-index: 9998;
  930. transition: background-color 0.5s ease;
  931. }
  932. .modal {
  933. font-family: var(--ipt-font-family);
  934. position: fixed;
  935. top: 50%;
  936. left: 50%;
  937. transform: translate(-50%, -50%);
  938. width: 300px;
  939. padding: 20px;
  940. background-color: #fff;
  941. border-radius: 10px;
  942. box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
  943. z-index: 9999;
  944. opacity: 0;
  945. transition: opacity 0.5s ease;
  946. }
  947. .modal-title {
  948. margin-bottom: 20px;
  949. font-size: 16px;
  950. font-weight: bold;
  951. }
  952. .checkbox-label {
  953. display: block;
  954. margin-bottom: 10px;
  955. }
  956. .close-button {
  957. display: block;
  958. margin: 20px auto 0;
  959. }
  960. `);
  961.  
  962. // Darken background
  963. const overlay = document.createElement("div");
  964. overlay.className = "modal-overlay";
  965. overlay.style.backgroundColor = "rgba(0, 0, 0, 0)";
  966. setTimeout(() => {
  967. overlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
  968. }, 50);
  969.  
  970. // Create modal
  971. const modal = document.createElement("div");
  972. modal.className = "modal";
  973. setTimeout(() => {
  974. modal.style.opacity = "1";
  975. }, 50);
  976.  
  977. // Title of the modal
  978. const title = document.createElement("h3");
  979. title.innerText = "Which ratings should be displayed?";
  980. title.className = "modal-title";
  981. modal.appendChild(title);
  982.  
  983. // Add checkboxes
  984. ratingSources.forEach((ratingSource) => {
  985. const label = document.createElement("label");
  986. label.className = "checkbox-label";
  987.  
  988. const checkbox = document.createElement("input");
  989. checkbox.type = "checkbox";
  990. checkbox.checked = GM_getValue(ratingSource, false);
  991.  
  992. checkbox.addEventListener("change", () => {
  993. GM_setValue(ratingSource, checkbox.checked);
  994. if (!checkbox.checked) {
  995. document.querySelector(`span.rating-bar__base-button[${ratingSource}]`).remove();
  996. } else {
  997. // trigger observer to add new badges
  998. const tempElement = document.createElement("div");
  999. document.body.appendChild(tempElement);
  1000. document.body.removeChild(tempElement);
  1001. }
  1002. });
  1003.  
  1004. label.appendChild(checkbox);
  1005. label.appendChild(document.createTextNode(` ${ratingSource}`));
  1006. modal.appendChild(label);
  1007. });
  1008.  
  1009. // Add button to close
  1010. const closeButton = document.createElement("button");
  1011. closeButton.innerText = "Close";
  1012. closeButton.className =
  1013. "close-button ipc-btn ipc-btn--half-padding ipc-btn--default-height ipc-btn--core-accent1 ipc-btn--theme-baseAlt ";
  1014.  
  1015. closeButton.addEventListener("click", () => {
  1016. document.body.removeChild(overlay);
  1017. document.body.removeChild(modal);
  1018. });
  1019.  
  1020. modal.appendChild(closeButton);
  1021.  
  1022. // Add modal and overlay to the DOM
  1023. document.body.appendChild(overlay);
  1024. document.body.appendChild(modal);
  1025.  
  1026. // Close modal on click outside
  1027. overlay.addEventListener("click", () => {
  1028. document.body.removeChild(overlay);
  1029. document.body.removeChild(modal);
  1030. });
  1031. }
  1032.  
  1033. // add and keep elements in header container
  1034. async function main() {
  1035. // set default configuration
  1036. ratingSources.forEach((badge) => {
  1037. // Query default value (if does not exist, 'null' is returned)
  1038. const existingValue = GM_getValue(badge, null);
  1039. if (existingValue === null) {
  1040. // Value does not exist, set default value to true
  1041. GM_setValue(badge, true);
  1042. }
  1043. });
  1044.  
  1045. // ignore episode view
  1046. if (!document.title.includes('"')) {
  1047. addCss();
  1048. getTmdbData();
  1049. getDoubanData();
  1050. getMetacriticData();
  1051. getMyAnimeListDataByImdbId();
  1052. getMyAnimeListDataByTitle();
  1053.  
  1054. const observer = new MutationObserver(async () => {
  1055. addCss();
  1056. await addMyAnimeListRatingBadge();
  1057. await addMetacriticRatingBadge();
  1058. await addDoubanRatingBadge();
  1059. await addTmdbRatingBadge();
  1060.  
  1061. addDdl();
  1062. // addGenresToTitle();
  1063. collectMetadataForClipboard();
  1064. });
  1065.  
  1066. observer.observe(document.documentElement, { childList: true, subtree: true });
  1067. }
  1068. }
  1069.  
  1070. // -----------------------------------------------------------------------------------------------------
  1071. // Main
  1072. // -----------------------------------------------------------------------------------------------------
  1073.  
  1074. main();
  1075. // GM_setValue("authorsMode", true);