Letterboxd Custom Images

Customize letterboxd posters and backdrops without letterboxd PATRON

// ==UserScript==
// @name         Letterboxd Custom Images
// @description  Customize letterboxd posters and backdrops without letterboxd PATRON
// @author       Tetrax-10
// @namespace    https://github.com/Tetrax-10/letterboxd-custom-images
// @version      4.2
// @license      MIT
// @match        *://*.letterboxd.com/*
// @connect      themoviedb.org
// @homepageURL  https://github.com/Tetrax-10/letterboxd-custom-images
// @supportURL   https://github.com/Tetrax-10/letterboxd-custom-images/issues
// @icon         https://tetrax-10.github.io/letterboxd-custom-images/assets/icon.png
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

;(() => {
    // Register menu command to open settings popup
    GM_registerMenuCommand("Settings", showSettingsPopup)

    let currentPage = null

    // Retrieve logged-in username from cookies
    const loggedInAs =
        document.cookie
            ?.split("; ")
            ?.find((row) => row.startsWith("letterboxd.signed.in.as="))
            ?.split("=")[1]
            ?.toLowerCase() || null // Add null check for safety

    // Retrieve useMobileSite setting from cookies
    const isMobile =
        document.cookie
            ?.split("; ")
            ?.find((row) => row.startsWith("useMobileSite"))
            ?.split("=")[1]
            ?.toLowerCase() === "yes" || false

    // Default configuration settings
    const defaultConfig = {
        TMDB_API_KEY: "",

        FILM_DISPLAY_MISSING_BACKDROP: true,
        FILM_SHORT_BACKDROP: false,

        LIST_AUTO_SCRAPE: false,
        LIST_SHORT_BACKDROP: false,

        USER_AUTO_SCRAPE: false,
        USER_SHORT_BACKDROP: false,
        CURRENT_USER_BACKDROP_ONLY: true,

        PERSON_AUTO_SCRAPE: false,
        PERSON_SHORT_BACKDROP: false,

        REVIEW_AUTO_SCRAPE: false,
        REVIEW_SHORT_BACKDROP: true,
    }

    // Initialize configuration with defaults if not already set
    try {
        const currentConfig = GM_getValue("CONFIG", {})
        if (currentConfig.FILM_SHORT_BACKDROP === undefined) {
            GM_setValue("CONFIG", defaultConfig)
            console.debug("Configuration initialized with default values.")
        } else {
            Object.entries(defaultConfig).forEach(([key, value]) => {
                if (currentConfig[key] === undefined) {
                    currentConfig[key] = value
                    console.debug("Configuration updated with default value for", key)
                }
            })
            GM_setValue("CONFIG", currentConfig)
        }
    } catch (error) {
        console.error("Error initializing configuration:", error)
    }

    // Function to get a specific configuration value
    function getConfigData(configId) {
        try {
            const config = GM_getValue("CONFIG", {})
            return config[configId]
        } catch (error) {
            console.error(`Error getting config data for ${configId}:`, error)
            return null
        }
    }

    // Function to set a specific configuration value
    function setConfigData(configId, value) {
        try {
            const config = GM_getValue("CONFIG", {})
            config[configId] = value
            GM_setValue("CONFIG", config)
            console.debug(`Config data for ${configId} updated.`)
        } catch (error) {
            console.error(`Error setting config data for ${configId}:`, error)
        }
    }

    // IndexedDB database variables
    let db = null
    let upgradeNeeded = false

    // Function to open the IndexedDB database
    function openDb() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open("ItemDataDB", 1)

            request.onupgradeneeded = (event) => {
                db = event.target.result
                if (!db.objectStoreNames.contains("itemData")) {
                    db.createObjectStore("itemData", { keyPath: "itemId" })
                    upgradeNeeded = true
                    console.debug("Database upgrade needed, object store created.")
                }
            }

            request.onsuccess = (event) => {
                db = event.target.result
                console.debug("Database connection established.")
                resolve(db)
            }

            request.onerror = (event) => {
                console.error("Error opening database:", event.target.errorCode)
                reject(event.target.errorCode)
            }
        })
    }

    // Function to get the database instance
    async function getDatabase() {
        if (!db)
            db = await openDb().catch((error) => {
                console.error("Failed to open database:", error)
                throw error
            })
        return db
    }

    // Initialize the database and migrate old data if needed
    getDatabase()
        .then(async () => {
            if (upgradeNeeded) {
                const ITEM_DATA = GM_getValue("ITEM_DATA", {})
                if (Object.keys(ITEM_DATA).length) {
                    await setItemData(ITEM_DATA).catch((error) => {
                        console.error("Failed to migrate old item data:", error)
                    })
                    console.debug("Old item data migrated.")
                }
            }

            // Clean up old stored values except for the configuration
            let allKeys = GM_listValues()
            for (let i = 0; i < allKeys.length; i++) {
                const key = allKeys[i]
                if (key !== "CONFIG") {
                    GM_deleteValue(key)
                    console.debug("Deleted old stored value:", key)
                }
            }
        })
        .catch((error) => {
            console.error("Failed to initialize database and migrate data:", error)
        })

    // Function to get item data from the database
    async function getItemData(itemId, dataType) {
        try {
            const db = await getDatabase()
            return new Promise((resolve, reject) => {
                const transaction = db.transaction("itemData", "readonly")
                const store = transaction.objectStore("itemData")

                if (!itemId) {
                    // Get all items if no itemId is provided
                    const request = store.getAll()
                    request.onsuccess = (event) => {
                        const items = event.target.result
                        const result = {}

                        items.forEach((item) => {
                            const id = item.itemId
                            delete item.itemId
                            result[id] = item
                        })
                        resolve(result)
                        console.debug("Retrieved all item data.")
                    }

                    request.onerror = (event) => {
                        console.error("Error retrieving all item data:", event.target.error)
                        reject(event.target.error)
                    }
                    return
                }

                const request = store.get(itemId)
                request.onsuccess = (event) => {
                    const itemData = event.target.result || {}
                    let value = itemData[dataType] ?? ""

                    // Handle specific data transformations based on dataType
                    switch (dataType) {
                        case "pu":
                        case "bu":
                            if (value.startsWith("t/")) {
                                value = `https://image.tmdb.org/t/p/original/${value.slice(2)}.jpg`
                            }
                            break
                        case "ty":
                            if (value === "m") {
                                value = "movie"
                            } else if (value === "t") {
                                value = "tv"
                            }
                            break
                    }

                    resolve(value)
                    console.debug(`Retrieved item data for ${itemId}, type: ${dataType}`)
                }

                request.onerror = (event) => {
                    console.error(`Error retrieving item data for ${itemId}:`, event.target.error)
                    reject(event.target.error)
                }
            })
        } catch (error) {
            console.error(`Error in getItemData for itemId ${itemId} and dataType ${dataType}:`, error)
            throw error
        }
    }

    // Function to set item data in the database
    async function setItemData(itemId, dataType, value) {
        try {
            const db = await getDatabase()
            return new Promise((resolve, reject) => {
                const transaction = db.transaction("itemData", "readwrite")
                const store = transaction.objectStore("itemData")

                store.get(typeof itemId === "object" ? "" : itemId).onsuccess = (event) => {
                    const itemData = event.target.result || {}

                    if (typeof itemId === "object") {
                        // If itemId is an object, assume it's a full data object to be inserted
                        Object.keys(itemId).forEach((id) => {
                            store.put({ itemId: id, ...itemId[id] })
                        })
                        console.debug("Bulk item data inserted.")
                        resolve()
                        return
                    }

                    const data = itemData || {}

                    // Handle specific data transformations based on dataType
                    if (!value) {
                        delete data[dataType]
                    } else {
                        switch (dataType) {
                            case "pu":
                            case "bu":
                                if (value.includes(".org/t/p/")) {
                                    const id = value.match(/\/([^\/]+)\.jpg$/)?.[1] ?? ""
                                    if (id) data[dataType] = `t/${id}`
                                } else {
                                    data[dataType] = value
                                }
                                break
                            case "ty":
                                if (value === "movie") {
                                    data[dataType] = "m"
                                } else {
                                    data[dataType] = "t"
                                }
                                break
                            default:
                                data[dataType] = value
                                break
                        }
                    }

                    store.put({ itemId, ...data })
                    console.debug(`Item data set for ${itemId}, type: ${dataType}`)
                    resolve()
                }

                transaction.onerror = (event) => {
                    console.error(`Error setting item data for ${itemId}:`, event.target.error)
                    reject(event.target.error)
                }
            })
        } catch (error) {
            console.error(`Error in setItemData for itemId ${itemId} and dataType ${dataType}:`, error)
            throw error
        }
    }

    GM_addStyle(`
        #lci-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;
            overflow: hidden;
        }
        #lci-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: ${isMobile ? "80%" : "50%"};
            max-height: 80vh;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
            -webkit-overflow-scrolling: touch;
        }
        #lci-settings-popup[type="imageurlpopup"] {
            width: 80%;
        }
        body.lci-no-scroll {
            overflow: hidden;
        }
        #lci-settings-popup label {
            color: rgb(207, 207, 207);
            font-weight: bold;
            font-size: 1.2em;
            margin-bottom: 10px;
        }
        #lci-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;
        }
        #lci-settings-popup button {
            background-color: rgb(76, 175, 80);
            color: white;
            padding: 10px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 16px;
            margin-bottom: 10px;
        }
        #lci-settings-popup .import-export-container {
            display: flex;
            justify-content: space-between;
            margin-top: 20px;
        }
        .lci-checkbox-container {
            display: flex;
            align-items: center;
        }
        .lci-checkbox-container input[type="checkbox"] {
            appearance: none;
            background-color: rgb(32, 36, 44);
            border: 1px solid rgb(207, 207, 207);
            border-radius: 4px;
            width: 20px;
            height: 20px;
            cursor: pointer;
            position: relative;
            margin-right: 10px;
            outline: none;
        }
        .lci-checkbox-container input[type="checkbox"]:checked {
            background-color: rgb(76, 175, 80);
            border: none;
        }
        .lci-checkbox-container input[type="checkbox"]:checked::after {
            content: '\\2714'; /* Unicode checkmark */
            color: white;
            font-size: 1em;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }
        .lci-checkbox-container label {
            color: rgb(207, 207, 207);
            font-weight: bold;
            font-size: 1.2em;
        }
        #lci-image-grid {
            display: grid;
            grid-template-columns: repeat(${isMobile ? "1" : "3"}, 1fr);
            gap: 15px;
            margin-top: 20px;
        }
        #lci-image-grid.lci-poster-grid {
            grid-template-columns: repeat(${isMobile ? "2" : "5"}, 1fr);
        }
        .lci-image-item {
            cursor: pointer;
            border-radius: 8px;
            overflow: hidden;
            border: 2px solid transparent;
            transition: border-color 0.3s;
            position: relative;
        }
        .lci-image-item img {
            width: 100%;
            height: auto;
            display: block;
        }
        .lci-image-item:hover {
            border-color: rgb(76, 175, 80);
        }
        .lci-tooltip {
            visibility: hidden;
            background-color: rgba(32, 36, 44, 0.8);
            color: white;
            text-align: center;
            padding: 5px 10px;
            border-radius: 4px;
            position: absolute;
            bottom: 5px;
            left: 50%;
            transform: translateX(-50%);
            width: auto;
            white-space: nowrap;
            font-size: 0.9em;
            opacity: 0;
            transition: opacity 0.3s ease-in-out;
            box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3);
        }
        .lci-image-item:hover .lci-tooltip {
            visibility: visible;
            opacity: 1;
        }
        #lci-loading-spinner {
            border: 4px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top: 4px solid rgb(76, 175, 80);
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 20px auto;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        `)

    async function showImageUrlPopup({ itemId, targetedFilmId, filmElementSelector, mode = "backdrop" } = {}) {
        const modeName = mode === "poster" ? "Poster" : "Backdrop"
        const imageUrlKey = mode === "poster" ? "pu" : "bu"
        let hasInputValueChanged = false

        // Add the no-scroll class to the body
        document.body.classList.add("lci-no-scroll")

        // Create overlay for the popup
        const overlay = document.createElement("div")
        overlay.id = "lci-settings-overlay"
        overlay.onclick = (e) => {
            if (e.target === overlay) closePopup(overlay)
        }

        // Create popup container
        const popup = document.createElement("div")
        popup.id = "lci-settings-popup"
        popup.setAttribute("type", "imageurlpopup")

        // Add label for the input field
        const label = document.createElement("label")
        label.textContent = `Enter ${modeName} Image URL:`
        popup.appendChild(label)

        // Create input field for the URL
        const input = document.createElement("input")
        input.type = "text"
        try {
            input.value = await getItemData(itemId, imageUrlKey) // Retrieve existing image URL
        } catch (error) {
            console.error(`Failed to retrieve ${modeName} URL:`, error) // Log error if retrieval fails
            input.value = ""
        }
        input.placeholder = `${modeName} Image URL`
        if (!isMobile) input.autofocus = true
        input.oninput = () => {
            hasInputValueChanged = true
        }
        popup.appendChild(input)

        overlay.appendChild(popup)
        document.body.appendChild(overlay)

        // Focus on the input field after a short delay
        setTimeout(() => {
            if (!isMobile) input.focus()
        }, 100)

        async function updateImage(imageUrl, mode) {
            if (mode === "poster") {
                document.querySelectorAll(`.film-poster[data-film-link*="film/${itemId.slice(2)}"] .image`).forEach((posterImageElement) => {
                    injectPoster(posterImageElement, imageUrl)
                })
            } else if (mode === "backdrop" && currentPage !== "other") {
                const header = await waitForElement("#header")
                injectBackdrop(header, imageUrl, getConfigData(`${currentPage.toUpperCase()}_SHORT_BACKDROP`) ? ["shortbackdropped", "-crop"] : [])
            }
        }

        function closePopup(overlay) {
            if (hasInputValueChanged) {
                const imageUrl = document.querySelector(`input[placeholder="${modeName} Image URL"]`)?.value?.trim() || ""
                if (imageUrl) updateImage(imageUrl, mode)
                setItemData(itemId, imageUrlKey, imageUrl).catch((err) => {
                    console.error(`Failed to set ${modeName} URL:`, err)
                })
            }

            document.body.removeChild(overlay)
            // Remove the no-scroll class from the body
            document.body.classList.remove("lci-no-scroll")
        }

        // Exit if TMDB API key is not configured
        if (!getConfigData("TMDB_API_KEY")) return

        // Show loading spinner
        const spinner = document.createElement("div")
        spinner.id = "lci-loading-spinner"
        popup.appendChild(spinner)

        let filmId, tmdbIdType, tmdbId

        try {
            if (targetedFilmId) {
                // any item if but with targetedFilmId
                // "Set as item backdrop" context menu
                const targetedFilmTmdbId = await getItemData(targetedFilmId, "tId")
                if (targetedFilmTmdbId) {
                    filmId = targetedFilmId
                } else {
                    await scrapeFilmPage(targetedFilmId.slice(2))
                    filmId = targetedFilmId
                }
            } else if (itemId.startsWith("f/")) {
                // "Set film backdrop/poster" context menu
                const itemTmdbId = await getItemData(itemId, "tId")
                if (itemTmdbId) {
                    filmId = itemId
                } else {
                    await scrapeFilmPage(itemId.slice(2))
                    filmId = itemId
                }
            } else if (!itemId.startsWith("f/")) {
                // Set item backdrop menu
                const itemFilmId = await getItemData(itemId, "fId")
                const itemFilmTmdbId = await getItemData(itemFilmId, "tId")

                if (itemFilmTmdbId) {
                    filmId = itemFilmId
                } else {
                    await scrapeFilmLinkElement(filmElementSelector, true, itemId)
                    filmId = itemFilmId
                }
            }

            // Retrieve TMDB ID type and ID
            tmdbIdType = await getItemData(filmId, "ty")
            tmdbId = await getItemData(filmId, "tId")

            if (!tmdbIdType || !tmdbId) {
                console.error("TMDB ID or ID type is missing for filmId:", filmId) // Log missing ID error
                return
            }

            const imageGrid = document.createElement("div")
            imageGrid.id = "lci-image-grid"
            if (mode === "poster") imageGrid.className = "lci-poster-grid"
            popup.appendChild(imageGrid)

            async function getAllTmdbImages(tmdbIdType, tmdbId) {
                try {
                    const tmdbRawRes = await fetch(
                        `https://api.themoviedb.org/3/${tmdbIdType}/${tmdbId}/images?api_key=${getConfigData("TMDB_API_KEY")}`
                    )

                    if (!tmdbRawRes.ok) {
                        console.error(`Failed to fetch images from TMDB: ${tmdbRawRes.status} ${tmdbRawRes.statusText}`)
                        return []
                    }

                    const tmdbRes = await tmdbRawRes.json()

                    const images = tmdbRes[mode === "poster" ? "posters" : "backdrops"] || []

                    const localeImages = []
                    const nonLocaleImages = []

                    // Separate images into locale and non-locale
                    images.forEach((image) => {
                        if (!image.iso_639_1) {
                            nonLocaleImages.push(image)
                        } else {
                            localeImages.push(image)
                        }
                    })

                    // Group images by language
                    const postersByLanguage = localeImages.reduce((acc, image) => {
                        const language = image.iso_639_1
                        if (!acc[language]) acc[language] = []
                        acc[language].push(image)
                        return acc
                    }, {})

                    // Sort images by number of images in each language
                    const sortedLanguages = Object.keys(postersByLanguage).sort((a, b) => {
                        return postersByLanguage[b].length - postersByLanguage[a].length
                    })

                    const sortedLocaleImages = sortedLanguages.flatMap((language) => postersByLanguage[language])

                    return mode === "poster" ? [...sortedLocaleImages, ...nonLocaleImages] : [...nonLocaleImages, ...sortedLocaleImages]
                } catch (error) {
                    console.error("Error in getAllTmdbImages:", error)
                    return []
                }
            }

            let allImageUrls = await getAllTmdbImages(tmdbIdType, tmdbId)
            let currentRow = 0
            const columnsToLoad = isMobile ? 1 : mode === "poster" ? 5 : 3
            const rowsToLoad = 15 / columnsToLoad

            // Remove spinner and load initial images
            await loadImages()
            spinner.remove()

            async function loadImages() {
                const nextImages = allImageUrls.slice(currentRow * columnsToLoad, (currentRow + rowsToLoad) * columnsToLoad)
                nextImages.forEach((image) => {
                    const imageUrl = `https://image.tmdb.org/t/p/original${image.file_path}`

                    const imageItem = document.createElement("div")
                    imageItem.className = "lci-image-item"
                    if (imageUrl === input.value) imageItem.style.borderColor = "#40bcf4"

                    const img = document.createElement("img")
                    img.src = imageUrl.replace("original", mode === "poster" ? "w342" : "w780")
                    imageItem.appendChild(img)

                    // Create tooltip with image metadata
                    const tooltip = document.createElement("div")
                    tooltip.className = "lci-tooltip"
                    tooltip.textContent = `${image.width && image.height ? `${image.width} × ${image.height}` : ""}${
                        image.iso_639_1 ? ` • ${image.iso_639_1}` : ""
                    }`
                    if (tooltip.textContent) imageItem.appendChild(tooltip)

                    imageItem.onclick = () => {
                        hasInputValueChanged = false
                        updateImage(imageUrl, mode)
                        setItemData(itemId, imageUrlKey, imageUrl).catch((err) => {
                            console.error(`Failed to set ${modeName} URL:`, err)
                        })
                        closePopup(overlay)
                    }
                    imageGrid.appendChild(imageItem)
                })

                currentRow += rowsToLoad
            }

            // Auto-load more images when scrolling to the bottom using IntersectionObserver
            const observer = new IntersectionObserver((entries) => {
                if (entries[0].isIntersecting) {
                    if (currentRow * columnsToLoad < allImageUrls.length) {
                        loadImages()
                    } else {
                        // Disconnect the observer when all images are loaded
                        observer.disconnect()
                    }
                }
            })

            // Create a sentinel element at the bottom of the image grid to trigger loading
            const sentinel = document.createElement("div")
            sentinel.id = "lci-sentinel"
            popup.appendChild(sentinel)

            observer.observe(sentinel)
        } catch (error) {
            console.error("An error occurred while setting up the image URL popup:", error)
        }
    }

    function showSettingsPopup() {
        // Add the no-scroll class to the body
        document.body.classList.add("lci-no-scroll")

        // Create overlay for the settings popup
        const overlay = document.createElement("div")
        overlay.id = "lci-settings-overlay"
        overlay.onclick = (e) => {
            if (e.target === overlay) closePopup(overlay)
        }

        const popup = document.createElement("div")
        popup.id = "lci-settings-popup"

        // Helper function to create label elements
        function createLabelElement(text) {
            const label = document.createElement("label")
            label.textContent = text
            popup.appendChild(label)
        }

        // Helper function to create input elements
        function createInputElement(name, id, placeholder) {
            createLabelElement(name)

            const input = document.createElement("input")
            input.type = "text"
            input.value = getConfigData(id)
            input.placeholder = placeholder
            input.oninput = (e) => {
                const value = e.target.value?.trim()
                setConfigData(id, value).catch((err) => {
                    console.error(`Failed to set config data for ${id}:`, err) // Log error if setting data fails
                })
            }
            popup.appendChild(input)
        }

        // Helper function to create checkbox elements
        function createCheckboxElement(labelText, id) {
            const container = document.createElement("div")
            container.className = "lci-checkbox-container"

            const checkbox = document.createElement("input")
            checkbox.type = "checkbox"
            checkbox.checked = getConfigData(id)
            checkbox.onchange = (e) => {
                setConfigData(id, e.target.checked).catch((err) => {
                    console.error(`Failed to set config data for ${id}:`, err) // Log error if setting data fails
                })
            }
            container.appendChild(checkbox)

            const label = document.createElement("label")
            label.textContent = labelText
            container.appendChild(label)

            popup.appendChild(container)
        }

        function createSpaceComponent() {
            const space = document.createElement("div")
            space.style.marginBottom = "10px"
            popup.appendChild(space)
        }

        // Export settings to a JSON file
        async function exportSettings() {
            try {
                const settings = {
                    CONFIG: GM_getValue("CONFIG", {}),
                    ITEM_DATA: await getItemData(),
                }

                // Create a data URL for the JSON file
                const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(settings, null, 2))
                const downloadAnchor = document.createElement("a")
                downloadAnchor.setAttribute("href", dataStr)
                downloadAnchor.setAttribute("download", "lciSettings.json")
                document.body.appendChild(downloadAnchor)
                downloadAnchor.click()
                document.body.removeChild(downloadAnchor)
            } catch (error) {
                console.error("Failed to export settings:", error) // Log error if export fails
            }
        }

        // Import settings from a JSON file
        function importSettings(event) {
            const file = event.target.files[0]
            if (!file) return

            const reader = new FileReader()
            reader.onload = (e) => {
                const content = e.target.result
                try {
                    const settings = JSON.parse(content)

                    GM_setValue("CONFIG", settings.CONFIG || {})
                    setItemData(settings.ITEM_DATA || {}).catch((err) => {
                        console.error("Failed to import item data:", err) // Log error if importing data fails
                    })

                    // Refresh the popup to reflect imported settings
                    closePopup(overlay)
                    showSettingsPopup()
                } catch (err) {
                    console.error("Failed to import settings:", err) // Log error if JSON parsing fails
                    alert("Failed to import settings: Invalid JSON file.")
                }
            }
            reader.onerror = (err) => {
                console.error("Error reading import file:", err) // Log error if file reading fails
                alert("Failed to read import file.")
            }
            reader.readAsText(file)
        }

        // UI Elements
        createInputElement(
            "Enter your TMDB API key to display missing film backdrops and get the ability to select backdrops from UI:",
            "TMDB_API_KEY",
            "TMDB API Key"
        )
        createSpaceComponent()

        createLabelElement("Film Page:")
        createCheckboxElement("Display missing backdrop for less popular films", "FILM_DISPLAY_MISSING_BACKDROP")
        createCheckboxElement("Short backdrops", "FILM_SHORT_BACKDROP")
        createSpaceComponent()

        createLabelElement("List Page:")
        createCheckboxElement("Auto scrape backdrops", "LIST_AUTO_SCRAPE")
        createCheckboxElement("Short backdrops", "LIST_SHORT_BACKDROP")
        createSpaceComponent()

        createLabelElement("User Page:")
        createCheckboxElement("Auto scrape backdrops", "USER_AUTO_SCRAPE")
        createCheckboxElement("Short backdrops", "USER_SHORT_BACKDROP")
        createCheckboxElement("Don't scrape backdrops for other users", "CURRENT_USER_BACKDROP_ONLY")
        createSpaceComponent()

        createLabelElement("Person Page:")
        createCheckboxElement("Auto scrape backdrops", "PERSON_AUTO_SCRAPE")
        createCheckboxElement("Short backdrops", "PERSON_SHORT_BACKDROP")
        createSpaceComponent()

        createLabelElement("Review Page:")
        createCheckboxElement("Auto scrape backdrops", "REVIEW_AUTO_SCRAPE")
        createCheckboxElement("Short backdrops", "REVIEW_SHORT_BACKDROP")
        createSpaceComponent()

        // Import/Export Buttons
        const importExportContainer = document.createElement("div")
        importExportContainer.className = "import-export-container"

        const exportButton = document.createElement("button")
        exportButton.textContent = "Export Settings"
        exportButton.onclick = exportSettings

        const importButton = document.createElement("button")
        importButton.textContent = "Import Settings"
        importButton.onclick = () => {
            const fileInput = document.createElement("input")
            fileInput.type = "file"
            fileInput.accept = ".json"
            fileInput.onchange = importSettings
            fileInput.click()
        }

        importExportContainer.appendChild(exportButton)
        importExportContainer.appendChild(importButton)
        popup.appendChild(importExportContainer)

        overlay.appendChild(popup)
        document.body.appendChild(overlay)

        function closePopup(overlay) {
            document.body.removeChild(overlay)
            // Remove the no-scroll class from the body
            document.body.classList.remove("lci-no-scroll")
        }
    }

    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 getTmdbBackdrop(tmdbIdType, tmdbId) {
        if (!getConfigData("TMDB_API_KEY")) {
            console.error("TMDB API key is not configured.") // Log missing API key
            return null
        }

        try {
            const tmdbRawRes = await fetch(`https://api.themoviedb.org/3/${tmdbIdType}/${tmdbId}/images?api_key=${getConfigData("TMDB_API_KEY")}`)
            if (!tmdbRawRes.ok) {
                console.error(`Failed to fetch TMDB backdrops: ${tmdbRawRes.statusText}`) // Log HTTP error
                return null
            }

            const tmdbRes = await tmdbRawRes.json()
            const imageId = tmdbRes.backdrops?.[0]?.file_path

            return imageId ? `https://image.tmdb.org/t/p/original${imageId}` : null
        } catch (error) {
            console.error("Error fetching TMDB backdrop:", error) // General error catch
            return null
        }
    }

    async function isDefaultBackdropAvailable(dom) {
        let defaultBackdropElement
        if (dom) {
            defaultBackdropElement = dom.querySelector("#backdrop")
        } else {
            defaultBackdropElement = document.querySelector("#backdrop")
            if (!defaultBackdropElement) {
                try {
                    defaultBackdropElement = await waitForElement("#backdrop", 100)
                } catch (error) {
                    console.error("Failed to find default backdrop element:", error) // Log element not found
                    return false
                }
            }
        }

        const defaultBackdropUrl =
            defaultBackdropElement?.dataset?.backdrop2x ||
            defaultBackdropElement?.dataset?.backdrop ||
            defaultBackdropElement?.dataset?.backdropMobile

        if (defaultBackdropUrl?.includes("https://a.ltrbxd.com/resized/sm/upload")) {
            return defaultBackdropUrl
        }
        return false
    }

    async function extractBackdropUrlFromLetterboxdFilmPage(filmId, dom, shouldScrape = true) {
        try {
            const filmBackdropUrl = await isDefaultBackdropAvailable(dom)

            // Get TMDB ID and type
            let tmdbElement
            if (dom) {
                tmdbElement = dom.querySelector(`.micro-button.track-event[data-track-action="TMDb"]`)
            } else {
                tmdbElement = await waitForElement(`.micro-button.track-event[data-track-action="TMDb"]`, 5000)
            }

            const tmdbIdType = tmdbElement.href?.match(/\/(movie|tv)\/(\d+)\//)?.[1] ?? null
            const tmdbId = tmdbElement.href?.match(/\/(movie|tv)\/(\d+)\//)?.[2] ?? null

            if (tmdbIdType && tmdbId) {
                await setItemData(filmId, "ty", tmdbIdType)
                await setItemData(filmId, "tId", tmdbId)
            }

            if (!filmBackdropUrl && !document.querySelector(`#lci-settings-popup[type="imageurlpopup"]`) && shouldScrape) {
                return await getTmdbBackdrop(tmdbIdType, tmdbId)
            }

            return filmBackdropUrl
        } catch (error) {
            console.error("Error extracting backdrop URL from Letterboxd film page:", error) // General error catch
            return null
        }
    }

    function scrapeFilmPage(filmName) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://letterboxd.com/film/${filmName}/`,
                onload: async function (response) {
                    try {
                        const parser = new DOMParser()
                        const dom = parser.parseFromString(response.responseText, "text/html")

                        // Resolve with URL and cache status
                        resolve([await extractBackdropUrlFromLetterboxdFilmPage(`f/${filmName}`, dom), false])
                    } catch (error) {
                        console.error("Error parsing or extracting backdrop from Letterboxd page:", error) // General error catch
                        resolve([null, false])
                    }
                },
                onerror: function (error) {
                    console.error(`Can't scrape Letterboxd page: ${filmName}`, error) // Log scraping error
                    resolve([null, false])
                },
            })
        })
    }

    async function scrapeFilmLinkElement(selector, shouldScrape, itemId) {
        try {
            const firstPosterElement = await waitForElement(selector, 2000)
            if (!firstPosterElement) return [null, false]

            const filmName = firstPosterElement.href?.match(/\/film\/([^\/]+)/)?.[1]
            const filmId = `f/${filmName}`

            if (!itemId.startsWith("f/")) await setItemData(itemId, "fId", filmId)

            const cacheBackdrop = await getItemData(filmId, "bu")

            if (cacheBackdrop) {
                return [cacheBackdrop, true]
            } else if (!shouldScrape) {
                return [null, false]
            } else {
                return await scrapeFilmPage(filmName)
            }
        } catch (error) {
            console.error("Error scraping film link element:", error) // General error catch
            return [null, false]
        }
    }

    function injectPoster(posterImageElement, imageUrl) {
        let posterSize = posterImageElement.src.includes("0-70-0-105-crop") ? "w154" : "original"
        posterSize = posterImageElement.src.includes("0-150-0-225-crop") ? "w342" : posterSize
        posterSize = posterImageElement.src.includes("0-230-0-345-crop") ? "w500" : posterSize

        imageUrl = imageUrl.replace("original", posterSize)
        posterImageElement.src = imageUrl
        posterImageElement.srcset = imageUrl
    }

    function injectBackdrop(header, backdropUrl, attributes = []) {
        try {
            // Get or inject backdrop containers
            const backdropContainer =
                // For patron users who already have a backdrop
                document.querySelector(".backdrop-container") ||
                // For non-patron users
                Object.assign(document.createElement("div"), { className: "backdrop-container" })

            // Inject necessary classes
            document.body.classList.add("backdropped", "backdrop-loaded", ...attributes)
            document.getElementById("content")?.classList.add("-backdrop")

            // Ensure .-backdrop is added to #content if missed before
            const intervalId = setInterval(() => document.getElementById("content")?.classList.add("-backdrop"), 100)
            setTimeout(() => clearInterval(intervalId), 5000)

            // Inject backdrop child
            backdropContainer.innerHTML = `
                <div id="backdrop" class="backdrop-wrapper -loaded" data-backdrop="${backdropUrl}" data-backdrop2x="${backdropUrl}" data-backdrop-mobile="${backdropUrl}" data-offset="0">
                    <div class="backdropimage js-backdrop-image" style="background-image: url(${backdropUrl}); background-position: center 0px;"></div>
                    <div class="backdropmask js-backdrop-fade"></div>
                </div>`

            header.before(backdropContainer)
        } catch (error) {
            console.error("Error injecting backdrop:", error) // General error catch
        }
    }

    async function injectContextMenuToAllFilmPosterItems({ itemId, name } = {}) {
        if (isMobile) return

        function addFilmOption({ contextmenu, className, name, onClick = () => {}, itemId = undefined } = {}) {
            try {
                const activityMenuElement = contextmenu.querySelector(".fm-show-activity")
                const filmName = activityMenuElement?.firstElementChild?.href?.match(/\/film\/([^\/]+)/)?.[1]

                const imageMenuElement = document.createElement("li")
                imageMenuElement.classList.add(className, "popmenu-textitem", "-centered")

                const imageMenuLinkElement = document.createElement("a")
                imageMenuLinkElement.style.cursor = "pointer"
                imageMenuLinkElement.textContent = name
                imageMenuElement.onclick = () => {
                    contextmenu.setAttribute("hidden", "")
                    onClick(filmName, itemId)
                }

                imageMenuElement.appendChild(imageMenuLinkElement)
                activityMenuElement.parentNode.insertBefore(imageMenuElement, activityMenuElement)
            } catch (error) {
                console.error("Error adding film option to context menu:", error) // General error catch
            }
        }

        try {
            const observer = new MutationObserver(() => {
                if (!document.querySelector("body > .popmenu.film-poster-popmenu:not([contextmenu-processed])")) return

                const allContextmenu = document.querySelectorAll(`body > .popmenu.film-poster-popmenu:not([contextmenu-processed])`)
                for (const contextmenu of allContextmenu) {
                    contextmenu.setAttribute("contextmenu-processed", "")
                    if (itemId) {
                        addFilmOption({
                            contextmenu,
                            className: "fm-set-as-item-backdrop",
                            name: `Set as ${name} backdrop`,
                            onClick: (filmName, itemId) => showImageUrlPopup({ itemId: itemId, targetedFilmId: `f/${filmName}` }),
                            itemId: itemId,
                        })
                    }
                    addFilmOption({
                        contextmenu,
                        className: "fm-set-film-backdrop",
                        name: "Set film backdrop",
                        onClick: (filmName) => showImageUrlPopup({ itemId: `f/${filmName}` }),
                    })
                    addFilmOption({
                        contextmenu,
                        className: "fm-set-film-poster",
                        name: "Set film poster",
                        onClick: (filmName) => showImageUrlPopup({ itemId: `f/${filmName}`, mode: "poster" }),
                    })
                }
            })

            await waitForElement("body")
            observer.observe(document.body, { childList: true })
        } catch (error) {
            console.error("Error injecting context menu to all film poster items:", error) // General error catch
        }
    }

    async function filmPageMenuInjector({ filmId, mode } = {}) {
        const yourActivityMenuItem = await waitForElement(`ul.js-actions-panel > li:has(a[href*="/activity/"])`, 5000)

        const setFilmImageMenuItem = document.createElement("li")

        const anchor = document.createElement("a")
        anchor.textContent = `Set film ${mode}`
        anchor.style.cursor = "pointer"
        anchor.onclick = () => showImageUrlPopup({ itemId: filmId, mode })

        setFilmImageMenuItem.appendChild(anchor)
        yourActivityMenuItem.parentNode.insertBefore(setFilmImageMenuItem, yourActivityMenuItem)
    }

    async function filmPageInjector() {
        try {
            const filmId = `f/${location.pathname.split("/")?.[2]}`

            const header = await waitForElement("#header")
            filmPageMenuInjector({ filmId, mode: "backdrop" })
            filmPageMenuInjector({ filmId, mode: "poster" })
            injectContextMenuToAllFilmPosterItems()

            const cacheBackdrop = await getItemData(filmId, "bu")

            async function scrapeTmdbIdAndType() {
                try {
                    // Extracts TMDB ID and type
                    const tmdbElement = await waitForElement(`.micro-button.track-event[data-track-action="TMDb"]`, 5000)
                    const tmdbIdType = tmdbElement.href?.match(/\/(movie|tv)\/(\d+)\//)?.[1] ?? null
                    const tmdbId = tmdbElement.href?.match(/\/(movie|tv)\/(\d+)\//)?.[2] ?? null

                    if (tmdbIdType && tmdbId) {
                        await setItemData(filmId, "ty", tmdbIdType)
                        await setItemData(filmId, "tId", tmdbId)
                    }
                } catch (error) {
                    console.error("Error scraping TMDB ID and type:", error) // General error catch
                }
            }

            if (cacheBackdrop) {
                // Inject backdrop
                injectBackdrop(header, cacheBackdrop, getConfigData("FILM_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                scrapeTmdbIdAndType()
                return
            }

            // If original backdrop is available then return
            if (await isDefaultBackdropAvailable()) {
                scrapeTmdbIdAndType()
                return
            }

            if (getConfigData("TMDB_API_KEY") && getConfigData("FILM_DISPLAY_MISSING_BACKDROP")) {
                const backdropUrl = await extractBackdropUrlFromLetterboxdFilmPage(filmId)

                // Inject backdrop
                if (backdropUrl) {
                    injectBackdrop(header, backdropUrl, getConfigData("FILM_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                    await setItemData(filmId, "bu", backdropUrl)
                }
            } else {
                await extractBackdropUrlFromLetterboxdFilmPage(filmId, undefined, false)
            }
        } catch (error) {
            console.error("Error in film page injector:", error) // General error catch
        }
    }

    async function userPageMenuInjector(userId, filmElementSelector) {
        const copyLinkMenuItem = await waitForElement(`.menuitem:has(> button[data-menuitem-trigger="clipboard"])`, 5000)

        const setUserBackdropMenuItem = document.createElement("div")
        setUserBackdropMenuItem.classList.add("menuitem", "-trigger", "-has-icon", "js-menuitem")
        setUserBackdropMenuItem.role = "none"

        const setUserBackdropMenuButton = document.createElement("button")
        setUserBackdropMenuButton.type = "button"
        setUserBackdropMenuButton.role = "menuitem"
        setUserBackdropMenuButton.setAttribute("data-dismiss", "dropdown")
        setUserBackdropMenuButton.onclick = () => showImageUrlPopup({ itemId: userId, filmElementSelector: filmElementSelector })
        setUserBackdropMenuButton.innerHTML = `
            <svg class="glyph" role="presentation" width="8" height="8" viewBox="0 0 16 16" style="margin-bottom: 6px">
                <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
                    <g id="Icon-Set" sketch:type="MSLayerGroup" transform="translate(-360.000000, -99.000000)" fill="currentColor">
                        <path
                            d="M368,109 C366.896,109 366,108.104 366,107 C366,105.896 366.896,105 368,105 C369.104,105 370,105.896 370,107 C370,108.104 369.104,109 368,109 L368,109 Z M368,103 C365.791,103 364,104.791 364,107 C364,109.209 365.791,111 368,111 C370.209,111 372,109.209 372,107 C372,104.791 370.209,103 368,103 L368,103 Z M390,116.128 L384,110 L374.059,120.111 L370,116 L362,123.337 L362,103 C362,101.896 362.896,101 364,101 L388,101 C389.104,101 390,101.896 390,103 L390,116.128 L390,116.128 Z M390,127 C390,128.104 389.104,129 388,129 L382.832,129 L375.464,121.535 L384,112.999 L390,118.999 L390,127 L390,127 Z M364,129 C362.896,129 362,128.104 362,127 L362,126.061 L369.945,118.945 L380.001,129 L364,129 L364,129 Z M388,99 L364,99 C361.791,99 360,100.791 360,103 L360,127 C360,129.209 361.791,131 364,131 L388,131 C390.209,131 392,129.209 392,127 L392,103 C392,100.791 390.209,99 388,99 L388,99 Z"
                            id="image-picture"
                            sketch:type="MSShapeGroup"
                        ></path>
                    </g>
                </g>
            </svg>
            <span class="label">Set user backdrop</span>
            `

        setUserBackdropMenuItem.appendChild(setUserBackdropMenuButton)
        copyLinkMenuItem.parentNode.insertBefore(setUserBackdropMenuItem, copyLinkMenuItem.nextSibling)
    }

    async function userPageInjector() {
        try {
            const userName = location.pathname.split("/")?.[1]?.toLowerCase()
            const userId = `u/${userName}`
            const filmElementSelector = "#favourites .poster-list > li:first-child a"

            if (getConfigData("CURRENT_USER_BACKDROP_ONLY") && userName !== loggedInAs) return

            const cacheBackdrop = await getItemData(userId, "bu")
            const header = await waitForElement("#header")
            userPageMenuInjector(userId, filmElementSelector)
            injectContextMenuToAllFilmPosterItems({ itemId: userId, name: "user" })

            if (cacheBackdrop) {
                injectBackdrop(header, cacheBackdrop, getConfigData("USER_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                await scrapeFilmLinkElement(filmElementSelector, false, userId)
                return
            }

            if (await isDefaultBackdropAvailable()) {
                await scrapeFilmLinkElement(filmElementSelector, false, userId)
                return
            }

            const [scrapedImage, isCached] = await scrapeFilmLinkElement(filmElementSelector, getConfigData("USER_AUTO_SCRAPE"), userId)

            if (scrapedImage) {
                injectBackdrop(header, scrapedImage, getConfigData("USER_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])

                if (!isCached) {
                    await setItemData(userId, "bu", scrapedImage)
                }
            }
        } catch (error) {
            console.error("Error in userPageInjector:", error)
        }
    }

    async function listPageMenuInjector(listId, filmElementSelector) {
        const likeMenuItem = await waitForElement("li.like-link-target", 5000)

        const setListBackdropMenuItem = document.createElement("li")

        const setListBackdropLink = document.createElement("a")
        setListBackdropLink.textContent = "Set list backdrop"
        setListBackdropLink.style.cursor = "pointer"
        setListBackdropLink.onclick = () => showImageUrlPopup({ itemId: listId, filmElementSelector: filmElementSelector })

        setListBackdropMenuItem.appendChild(setListBackdropLink)
        likeMenuItem.parentNode.insertBefore(setListBackdropMenuItem, likeMenuItem.nextSibling)
    }

    async function listPageInjector() {
        try {
            const listId = `l/${location.pathname.split("/")?.[1]?.toLowerCase()}/${location.pathname.split("/")?.[3]}`
            const filmElementSelector = ".poster-list > li:first-child a"

            const cacheBackdrop = await getItemData(listId, "bu")
            const header = await waitForElement("#header")
            listPageMenuInjector(listId, filmElementSelector)
            injectContextMenuToAllFilmPosterItems({ itemId: listId, name: "list" })

            if (!getConfigData("LIST_SHORT_BACKDROP")) {
                document.body.classList.remove("shortbackdropped", "-crop")
            }

            if (cacheBackdrop) {
                injectBackdrop(header, cacheBackdrop, getConfigData("LIST_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                await scrapeFilmLinkElement(filmElementSelector, false, listId)
                return
            }

            if (await isDefaultBackdropAvailable()) {
                await scrapeFilmLinkElement(filmElementSelector, false, listId)
                return
            }

            const [scrapedImage, isCached] = await scrapeFilmLinkElement(filmElementSelector, getConfigData("LIST_AUTO_SCRAPE"), listId)

            if (scrapedImage) {
                injectBackdrop(header, scrapedImage, getConfigData("LIST_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])

                if (!isCached) {
                    await setItemData(listId, "bu", scrapedImage)
                }
            }
        } catch (error) {
            console.error("Error in listPageInjector:", error)
        }
    }

    async function personPageMenuInjector(personId, filmElementSelector) {
        const personImageElement = await waitForElement(".person-image", 5000)

        const setPersonBackdropButton = document.createElement("button")
        setPersonBackdropButton.style.borderRadius = "4px"
        setPersonBackdropButton.style.width = "100%"
        setPersonBackdropButton.style.border = "1px solid hsla(0,0%,100%,0.25)"
        setPersonBackdropButton.style.backgroundColor = "transparent"
        setPersonBackdropButton.style.color = "#9ab"
        setPersonBackdropButton.style.height = "40px"
        setPersonBackdropButton.style.cursor = "pointer"
        setPersonBackdropButton.style.fontFamily = "Graphik-Regular-Web, sans-serif"
        setPersonBackdropButton.textContent = "Set person backdrop"
        setPersonBackdropButton.addEventListener("mouseenter", () => {
            setPersonBackdropButton.style.color = "#def"
        })
        setPersonBackdropButton.addEventListener("mouseleave", () => {
            setPersonBackdropButton.style.color = "#9ab"
        })
        setPersonBackdropButton.onclick = () => showImageUrlPopup({ itemId: personId, filmElementSelector: filmElementSelector })

        personImageElement.parentNode.insertBefore(setPersonBackdropButton, personImageElement.nextSibling)
    }

    async function personPageInjector() {
        try {
            const personId = `p/${location.pathname.split("/")?.[2]}`
            const filmElementSelector = ".grid > li:first-child a"

            const cacheBackdrop = await getItemData(personId, "bu")
            const header = await waitForElement("#header")
            personPageMenuInjector(personId, filmElementSelector)
            injectContextMenuToAllFilmPosterItems({ itemId: personId, name: "person" })

            if (cacheBackdrop) {
                injectBackdrop(header, cacheBackdrop, getConfigData("PERSON_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                await scrapeFilmLinkElement(filmElementSelector, false, personId)
                return
            }

            if (await isDefaultBackdropAvailable()) {
                await scrapeFilmLinkElement(filmElementSelector, false, personId)
                return
            }

            const [scrapedImage, isCached] = await scrapeFilmLinkElement(filmElementSelector, getConfigData("PERSON_AUTO_SCRAPE"), personId)

            if (scrapedImage) {
                injectBackdrop(header, scrapedImage, getConfigData("PERSON_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])

                if (!isCached) {
                    await setItemData(personId, "bu", scrapedImage)
                }
            }
        } catch (error) {
            console.error("Error in personPageInjector:", error)
        }
    }

    async function reviewPageInjector() {
        try {
            const filmName = location.pathname.match(/\/film\/([^\/]+)/)?.[1]
            const filmId = `f/${filmName}`
            const filmElementSelector = `.film-poster a[href^="/film/"]`

            const cacheBackdrop = await getItemData(filmId, "bu")
            const header = await waitForElement("#header")
            filmPageMenuInjector({ filmId, mode: "backdrop" })
            filmPageMenuInjector({ filmId, mode: "poster" })
            injectContextMenuToAllFilmPosterItems()

            if (cacheBackdrop) {
                injectBackdrop(header, cacheBackdrop, getConfigData("REVIEW_SHORT_BACKDROP") ? ["shortbackdropped", "-crop"] : [])
                return
            }

            if (await isDefaultBackdropAvailable()) return

            const [scrapedImage, isCached] = await scrapeFilmLinkElement(filmElementSelector, getConfigData("REVIEW_AUTO_SCRAPE"), filmId)

            if (scrapedImage) {
                injectBackdrop(header, scrapedImage, ["shortbackdropped", "-crop"])

                if (!isCached) {
                    await setItemData(filmId, "bu", scrapedImage)
                }
            }
        } catch (error) {
            console.error("Error in reviewPageInjector:", error)
        }
    }

    async function injectPosters() {
        await waitForElement("body")

        const observer = new MutationObserver(async () => {
            if (!document.querySelector(".film-poster:not([poster-processed])")) return

            const allPosterImageElements = document.querySelectorAll(`.film-poster:not([poster-processed]) .image`)
            for (const posterImageElement of allPosterImageElements) {
                // Get the film name
                const posterElement = posterImageElement.parentElement?.parentElement
                const filmPath = posterImageElement.nextElementSibling?.href || posterElement?.getAttribute("data-film-link")
                const filmName = filmPath?.match(/\/film\/([^\/]+)/)?.[1] || ""
                if (!filmName) continue

                // Mark the element as processed to avoid reprocessing
                posterElement?.setAttribute("poster-processed", "")

                const filmId = `f/${filmName}`
                const cachePoster = await getItemData(filmId, "pu")

                if (cachePoster) injectPoster(posterImageElement, cachePoster)
            }
        })

        observer.observe(document.body, {
            childList: true,
            subtree: true,
        })
    }

    // MAIN

    try {
        const currentURL = location.protocol + "//" + location.hostname + location.pathname

        const filmPageRegex = /^(https?:\/\/letterboxd\.com\/film\/[^\/]+\/?(crew|details|releases|genres)?\/)$/
        const userPageRegex = /^(https?:\/\/letterboxd\.com\/[^\/]+(?:\/\?.*)?\/?)$/
        const listPageRegex =
            /^(https?:\/\/letterboxd\.com\/[A-Za-z0-9-_]+\/list\/[A-Za-z0-9-_]+(?:\/(by|language|country|decade|genre|on|detail|year)\/[A-Za-z0-9-_\/]+)?\/(?:(detail|page\/\d+)\/?)?)$/
        const personPageRegex =
            /^(https?:\/\/letterboxd\.com\/(director|actor|producer|executive-producer|writer|cinematography|additional-photography|editor|sound|story|visual-effects)\/[A-Za-z0-9-_]+(?:\/(by|language|country|decade|genre|on|year)\/[A-Za-z0-9-_\/]+)?\/(?:page\/\d+\/?)?)$/
        const reviewPageRegex = /^(https?:\/\/letterboxd\.com\/[A-Za-z0-9-_]+\/film\/[A-Za-z0-9-_]+\/(\d+\/)?(?:reviews\/?)?(?:page\/\d+\/?)?)$/

        injectPosters()

        if (filmPageRegex.test(currentURL)) {
            currentPage = "film"
            filmPageInjector()
        } else if (
            userPageRegex.test(currentURL) &&
            ![
                "/settings/",
                "/films/",
                "/lists/",
                "/members/",
                "/journal/",
                "/sign-in/",
                "/create-account/",
                "/pro/",
                "/search/",
                "/activity/",
                "/countries/",
            ].some((ending) => currentURL.toLowerCase().endsWith(ending))
        ) {
            currentPage = "user"
            userPageInjector()
        } else if (listPageRegex.test(currentURL)) {
            currentPage = "list"
            listPageInjector()
        } else if (personPageRegex.test(currentURL)) {
            currentPage = "person"
            personPageInjector()
        } else if (reviewPageRegex.test(currentURL)) {
            currentPage = "review"
            reviewPageInjector()
        } else {
            currentPage = "other"
            injectContextMenuToAllFilmPosterItems({ itemId: `u/${loggedInAs}`, name: "user" })
        }
    } catch (error) {
        console.error("Error in main function:", error)
    }
})()