// ==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.1
// @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/*
// @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)
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) => GM_setValue("TMDB_API_KEY", e.target.value)
// 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
// 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(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(`Can't scrape IMDb: ${imdbId}`, error)
},
})
})
}
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"
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); }
}
`)
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"
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
}
return linkElement
}
function mirrorElements(parentContainer, isMobile) {
const observer = new MutationObserver(() => {
const clonedContainer = parentContainer?.cloneNode(true)
commonUtils.waitForElement("div:has( > div[data-testid='hero-rating-bar__user-rating'])", 10000, !isMobile ? 2 : 1).then((element) => {
for (const parentEle of element.querySelectorAll("#linker-parent")) {
parentEle?.remove()
}
element.insertBefore(clonedContainer, element.firstChild)
})
})
observer.observe(parentContainer, { childList: true, subtree: true, attributes: true })
}
return {
element: {
createLetterboxdElement: createLetterboxdElement,
createTmdbElement: createTmdbElement,
mirrorElements: mirrorElements,
},
}
})()
async function imdbTitlePageInjector() {
// check is mobile
const isMobile = location.host.includes("m.imdb")
// extract imdb id from url
const path = location.pathname.split("/")
const imdbId = path[2] || null
// create elements that doesn't require any API calls
const parentContainer = commonUtils.element.createParentContainer()
const letterboxdElement = imdbPageUtils.element.createLetterboxdElement(imdbId)
const dividerElement = commonUtils.element.createDividerElement()
const loadingElement = commonUtils.element.createLoadingElement()
// inject elements that doesn't require any API calls
window.addEventListener("load", () => {
commonUtils.waitForElement("div:has( > div[data-testid='hero-rating-bar__user-rating'])", 10000, isMobile ? 2 : 1).then((element) => {
element.insertBefore(parentContainer, element.firstChild)
imdbPageUtils.element.mirrorElements(parentContainer, isMobile)
parentContainer.appendChild(letterboxdElement)
parentContainer.appendChild(dividerElement)
if (!TMDB_API_KEY) return
parentContainer.appendChild(loadingElement)
})
})
// inject parent element if not present
function injectParentElement() {
if (!document.querySelectorAll("#linker-parent")[isMobile ? 2 : 1]) {
commonUtils.waitForElement("div:has( > div[data-testid='hero-rating-bar__user-rating'])", 10000, isMobile ? 2 : 1).then((element) => {
element.insertBefore(parentContainer, element.firstChild)
})
}
if (!document.querySelectorAll("#linker-parent")[!isMobile ? 2 : 1]) {
imdbPageUtils.element.mirrorElements(parentContainer, isMobile)
}
}
// 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)
// Stop the interval after 5 seconds
setTimeout(() => {
clearInterval(intervalId)
}, 5000)
if (!TMDB_API_KEY) {
await commonUtils.waitForElement("#linker-divider")
const tmdbElement = imdbPageUtils.element.createTmdbElement(`https://www.themoviedb.org/redirect?external_source=imdb_id&external_id=${imdbId}`)
parentContainer.appendChild(tmdbElement)
return
}
// fetch tmdb id
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))) {
// inject tmdb element and remove loading element
const tmdbElement = imdbPageUtils.element.createTmdbElement(tmdbData)
parentContainer.removeChild(loadingElement)
parentContainer.appendChild(tmdbElement)
} else {
// if no tmdb id then remove divider and loading element
parentContainer.removeChild(dividerElement)
parentContainer.removeChild(loadingElement)
}
}
async function imdbPersonPageInjector() {
// check is mobile
const isMobile = location.host.includes("m.imdb")
// extract imdb id from url
const path = location.pathname.split("/")
const imdbId = path[2] || null
// create parent and loading element
const parentContainer = commonUtils.element.createParentContainer()
const loadingElement = commonUtils.element.createLoadingElement()
// inject parent and loading element
window.addEventListener("load", () => {
commonUtils.waitForElement("div:has( > .starmeter-logo)", 10000, isMobile ? 2 : 1).then((element) => {
element.insertBefore(parentContainer, element.firstChild)
imdbPageUtils.element.mirrorElements(parentContainer, isMobile)
parentContainer.appendChild(loadingElement)
})
})
// inject parent element if not present
function injectParentElement() {
if (!document.querySelector("#linker-parent")) {
commonUtils.waitForElement("div:has( > .starmeter-logo)", 10000, isMobile ? 2 : 1).then((element) => {
element.insertBefore(parentContainer, element.firstChild)
})
}
if (!document.querySelectorAll("#linker-parent")[!isMobile ? 2 : 1]) {
imdbPageUtils.element.mirrorElements(parentContainer, isMobile)
}
}
// 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)
// Stop the interval after 5 seconds
setTimeout(() => {
clearInterval(intervalId)
}, 5000)
if (!TMDB_API_KEY) {
await commonUtils.waitForElement("#linker-loading")
const tmdbElement = imdbPageUtils.element.createTmdbElement(`https://www.themoviedb.org/redirect?external_source=imdb_id&external_id=${imdbId}`)
parentContainer.removeChild(loadingElement)
parentContainer.appendChild(tmdbElement)
return
}
// fetch tmdb id
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))) {
// inject tmdb element and remove loading element
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 {
// if no tmdb id then remove loading element
parentContainer.removeChild(loadingElement)
}
}
const tmdbTitlePageUtils = (() => {
function createLetterboxdElement(tmdbId, type) {
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
}
function createImdbContainer() {
const imdbContainer = document.createElement("div")
imdbContainer.id = "linker-imdb-container"
return imdbContainer
}
function createImdbElement(imdbId) {
const linkElement = document.createElement("a")
linkElement.href = `https://imdb.com/title/${imdbId}`
linkElement.target = "_blank"
linkElement.innerHTML = commonUtils.svg.ImdbSvg
return linkElement
}
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: {
createLetterboxdElement: createLetterboxdElement,
createImdbContainer: createImdbContainer,
createImdbElement: createImdbElement,
createImdbRatingElement: createImdbRatingElement,
},
}
})()
async function tmdbTitlePageInjector() {
// check is mobile
const isMobile = commonUtils.isMobile()
// extract tmdb id from url
const path = location.pathname.split("/")
const tmdbId = path[2].match(/\d+/)?.[0] || null
// inject elements that doesn't require any API calls
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) => {
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)
})
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}`)
const tmdbRes = await tmdbRawRes.json()
const imdbId = tmdbRes["imdb_id"] || null
// exit if no IMDb Id found
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 () => {
await commonUtils.waitForElement("#linker-imdb-container", 5000)
imdbContainer.insertBefore(imdbElement, loadingElement)
})
// scrape IMDb ratings
const [imdbRating, imdbNumRating] = await commonUtils.getImdbRating(imdbId)
// inject imdb rating element
const imdbRatingElement = tmdbTitlePageUtils.element.createImdbRatingElement(imdbRating, imdbNumRating)
await commonUtils.waitForElement("#linker-loading", 10000)
imdbContainer.removeChild(loadingElement)
if (imdbRatingElement) imdbContainer.appendChild(imdbRatingElement)
}
const tmdbPersonPageUtils = (() => {
function createLogoElement(id, type = "imdb") {
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
}
return {
element: {
createLogoElement: createLogoElement,
},
}
})()
async function tmdbPersonPageInjector() {
// extract tmdb id from url
const path = location.pathname.split("/")
const tmdbId = path[2].match(/\d+/)?.[0] || null
// inject elements that doesn't require any API calls
const letterboxdElement = tmdbPersonPageUtils.element.createLogoElement(tmdbId, "letterboxd")
commonUtils.waitForElement(".social_links", 10000).then((element) => {
element.insertBefore(letterboxdElement, element.firstChild)
})
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}`)
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) => {
await commonUtils.waitForElement("#linker-letterboxd-svg")
element.insertBefore(imdbElement, letterboxdElement.nextElementSibling)
})
}
}
function letterboxdTitlePageInjector() {
commonUtils.waitForElement(`.micro-button.track-event[data-track-action="IMDb"]`, 10000).then(async (element) => {
// 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
const [imdbRating, imdbNumRating] = await commonUtils.getImdbRating(imdbId)
// remove loading element
await commonUtils.waitForElement("#linker-loading", 10000)
element.removeChild(loadingElement)
element.style.display = originalDisplayStyle
// update imdb button
element.innerText = `IMDB${imdbRating ? ` | ${imdbRating}` : ""}${imdbNumRating !== undefined ? ` (${imdbNumRating})` : ""}`
})
}
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|genres)?)$/.test(currentURL)) {
// Letterboxd title page
GM_addStyle(letterboxdTitlePageCss)
letterboxdTitlePageInjector()
}
})()