// ==UserScript== // @name IMDb TMDB Linker // @description Opens the corresponding IMDB/TMDB/Letterboxd movie/tv page in just one click. Also adds the ability to see IMDB ratings on TMDB and Letterboxd pages. // @author Tetrax-10 // @namespace https://github.com/Tetrax-10/imdb-tmdb-linker // @version 1.2 // @license MIT // @match *://*.imdb.com/title/tt* // @match *://*.themoviedb.org/movie/* // @match *://*.themoviedb.org/tv/* // @match *://*.letterboxd.com/film/* // @connect imdb.com // @connect themoviedb.org // @homepageURL https://github.com/Tetrax-10/imdb-tmdb-linker // @supportURL https://github.com/Tetrax-10/imdb-tmdb-linker/issues // @icon https://tetrax-10.github.io/imdb-tmdb-linker/assets/icon.png // @run-at document-end // @grant GM_xmlhttpRequest // @downloadURL none // ==/UserScript== ;(function () { const tmdbApi = "YOUR_TMDB_API_KEY" const imdbCss = ` #linker-parent { display: flex; align-self: center; } #linker-letterboxd-a { align-self: center; } #linker-letterboxd { display: flex; height: 30px; border-radius: 4px; } #linker-divider { border-left: 3px solid rgba(232, 230, 227, 0.5); 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: 26px; width: 70px; 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; } @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 tmdbCss = ` #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); height: 20px; border-radius: 10px; margin-left: 10px; } #linker-letterboxd { height: 22px; border-radius: 4px; } #linker-loading { height: 20px; margin-left: 10px; } #linker-imdb-container { display: flex; align-items: flex-start; margin-left: 10px; } #linker-imdb-rating { margin-left: 10px; } html.k-mobile #linker-parent { margin-top: unset; margin-left: auto; margin-right: auto; } ` const letterboxdCss = ` #linker-loading { height: 14px; margin-left: 4px; } ` async function waitForElement(selector, timeout = null, nthElement = 1) { 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(false) }, 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) => { GM_xmlhttpRequest({ method: "GET", url: `https://www.imdb.com/title/${imdbId}/ratings`, onload: function (response) { 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]) }, onerror: function (error) { console.error("Request failed:", error) }, }) }) } function injectCSS(css) { const style = document.createElement("style") style.appendChild(document.createTextNode(css)) document.head.appendChild(style) } const imdbUtils = (() => { function createParentElement() { const parentElement = document.createElement("div") parentElement.id = "linker-parent" return parentElement } function createLetterboxdElement(imdbId) { const letterboxdElement = document.createElement("a") letterboxdElement.id = "linker-letterboxd-a" letterboxdElement.href = `https://letterboxd.com/imdb/${imdbId}/` letterboxdElement.target = "_blank" const letterboxdImage = document.createElement("img") letterboxdImage.id = "linker-letterboxd" letterboxdImage.src = "https://tetrax-10.github.io/imdb-tmdb-linker/assets/letterboxd.png" letterboxdElement.appendChild(letterboxdImage) return letterboxdElement } function createDivider() { const divider = document.createElement("div") divider.id = "linker-divider" return divider } function createLoadingElement() { const loadingElement = document.createElement("img") loadingElement.id = "linker-loading" loadingElement.src = "https://tetrax-10.github.io/imdb-tmdb-linker/assets/loading.gif" return loadingElement } function createTmdbButtonElement(tmdbId) { const tmdbElement = document.createElement("a") tmdbElement.id = "linker-tmdb-link" tmdbElement.target = "_blank" tmdbElement.innerText = "TMDB" if (tmdbId["media_type"] !== "tv_episode") { tmdbElement.href = `https://www.themoviedb.org/${tmdbId["media_type"]}/${tmdbId.id}` } else { tmdbElement.href = `https://www.themoviedb.org/tv/${tmdbId["show_id"]}/season/${tmdbId["season_number"]}/episode/${tmdbId["episode_number"]}` } return tmdbElement } return { element: { parent: createParentElement, letterboxd: createLetterboxdElement, divider: createDivider, loading: createLoadingElement, tmdbButton: createTmdbButtonElement, }, } })() async function imdb() { const isMobile = location.host.includes("m.imdb") const path = location.pathname.split("/") const imdbId = path[2] || null if (imdbId) { const parentElement = imdbUtils.element.parent() const letterboxdElement = imdbUtils.element.letterboxd(imdbId) const dividerElement = imdbUtils.element.divider() const loadingElement = imdbUtils.element.loading() waitForElement("div:has( > div[data-testid='hero-rating-bar__user-rating'])", 10000, isMobile ? 2 : 1).then((location) => { location.insertBefore(parentElement, location.firstChild) parentElement.appendChild(letterboxdElement) parentElement.appendChild(dividerElement) parentElement.appendChild(loadingElement) }) const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/find/${imdbId}?api_key=${tmdbApi}&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) { const imdbElement = imdbUtils.element.tmdbButton(tmdbData) parentElement.removeChild(loadingElement) parentElement.appendChild(imdbElement) } else { parentElement.removeChild(dividerElement) parentElement.removeChild(loadingElement) } } } const ImdbSvg = `` // prettier-ignore const tmdbUtils = (() => { function createParentElement() { const parentElement = document.createElement("div") parentElement.id = "linker-parent" return parentElement } function createLetterboxdElement(tmdbId, type) { const letterboxdElement = document.createElement("a") letterboxdElement.href = `https://letterboxd.com/tmdb/${tmdbId}/${type === "tv" ? "tv" : ""}` letterboxdElement.target = "_blank" const letterboxdImage = document.createElement("img") letterboxdImage.id = "linker-letterboxd" letterboxdImage.src = "https://tetrax-10.github.io/imdb-tmdb-linker/assets/letterboxd.png" letterboxdElement.appendChild(letterboxdImage) return letterboxdElement } function createDivider() { const divider = document.createElement("div") divider.id = "linker-divider" return divider } function createLoadingElement() { const loadingElement = document.createElement("img") loadingElement.id = "linker-loading" loadingElement.src = "https://tetrax-10.github.io/imdb-tmdb-linker/assets/loading.gif" return loadingElement } function createImdbContainer() { const imdbContainer = document.createElement("div") imdbContainer.id = "linker-imdb-container" return imdbContainer } function createImdbLinkElement(imdbId, svg) { const link = document.createElement("a") link.href = `https://imdb.com/title/${imdbId}` link.target = "_blank" link.innerHTML = svg return link } function createImdbRatingElement(rating, numRatings) { const text = rating !== undefined ? `${rating}${numRatings !== undefined ? ` ( ${numRatings} )` : ""}` : null const ratingElement = document.createElement("div") ratingElement.id = "linker-imdb-rating" ratingElement.innerText = text if (text) { return ratingElement } else { return null } } return { element: { parent: createParentElement, letterboxd: createLetterboxdElement, divider: createDivider, loading: createLoadingElement, imdbContainer: createImdbContainer, imdbLink: createImdbLinkElement, imdbRating: createImdbRatingElement, }, } })() async function tmdb() { const isMobile = document.querySelector("html.k-mobile") const path = location.pathname.split("/") const tmdbId = path[2].match(/\d+/)?.[0] || null if (tmdbId) { const parentElement = tmdbUtils.element.parent() const letterboxdElement = tmdbUtils.element.letterboxd(tmdbId, path[1]) const divider = tmdbUtils.element.divider() const imdbContainer = tmdbUtils.element.imdbContainer() const loadingElement = tmdbUtils.element.loading() waitForElement(`.header.poster${isMobile ? " > .title" : ""}`, 10000).then((location) => { if (isMobile) { location.insertBefore(parentElement, location?.firstChild?.nextSibling?.nextSibling) } else { location.appendChild(parentElement) } parentElement.appendChild(letterboxdElement) parentElement.appendChild(divider) parentElement.appendChild(imdbContainer) imdbContainer.appendChild(loadingElement) }) // fetch imdb id const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/${path[1]}/${tmdbId}/external_ids?api_key=${tmdbApi}`) if (tmdbRawRes.status !== 200) return const tmdbRes = await tmdbRawRes.json() const imdbId = tmdbRes["imdb_id"] || null if (!imdbId) { parentElement.removeChild(divider) parentElement.removeChild(imdbContainer) return } // inject imdb link const imdbLink = tmdbUtils.element.imdbLink(imdbId, ImdbSvg) imdbContainer.insertBefore(imdbLink, loadingElement) // inject imdb rating const [imdbRating, imdbNumRating] = await getImdbRating(imdbId) const imdbRatingElement = tmdbUtils.element.imdbRating(imdbRating, imdbNumRating) imdbContainer.removeChild(loadingElement) if (!imdbRatingElement) return imdbContainer.appendChild(imdbRatingElement) } } function letterboxd() { waitForElement(`.micro-button.track-event[data-track-action="IMDb"]`, 10000).then(async (element) => { const originalDisplay = element.style.display // add loading element const loadingElement = tmdbUtils.element.loading() element.style.display = "inline-flex" element.appendChild(loadingElement) // fetch imdb id and get ratings const imdbId = element.href?.match(/\/title\/(tt\d+)\/?/)?.[1] ?? null const [imdbRating, imdbNumRating] = await getImdbRating(imdbId) // remove loading element element.removeChild(loadingElement) element.style.display = originalDisplay // update element element.innerText = `IMDB${imdbRating ? ` | ${imdbRating}` : ""}${imdbNumRating !== undefined ? ` (${imdbNumRating})` : ""}` }) } const currentURL = window.location.protocol + "//" + window.location.hostname + window.location.pathname if (/^(https?:\/\/[^.]+\.imdb\.com\/title\/tt[^\/]+(?:\/\?.*)?\/?)$/.test(currentURL)) { injectCSS(imdbCss) imdb() } if (/^(https?:\/\/[^.]+\.themoviedb\.org\/(movie|tv)\/\d[^\/]+(?:\/\?.*)?\/?)$/.test(currentURL)) { injectCSS(tmdbCss) tmdb() } if (/^(https?:\/\/letterboxd\.com\/film\/[^\/]+(?:\/\?.*)?\/?(crew|details|genres)?)$/.test(currentURL)) { injectCSS(letterboxdCss) letterboxd() } })()