Raw Source
sp1ti / Trakt.tv External Letterboxd Link

// ==UserScript==
// @name         Trakt.tv External Letterboxd Link
// @namespace    https://trakt.tv/
// @version      1.1
// @description  Adds a Letterboxd link to Trakt.tv movie pages external section.
// @author       You
// @match        https://trakt.tv/*
// @grant        none
// @license      MIT
// @updateURL    https://openuserjs.org/meta/sp1ti/Trakt.tv_External_Letterboxd_Link.meta.js
// @downloadURL  https://openuserjs.org/install/sp1ti/Myanimelist.net_links_on_trakt.tv.user.js
// @icon         https://huere.net/i/extensions/trakt-letterboxd-icon-128.png

// ==/UserScript==

(function () {
    "use strict";

    let lastUrl = location.href;

    // Function to check if we're on a movie page
    function isMoviePage() {
        return location.pathname.startsWith("/movies/");
    }

    // Function to add the Letterboxd link
    function addLetterboxdLink() {
        if (!isMoviePage()) return; // Only run on movie pages

        const tmdbLink = document.getElementById("external-link-tmdb");
        if (tmdbLink) {
            const tmdbIdMatch = tmdbLink.href.match(/\/(\d+)$/);
            if (tmdbIdMatch) {
                const tmdbId = tmdbIdMatch[1];
                const letterboxdUrl = `https://letterboxd.com/tmdb/${tmdbId}`;

                const externalList = document.querySelector("ul.external");
                if (externalList && !externalList.querySelector(`a[href="${letterboxdUrl}"]`)) {
                    const letterboxdLink = document.createElement("a");
                    letterboxdLink.href = letterboxdUrl;
                    letterboxdLink.target = "_blank";
                    letterboxdLink.textContent = "Letterboxd";
                    tmdbLink.classList.forEach((cls) => letterboxdLink.classList.add(cls));
                    letterboxdLink.style.marginLeft = "5px";

                    const lastLi = externalList.querySelector("li:last-of-type");
                    if (lastLi) lastLi.appendChild(letterboxdLink);
                }
            }
        }
    }

    // Observe the external links section for updates
    function observeExternalLinks() {
        const externalList = document.querySelector("ul.external");
        if (!externalList) return;

        const observer = new MutationObserver(() => addLetterboxdLink());
        observer.observe(externalList, { childList: true });
    }

    // Detect URL changes using history API interception
    function interceptHistoryMethods() {
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        function handleNavigationChange() {
            requestAnimationFrame(() => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    if (isMoviePage()) {
                        addLetterboxdLink();
                        observeExternalLinks();
                    }
                }
            });
        }

        history.pushState = function (...args) {
            originalPushState.apply(history, args);
            handleNavigationChange();
        };

        history.replaceState = function (...args) {
            originalReplaceState.apply(history, args);
            handleNavigationChange();
        };

        window.addEventListener("popstate", handleNavigationChange);
    }

    // Run script immediately and set up observers
    function initialize() {
        if (isMoviePage()) {
            addLetterboxdLink();
            observeExternalLinks();
        }
        interceptHistoryMethods();
    }

    initialize();
})();