I’ve vibe coded a userscript version that works but it would be great to have it work within the Trakt apps.
If anyone can improve on my script, please do share ![]()
Updated 24/07/2025
The play buttons work everywhere except in some where it says “Up Next” and “Recently Aired” tv show episodes. So far I haven’t been able to get it to work there.
// ==UserScript==
// @name         Trakt.tv to Infuse Links (New API)
// @namespace    http://tampermonkey.net/
// @version      6.4
// @description  A robust script that reliably replaces 'Watch Now' buttons to open directly in Infuse on both desktop and iOS.
// @author       Your Name Here
// @match        https://trakt.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=trakt.tv
// @grant        GM.xmlHttpRequest
// @connect      trakt.tv
// @run-at       document-start
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';
    // --- Configuration ---
    const PROCESSED_FLAG = 'data-infuse-processed';
    // This selector targets any link that is designed to open the "Watch Now" modal.
    const WATCH_NOW_SELECTOR = 'a[data-target="#watch-now-modal"]';
    // --- State Management ---
    let mainPageTmdbId = null;
    let isFetchingMainId = false;
    let itemUrlCache = new Map(); // Caches TMDB IDs for grid/list items to avoid re-fetching
    console.log(`Trakt to Infuse: Initializing v6.4`);
    /**
     * Fetches the TMDB ID from a given Trakt URL, with caching.
     * @param {string} traktUrl - The relative URL of the Trakt page (e.g., /movies/superman-2025).
     * @returns {Promise<string|null>} A promise that resolves with the TMDB ID.
     */
    function fetchTmdbIdFromUrl(traktUrl) {
        const fullUrl = new URL(traktUrl, window.location.origin).href;
        if (itemUrlCache.has(fullUrl)) {
            return Promise.resolve(itemUrlCache.get(fullUrl));
        }
        return new Promise((resolve) => {
            console.log(`Trakt to Infuse: Fetching TMDB ID from ${fullUrl}`);
            GM.xmlHttpRequest({
                method: "GET",
                url: fullUrl,
                onload: function(response) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    const tmdbLink = doc.querySelector('li.tmdb a[href*="themoviedb.org/"], a#external-link-tmdb[href*="themoviedb.org/"]');
                    if (tmdbLink) {
                        const match = tmdbLink.href.match(/themoviedb\.org\/(movie|tv)\/(\d+)/);
                        if (match && match[2]) {
                            itemUrlCache.set(fullUrl, match[2]);
                            resolve(match[2]);
                        } else { resolve(null); }
                    } else { resolve(null); }
                },
                onerror: function(error) {
                    console.error(`Trakt to Infuse: Fetch for ${fullUrl} failed.`, error);
                    resolve(null);
                }
            });
        });
    }
    /**
     * Gets the TMDB ID for the current main detail page (movie or show). Caches the result for the current page view.
     * @returns {Promise<string|null>}
     */
    function getMainPageTmdbId() {
        return new Promise((resolve) => {
            if (mainPageTmdbId) {
                resolve(mainPageTmdbId);
                return;
            }
            const tmdbLink = document.querySelector('li.tmdb a[href*="themoviedb.org/"], a#external-link-tmdb[href*="themoviedb.org/"]');
            if (tmdbLink) {
                const match = tmdbLink.href.match(/themoviedb\.org\/(movie|tv)\/(\d+)/);
                if (match && match[2]) {
                    mainPageTmdbId = match[2];
                    resolve(mainPageTmdbId);
                    return;
                }
            }
            const path = window.location.pathname;
            const showMatch = path.match(/(\/shows\/[^\/]+)/);
            if (showMatch && showMatch[1] && !isFetchingMainId) {
                isFetchingMainId = true;
                const showUrl = showMatch[1];
                fetchTmdbIdFromUrl(showUrl).then(id => {
                    if (id) mainPageTmdbId = id;
                    isFetchingMainId = false;
                    resolve(id);
                });
            } else {
                resolve(null);
            }
        });
    }
    /**
     * Extracts season and episode numbers from a given URL path.
     * @param {string} path - The URL path to parse.
     * @returns {object|null}
     */
    function getEpisodeDetails(path) {
        const episodeMatch = path.match(/\/seasons\/(\d+)\/episodes\/(\d+)/);
        if (episodeMatch) return { season: episodeMatch[1], episode: episodeMatch[2] };
        if (path.includes('/seasons/')) {
            const seasonMatch = path.match(/\/seasons\/(\d+)/);
            if (seasonMatch) return { season: seasonMatch[1], episode: null };
        }
        return null;
    }
    /**
     * The core function. Finds a button, gets its data, generates the Infuse URL,
     * and replaces the button with a clean, functional clone.
     * @param {HTMLElement} button - The button element to process.
     */
    async function processButton(button) {
        if (button.getAttribute(PROCESSED_FLAG)) return;
        button.setAttribute(PROCESSED_FLAG, 'processing');
        const container = button.closest('[data-url]');
        const isListItem = container !== null;
        const itemUrl = isListItem ? container.dataset.url : window.location.pathname;
        let id;
        // **REVISED LOGIC**: Determine HOW to get the ID based on context.
        if (isListItem) {
            let urlToFetchIdFrom = itemUrl;
            // If the item URL is for a season or episode, we must find the main show's URL to get the TMDB ID.
            const showMatch = itemUrl.match(/(\/shows\/[^\/]+)/);
            if (showMatch && showMatch[1]) {
                // This handles URLs like /shows/rick-and-morty/seasons/7/episodes/1
                urlToFetchIdFrom = showMatch[1];
            } else if (itemUrl.startsWith('/seasons') || itemUrl.startsWith('/episodes')) {
                // This handles URLs like /seasons/12345 or /episodes/12345 from a show's main page
                const pageShowMatch = window.location.pathname.match(/(\/shows\/[^\/]+)/);
                if (pageShowMatch && pageShowMatch[1]) {
                    urlToFetchIdFrom = pageShowMatch[1];
                }
            }
            id = await fetchTmdbIdFromUrl(urlToFetchIdFrom);
        } else {
            // This is a main detail page button (e.g., /shows/rick-and-morty).
            id = await getMainPageTmdbId();
        }
        if (!id) {
            console.warn("Trakt to Infuse: Could not get TMDB ID for", itemUrl);
            button.removeAttribute(PROCESSED_FLAG);
            return;
        }
        let infuseUrl = '';
        if (itemUrl.includes('/movies/')) {
            infuseUrl = `infuse://movie/${id}`;
        } else if (itemUrl.includes('/shows/') || itemUrl.includes('/seasons/') || itemUrl.includes('/episodes/')) {
            const details = getEpisodeDetails(itemUrl);
            if (details && details.episode) {
                infuseUrl = `infuse://series/${id}-${details.season}-${details.episode}`;
            } else if (details && details.season) {
                infuseUrl = `infuse://series/${id}-${details.season}`;
            } else {
                infuseUrl = `infuse://series/${id}`;
            }
        }
        if (!infuseUrl) return;
        console.log(`Trakt to Infuse: Replacing button for ${itemUrl} with URL ${infuseUrl}`);
        const buttonClone = button.cloneNode(true);
        buttonClone.href = infuseUrl;
        buttonClone.removeAttribute('data-toggle');
        buttonClone.removeAttribute('data-target');
        buttonClone.removeAttribute('data-url');
        buttonClone.setAttribute(PROCESSED_FLAG, 'true');
        if (isListItem) {
            buttonClone.style.backgroundColor = 'rgba(255, 111, 0, 0.8)';
            buttonClone.style.color = 'white';
        } else {
            const mainInfo = buttonClone.querySelector('.main-info');
            if (mainInfo) mainInfo.textContent = 'Open in Infuse';
            const underInfo = buttonClone.querySelector('.under-info');
            if (underInfo) underInfo.textContent = 'Direct Link';
            buttonClone.style.outline = '2px solid #FF6F00';
            buttonClone.style.outlineOffset = '2px';
            buttonClone.style.backgroundColor = '#333';
        }
        if (button.parentNode) {
            button.parentNode.replaceChild(buttonClone, button);
        }
    }
    /**
     * Scans the page for any unprocessed buttons.
     */
    function processAllButtonsOnPage() {
        const buttons = document.querySelectorAll(`${WATCH_NOW_SELECTOR}:not([${PROCESSED_FLAG}])`);
        buttons.forEach(processButton);
    }
    function resetStateAndRun() {
        console.log("Trakt to Infuse: Page loaded or changed. Initializing script.");
        mainPageTmdbId = null;
        isFetchingMainId = false;
        itemUrlCache.clear();
        processAllButtonsOnPage();
    }
    // --- Main Execution ---
    const observer = new MutationObserver(processAllButtonsOnPage);
    observer.observe(document.body, { childList: true, subtree: true });
    document.addEventListener('turbo:load', resetStateAndRun);
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        resetStateAndRun();
    } else {
        document.addEventListener('DOMContentLoaded', resetStateAndRun, { once: true });
    }
    console.log("Trakt to Infuse: Script loaded (v6.3). Event listeners attached.");
})();