// ==UserScript== // @name IMDb TMDB Letterboxd Linker // @description Opens the corresponding IMDb, TMDB, or Letterboxd page for movies, TV shows and people with a single click. Additionally, it also displays IMDb ratings on both TMDB and Letterboxd pages. // @author Tetrax-10 // @namespace https://github.com/Tetrax-10/imdb-tmdb-letterboxd-linker // @version 2.3 // @license MIT // @match *://*.imdb.com/title/tt* // @match *://*.imdb.com/name/nm* // @match *://*.themoviedb.org/movie/* // @match *://*.themoviedb.org/tv/* // @match *://*.themoviedb.org/person/* // @match *://*.letterboxd.com/film/* // @include /^https?:\/\/(?:www\.)?letterboxd\.com\/(actor|additional-photography|camera-operator|cinematography|composer|costume-design|director|editor|executive-producer|hairstyling|makeup|original-writer|producer|set-decoration|sound|story|visual-effects|writer)\/.*$/ // @connect imdb.com // @connect themoviedb.org // @homepageURL https://github.com/Tetrax-10/imdb-tmdb-letterboxd-linker // @supportURL https://github.com/Tetrax-10/imdb-tmdb-letterboxd-linker/issues // @icon https://tetrax-10.github.io/imdb-tmdb-letterboxd-linker/assets/icon.png // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @downloadURL none // ==/UserScript== ;(() => { const TMDB_API_KEY = GM_getValue("TMDB_API_KEY", null)?.trim() GM_registerMenuCommand("Settings", showPopup) function showPopup() { GM_addStyle(` #linker-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; } #linker-settings-popup { background-color: rgb(32, 36, 44); padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); z-index: 10001; font-family: Source Sans Pro, Arial, sans-serif; font-feature-settings: normal; font-variation-settings: normal; font-size: 100%; font-weight: inherit; line-height: 1.5; letter-spacing: normal; width: 60%; max-height: 80vh; overflow-y: auto; display: flex; flex-direction: column; -webkit-overflow-scrolling: touch; } #linker-settings-popup input { color: #cfcfcf; } #linker-settings-popup label { color: rgb(207, 207, 207); font-weight: bold; font-size: 1.2em; margin-bottom: 10px; } #linker-settings-popup input { background-color: rgb(32, 36, 44); border: 1px solid rgb(207, 207, 207); color: rgb(207, 207, 207); padding: 10px; border-radius: 8px; margin-bottom: 10px; } `) // Create overlay const overlay = document.createElement("div") overlay.id = "linker-settings-overlay" overlay.onclick = (e) => { if (e.target === overlay) closePopup(overlay) } // popup element const popup = document.createElement("div") popup.id = "linker-settings-popup" // popup content const label = document.createElement("label") label.textContent = "Enter your TMDB API key:" // input element const input = document.createElement("input") input.type = "text" input.value = GM_getValue("TMDB_API_KEY", "") input.oninput = (e) => { try { GM_setValue("TMDB_API_KEY", e.target?.value?.trim()) } catch (error) { console.error("Failed to set TMDB API key", error) } } // inject popup popup.appendChild(label) popup.appendChild(input) overlay.appendChild(popup) document.body.appendChild(overlay) input.focus() } function closePopup(overlay) { document.body.removeChild(overlay) } const imdbPageCss = ` #linker-parent { display: flex; align-self: center; } #linker-letterboxd-link { align-self: center; } #linker-letterboxd { height: 27px; width: 53px; margin-top: 7px; } #linker-divider { border-left: 3px solid rgba(232, 230, 227, 0.5) !important; height: 25px; border-radius: 10px; margin-left: 10px; align-self: center; } #linker-loading { height: 20px; align-self: center; text-align: center; margin-left: 10px; margin-right: 40px; } #linker-tmdb-link { height: 27px; width: 60px; background: #022036 !important; color: #51b4ad !important; border: solid #51b4ad 2px !important; border-radius: 6px; align-self: center; margin-left: 10px; margin-right: 20px; font-weight: bold; text-align: center; align-content: center; } @media only screen and (max-width: 767px) { #linker-loading { margin-right: 6px; } #linker-tmdb-link { width: 48px; margin-left: 10px; margin-right: 10px; font-size: smaller; } } ` const tmdbTitlePageCss = ` #linker-parent { margin-top: 20px; display: flex; align-items: flex-start; } #linker-imdb-svg-bg { fill: #c59f00 !important; } #linker-divider { border-left: 2px solid rgba(232, 230, 227, 0.5) !important; height: 23px; border-radius: 10px; margin-left: 10px; } #linker-loading { height: 20px; margin-left: 10px; } #linker-imdb-container { display: flex; align-items: center; margin-left: 10px; } #linker-imdb-rating { margin-left: 10px; } html.k-mobile #linker-parent { margin-top: unset; margin-left: auto; margin-right: auto; } ` const tmdbPersonPageCss = ` #linker-imdb-svg, #linker-letterboxd-svg path { --darkreader-inline-fill: #d0d0d0 !important; } ` const letterboxdTitlePageCss = ` #linker-loading { border: 2px solid rgba(255, 255, 255, 0.3) !important; border-top: 2px solid #cfcfcf !important; height: 8px !important; width: 8px !important; margin-left: 4px; } ` const commonUtils = (() => { const ImdbSvg = `` const ImdbSvgWithoutBg = `` const letterboxdSvg = `` const LetterboxdSvgWithoutBg = `` function isMobile() { const data = navigator.userAgent || navigator.vendor || window.opera // Check for userAgentData mobile status (newer browsers) // prettier-ignore if (navigator.userAgentData?.mobile || /Mobi/i.test(navigator.userAgent) || 'ontouchstart' in document.documentElement || /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(data) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(data.substr(0, 4))) { return true } else { return false } } async function waitForElement(selector, timeout = null, nthElement = 1) { // wait till document body loads while (!document.body) { await new Promise((resolve) => setTimeout(resolve, 10)) } nthElement -= 1 return new Promise((resolve) => { if (document.querySelectorAll(selector)?.[nthElement]) { return resolve(document.querySelectorAll(selector)?.[nthElement]) } const observer = new MutationObserver(async () => { if (document.querySelectorAll(selector)?.[nthElement]) { resolve(document.querySelectorAll(selector)?.[nthElement]) observer.disconnect() } else { if (timeout) { async function timeOver() { return new Promise((resolve) => { setTimeout(() => { observer.disconnect() resolve(undefined) }, timeout) }) } resolve(await timeOver()) } } }) observer.observe(document.body, { childList: true, subtree: true, }) }) } async function getImdbRating(imdbId) { if (!imdbId) return [undefined, undefined] return new Promise((resolve) => { try { GM_xmlhttpRequest({ method: "GET", url: `https://www.imdb.com/title/${imdbId}/ratings`, onload: function (response) { try { const parser = new DOMParser() const dom = parser.parseFromString(response.responseText, "text/html") const rating = dom.querySelector(`div[data-testid="rating-button__aggregate-rating__score"] > span`)?.innerText const numRating = dom.querySelector(`div[data-testid="rating-button__aggregate-rating__score"] + div`)?.innerText resolve([rating, numRating]) } catch (parsingError) { console.error("Error parsing IMDb rating data", parsingError) resolve([undefined, undefined]) } }, onerror: function (error) { console.error(`Can't scrape IMDb: ${imdbId}`, error) resolve([undefined, undefined]) }, }) } catch (requestError) { console.error("Failed to initiate IMDb request", requestError) resolve([undefined, undefined]) } }) } function createDividerElement() { const divider = document.createElement("div") divider.id = "linker-divider" return divider } function createParentContainer() { const parentContainer = document.createElement("div") parentContainer.id = "linker-parent" return parentContainer } function createLoadingElement() { const loadingElement = document.createElement("div") loadingElement.id = "linker-loading" // Add loading animation CSS try { GM_addStyle(` #linker-loading { border: 4px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top: 4px solid #cfcfcf; width: 22px; height: 22px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `) } catch (styleError) { console.error("Failed to add styles for loading element", styleError) } return loadingElement } return { isMobile: isMobile, waitForElement: waitForElement, getImdbRating: getImdbRating, svg: { ImdbSvg: ImdbSvg, ImdbSvgWithoutBg: ImdbSvgWithoutBg, letterboxdSvg: letterboxdSvg, LetterboxdSvgWithoutBg: LetterboxdSvgWithoutBg, }, element: { createDividerElement: createDividerElement, createParentContainer: createParentContainer, createLoadingElement: createLoadingElement, }, } })() const imdbPageUtils = (() => { function createLetterboxdElement(imdbId) { const linkElement = document.createElement("a") linkElement.id = "linker-letterboxd-link" linkElement.href = imdbId.startsWith("https") ? imdbId : `https://letterboxd.com/imdb/${imdbId}/` linkElement.target = "_blank" linkElement.innerHTML = commonUtils.svg.letterboxdSvg return linkElement } function createTmdbElement(tmdbData) { const linkElement = document.createElement("a") linkElement.id = "linker-tmdb-link" linkElement.target = "_blank" linkElement.innerText = "TMDB" try { if (tmdbData["media_type"] === "tv_episode") { linkElement.href = `https://www.themoviedb.org/tv/${tmdbData["show_id"]}/season/${tmdbData["season_number"]}/episode/${tmdbData["episode_number"]}` } else if (typeof tmdbData === "object") { linkElement.href = `https://www.themoviedb.org/${tmdbData["media_type"]}/${tmdbData.id}` } else if (typeof tmdbData === "string") { linkElement.href = tmdbData } } catch (error) { console.error("Failed to create TMDB element", error) } return linkElement } function mirrorElements(parentContainer, isMobile, rootElementSelector) { const observer = new MutationObserver(() => { try { const clonedContainer = parentContainer?.cloneNode(true) commonUtils.waitForElement(rootElementSelector, 10000, !isMobile ? 2 : 1).then((element) => { if (!element) return for (const parentEle of element.querySelectorAll("#linker-parent")) { parentEle?.remove() } element.insertBefore(clonedContainer, element.firstChild) }) } catch (error) { console.error("Error while mirroring elements", error) } }) observer.observe(parentContainer, { childList: true, subtree: true, attributes: true }) } return { element: { createLetterboxdElement: createLetterboxdElement, createTmdbElement: createTmdbElement, mirrorElements: mirrorElements, }, } })() async function imdbTitlePageInjector() { const isMobile = location.host.includes("m.imdb") const path = location.pathname.split("/") const imdbId = path[2] || null const parentContainer = commonUtils.element.createParentContainer() const letterboxdElement = imdbPageUtils.element.createLetterboxdElement(imdbId) const dividerElement = commonUtils.element.createDividerElement() const loadingElement = commonUtils.element.createLoadingElement() const rootElementSelector = "div:has( > div[data-testid='hero-rating-bar__user-rating'])" window.addEventListener("load", () => { try { commonUtils.waitForElement(rootElementSelector, 10000, isMobile ? 2 : 1).then((element) => { element.insertBefore(parentContainer, element.firstChild) imdbPageUtils.element.mirrorElements(parentContainer, isMobile, rootElementSelector) parentContainer.appendChild(letterboxdElement) parentContainer.appendChild(dividerElement) if (!TMDB_API_KEY) return parentContainer.appendChild(loadingElement) }) } catch (error) { console.error("Error during element injection on IMDb title page", error) } }) // inject parent element if not present function injectParentElement() { try { if (!document.querySelectorAll("#linker-parent")[isMobile ? 2 : 1]) { commonUtils.waitForElement(rootElementSelector, 10000, isMobile ? 2 : 1).then((element) => { element.insertBefore(parentContainer, element.firstChild) }) } if (!document.querySelectorAll("#linker-parent")[!isMobile ? 2 : 1]) { imdbPageUtils.element.mirrorElements(parentContainer, isMobile, rootElementSelector) } } catch (error) { console.error("Failed to inject parent element", error) } } // inject the parent element every 100ms. Since IMDb sometimes re-renders its components, the parent element may occasionally be removed. const intervalId = setInterval(injectParentElement, 100) setTimeout(() => { clearInterval(intervalId) }, 5000) if (!TMDB_API_KEY) { await commonUtils.waitForElement("#linker-divider") try { const tmdbElement = imdbPageUtils.element.createTmdbElement(`https://www.themoviedb.org/redirect?external_source=imdb_id&external_id=${imdbId}`) parentContainer.appendChild(tmdbElement) } catch (error) { console.error("Failed to create TMDB element for title page", error) } return } try { const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/find/${imdbId}?api_key=${TMDB_API_KEY}&external_source=imdb_id`) const tmdbRes = await tmdbRawRes.json() const tmdbData = tmdbRes["movie_results"]?.[0] || tmdbRes["tv_results"]?.[0] || tmdbRes["tv_episode_results"]?.[0] if (tmdbData && (await commonUtils.waitForElement("#linker-loading", 10000))) { const tmdbElement = imdbPageUtils.element.createTmdbElement(tmdbData) parentContainer.removeChild(loadingElement) parentContainer.appendChild(tmdbElement) } else { parentContainer.removeChild(dividerElement) parentContainer.removeChild(loadingElement) } } catch (error) { console.error("Failed to fetch or process TMDB data for title page", error) parentContainer.removeChild(dividerElement) parentContainer.removeChild(loadingElement) } } async function imdbPersonPageInjector() { const isMobile = location.host.includes("m.imdb") const path = location.pathname.split("/") const imdbId = path[2] || null const parentContainer = commonUtils.element.createParentContainer() const loadingElement = commonUtils.element.createLoadingElement() const rootElementSelector = "div:has( > .starmeter-logo)" window.addEventListener("load", () => { try { commonUtils.waitForElement(rootElementSelector, 10000, isMobile ? 2 : 1).then((element) => { element.insertBefore(parentContainer, element.firstChild) imdbPageUtils.element.mirrorElements(parentContainer, isMobile, rootElementSelector) parentContainer.appendChild(loadingElement) }) } catch (error) { console.error("Error during element injection on IMDb person page", error) } }) // inject parent element if not present function injectParentElement() { try { if (!document.querySelector("#linker-parent")) { commonUtils.waitForElement(rootElementSelector, 10000, isMobile ? 2 : 1).then((element) => { element.insertBefore(parentContainer, element.firstChild) }) } if (!document.querySelectorAll("#linker-parent")[!isMobile ? 2 : 1]) { imdbPageUtils.element.mirrorElements(parentContainer, isMobile, rootElementSelector) } } catch (error) { console.error("Failed to inject parent element on person page", error) } } // inject the parent element every 100ms. Since IMDb sometimes re-renders its components, the parent element may occasionally be removed. const intervalId = setInterval(injectParentElement, 100) setTimeout(() => { clearInterval(intervalId) }, 5000) if (!TMDB_API_KEY) { await commonUtils.waitForElement("#linker-loading") try { const tmdbElement = imdbPageUtils.element.createTmdbElement(`https://www.themoviedb.org/redirect?external_source=imdb_id&external_id=${imdbId}`) parentContainer.removeChild(loadingElement) parentContainer.appendChild(tmdbElement) } catch (error) { console.error("Failed to create TMDB element for person page", error) } return } try { const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/find/${imdbId}?api_key=${TMDB_API_KEY}&external_source=imdb_id`) const tmdbRes = await tmdbRawRes.json() const tmdbData = tmdbRes["movie_results"]?.[0] || tmdbRes["tv_results"]?.[0] || tmdbRes["tv_episode_results"]?.[0] || tmdbRes["person_results"]?.[0] if (tmdbData && (await commonUtils.waitForElement("#linker-loading", 10000))) { const tmdbElement = imdbPageUtils.element.createTmdbElement(tmdbData) const letterboxdElement = imdbPageUtils.element.createLetterboxdElement(`https://letterboxd.com/tmdb/${tmdbData.id}/person`) const dividerElement = commonUtils.element.createDividerElement() parentContainer.removeChild(loadingElement) parentContainer.appendChild(letterboxdElement) parentContainer.appendChild(dividerElement) parentContainer.appendChild(tmdbElement) } else { parentContainer.removeChild(loadingElement) } } catch (error) { console.error("Failed to fetch or process TMDB data for person page", error) parentContainer.removeChild(loadingElement) } } const tmdbTitlePageUtils = (() => { function createLetterboxdElement(tmdbId, type) { try { const linkElement = document.createElement("a") linkElement.href = `https://letterboxd.com/tmdb/${tmdbId}/${type === "movie" ? "" : type}` linkElement.target = "_blank" linkElement.innerHTML = commonUtils.svg.letterboxdSvg return linkElement } catch (error) { console.error("Failed to create Letterboxd element:", error) return null } } function createImdbContainer() { try { const imdbContainer = document.createElement("div") imdbContainer.id = "linker-imdb-container" return imdbContainer } catch (error) { console.error("Failed to create IMDb container:", error) return null } } function createImdbElement(imdbId) { try { const linkElement = document.createElement("a") linkElement.href = `https://imdb.com/title/${imdbId}` linkElement.target = "_blank" linkElement.innerHTML = commonUtils.svg.ImdbSvg return linkElement } catch (error) { console.error("Failed to create IMDb element:", error) return null } } function createImdbRatingElement(rating, numRatings) { try { const text = rating !== undefined ? `${rating}${numRatings !== undefined ? ` ( ${numRatings} )` : ""}` : null const ratingElement = document.createElement("div") ratingElement.id = "linker-imdb-rating" ratingElement.innerText = text return text ? ratingElement : null } catch (error) { console.error("Failed to create IMDb rating element:", error) return null } } return { element: { createLetterboxdElement: createLetterboxdElement, createImdbContainer: createImdbContainer, createImdbElement: createImdbElement, createImdbRatingElement: createImdbRatingElement, }, } })() async function tmdbTitlePageInjector() { try { const isMobile = commonUtils.isMobile() const path = location.pathname.split("/") const tmdbId = path[2].match(/\d+/)?.[0] || null if (!tmdbId) throw new Error("TMDB ID could not be extracted from the URL") const parentContainer = commonUtils.element.createParentContainer() const letterboxdElement = tmdbTitlePageUtils.element.createLetterboxdElement(tmdbId, path[1]) const dividerElement = commonUtils.element.createDividerElement() const imdbContainer = tmdbTitlePageUtils.element.createImdbContainer() const loadingElement = commonUtils.element.createLoadingElement() commonUtils.waitForElement(`.header.poster${isMobile ? " > .title" : ""}`, 10000).then((element) => { try { if (isMobile) { element.insertBefore(parentContainer, element?.firstChild?.nextSibling?.nextSibling) } else { element.appendChild(parentContainer) } parentContainer.appendChild(letterboxdElement) if (!TMDB_API_KEY) return parentContainer.appendChild(dividerElement) parentContainer.appendChild(imdbContainer) imdbContainer.appendChild(loadingElement) } catch (error) { console.error("Error during element injection on TMDB title page:", error) } }) if (!TMDB_API_KEY) return // Fetch IMDb ID const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/${path[1]}/${tmdbId}/external_ids?api_key=${TMDB_API_KEY}`).catch((error) => { console.error("Failed to fetch external IDs from TMDB:", error) }) if (!tmdbRawRes) return const tmdbRes = await tmdbRawRes.json() const imdbId = tmdbRes["imdb_id"] || null if (!imdbId) { parentContainer.removeChild(dividerElement) parentContainer.removeChild(imdbContainer) return } // Inject IMDb element const imdbElement = tmdbTitlePageUtils.element.createImdbElement(imdbId) commonUtils.waitForElement(`.header.poster${isMobile ? " > .title" : ""}`, 10000).then(async () => { try { await commonUtils.waitForElement("#linker-imdb-container", 5000) imdbContainer.insertBefore(imdbElement, loadingElement) } catch (error) { console.error("Error while waiting to inject IMDb element:", error) } }) // Scrape IMDb ratings const [imdbRating, imdbNumRating] = await commonUtils.getImdbRating(imdbId).catch((error) => { console.error("Failed to fetch IMDb rating:", error) }) // Inject IMDb rating element const imdbRatingElement = tmdbTitlePageUtils.element.createImdbRatingElement(imdbRating, imdbNumRating) await commonUtils.waitForElement("#linker-loading", 10000).catch((error) => { console.error("Failed to wait for linker loading:", error) }) try { imdbContainer.removeChild(loadingElement) if (imdbRatingElement) imdbContainer.appendChild(imdbRatingElement) } catch (error) { console.error("Failed to inject IMDb rating element:", error) } } catch (error) { console.error("Error in tmdbTitlePageInjector:", error) } } const tmdbPersonPageUtils = (() => { function createLogoElement(id, type = "imdb") { try { const linkContainer = document.createElement("div") const linkElement = document.createElement("a") linkElement.className = "social_link" linkElement.href = type === "imdb" ? `https://www.imdb.com/name/${id}` : `https://letterboxd.com/tmdb/${id}/person` linkElement.target = "_blank" linkElement.title = `Visit ${type === "imdb" ? "IMDb" : "Letterboxd"}` linkElement.rel = "noopener" if (type !== "imdb") linkElement.style.width = "38px" const svgContainer = document.createElement("div") svgContainer.className = "glyphicons_v2" svgContainer.style.width = "50px" svgContainer.innerHTML = type === "imdb" ? commonUtils.svg.ImdbSvgWithoutBg : commonUtils.svg.LetterboxdSvgWithoutBg linkElement.appendChild(svgContainer) linkContainer.appendChild(linkElement) return linkContainer } catch (error) { console.error("Failed to create logo element:", error) return null } } return { element: { createLogoElement: createLogoElement, }, } })() async function tmdbPersonPageInjector() { try { // Extract TMDB ID from URL const path = location.pathname.split("/") const tmdbId = path[2].match(/\d+/)?.[0] || null if (!tmdbId) throw new Error("TMDB ID could not be extracted from the URL") // Create and inject Letterboxd element const letterboxdElement = tmdbPersonPageUtils.element.createLogoElement(tmdbId, "letterboxd") commonUtils.waitForElement(".social_links", 10000).then((element) => { try { element.insertBefore(letterboxdElement, element.firstChild) } catch (error) { console.error("Failed to inject Letterboxd element:", error) } }) if (!TMDB_API_KEY) return // Fetch IMDb ID const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/${path[1]}/${tmdbId}/external_ids?api_key=${TMDB_API_KEY}`).catch((error) => { console.error("Failed to fetch external IDs from TMDB:", error) }) if (!tmdbRawRes) return const tmdbRes = await tmdbRawRes.json() const imdbId = tmdbRes["imdb_id"] || null // Inject IMDb element if (imdbId) { const imdbElement = tmdbPersonPageUtils.element.createLogoElement(imdbId) commonUtils.waitForElement(`.social_links`, 10000).then(async (element) => { try { await commonUtils.waitForElement("#linker-letterboxd-svg") element.insertBefore(imdbElement, letterboxdElement.nextElementSibling) } catch (error) { console.error("Failed to inject IMDb element:", error) } }) } } catch (error) { console.error("Error in tmdbPersonPageInjector:", error) } } function letterboxdTitlePageInjector() { commonUtils.waitForElement(`.micro-button.track-event[data-track-action="IMDb"]`, 10000).then(async (element) => { try { // Preserve original display style const originalDisplayStyle = element.style.display // Inject loading element const loadingElement = commonUtils.element.createLoadingElement() element.style.display = "inline-flex" element.appendChild(loadingElement) // Fetch IMDb ID and get ratings const imdbId = element.href?.match(/\/title\/(tt\d+)\/?/)?.[1] ?? null if (!imdbId) throw new Error("IMDb ID could not be extracted from the element href") const [imdbRating, imdbNumRating] = await commonUtils.getImdbRating(imdbId).catch((error) => { console.error("Failed to fetch IMDb ratings:", error) return [null, null] }) // Remove loading element await commonUtils.waitForElement("#linker-loading", 10000) element.removeChild(loadingElement) element.style.display = originalDisplayStyle // Update IMDb button with fetched rating information element.innerText = `IMDb${imdbRating ? ` | ${imdbRating}` : ""}${imdbNumRating !== undefined ? ` (${imdbNumRating})` : ""}` } catch (error) { console.error("Error in letterboxdTitlePageInjector:", error) } }) } function letterboxdPersonPageInjector() { commonUtils.waitForElement(`.micro-button[href^="https://www.themoviedb.org/person/"]`, 10000).then(async (element) => { try { // open tmdb link in new tab element.target = "_blank" // To make sure other scripts didn't inject imdb link if (document.querySelector(`.micro-button[href^="https://www.imdb.com/name/nm"]`)) return // Fetch TMDB ID const tmdbId = element.href?.match(/\/person\/(\d+)\/?/)?.[1] ?? null if (tmdbId) { // Fetch external IDs const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/person/${tmdbId}?api_key=${TMDB_API_KEY}&append_to_response=external_ids`).catch((error) => { console.error("Failed to fetch external IDs from TMDB:", error) }) if (!tmdbRawRes) return // Extract IMDb ID const tmdbRes = await tmdbRawRes.json() const imdbId = tmdbRes["external_ids"]["imdb_id"] || null if (imdbId && !document.querySelector(`.micro-button[href^="https://www.imdb.com/name/nm"]`)) { // create IMDb element const imdbElement = element.cloneNode(true) imdbElement.href = `https://www.imdb.com/name/${imdbId}` imdbElement.innerText = "IMDB" imdbElement.target = "_blank" imdbElement.setAttribute("data-track-action", "IMDb") imdbElement.style.marginRight = "5px" // inject IMDb element element.parentElement.insertBefore(imdbElement, element) } } } catch (error) { console.error("Error in letterboxdPersonPageInjector:", error) } }) } const currentURL = location.protocol + "//" + location.hostname + location.pathname if (/^(https?:\/\/[^.]+\.imdb\.com\/title\/tt[^\/]+(?:\/\?.*)?\/?)$/.test(currentURL)) { // IMDb title page GM_addStyle(imdbPageCss) imdbTitlePageInjector() } else if (/^(https?:\/\/[^.]+\.imdb\.com\/name\/nm[^\/]+(?:\/\?.*)?\/?)$/.test(currentURL)) { // IMDb person page GM_addStyle(imdbPageCss) imdbPersonPageInjector() } else if (/^(https?:\/\/[^.]+\.themoviedb\.org\/(movie|tv)\/\d[^\/]+(?:\/\?.*)?\/?)$/.test(currentURL)) { // TMDB title page GM_addStyle(tmdbTitlePageCss) tmdbTitlePageInjector() } else if (/^(https?:\/\/[^.]+\.themoviedb\.org\/person\/\d[^\/]+(?:\/\?.*)?\/?)$/.test(currentURL)) { // TMDB person page GM_addStyle(tmdbPersonPageCss) tmdbPersonPageInjector() } else if (/^(https?:\/\/letterboxd\.com\/film\/[^\/]+\/?(crew|details|releases|genres)?\/)$/.test(currentURL)) { // Letterboxd title page GM_addStyle(letterboxdTitlePageCss) letterboxdTitlePageInjector() } else if ( /^(https?:\/\/letterboxd\.com\/(actor|additional-photography|camera-operator|cinematography|composer|costume-design|director|editor|executive-producer|hairstyling|makeup|original-writer|producer|set-decoration|sound|story|visual-effects|writer)\/[A-Za-z0-9-_]+(?:\/(by|language|country|decade|genre|on|year)\/[A-Za-z0-9-_\/]+)?\/(?:page\/\d+\/?)?)$/.test( currentURL ) ) { // Letterboxd person page letterboxdPersonPageInjector() } })()