Greasy Fork

Greasy Fork is available in English.

Trakt.tv Universal Search (Anime and Non-Anime) - Improved v2.2

Enhanced version with better season/part matching, season subtitle support, and improved episode finding

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Trakt.tv Universal Search (Anime and Non-Anime) - Improved v2.2
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  Enhanced version with better season/part matching, season subtitle support, and improved episode finding
// @author       konvar
// @match        https://trakt.tv/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      hianime.to
// @connect      1flix.to
// ==/UserScript==

(function() {
    'use strict';

    // Configuration Object
    const CONFIG = {
        DEBUG: true,
        DEBOUNCE_DELAY: 300,
        TOP_RESULTS: 10,
        SIMILARITY_THRESHOLD: 0.1,
        EPISODE_TITLE_SIMILARITY_THRESHOLD: 0.8,
        MAX_SEARCH_PAGES: 1,
        CACHE_EXPIRATION_MS: 3600000, // 1 hour
        HIANIME_BASE_URL: 'https://hianime.to',
        FLIX_BASE_URL: 'https://1flix.to'
    };

    // Logging function
    function log(message) {
        if (CONFIG.DEBUG) {
            console.log(`[Trakt.tv Universal Search] ${message}`);
        }
    }

    // Debounce helper
    function debounce(fn, delay) {
        let timer = null;
        return function(...args) {
            clearTimeout(timer);
            timer = setTimeout(() => fn.apply(this, args), delay);
        };
    }

    // Helper: parse HTML string into a document
    function parseHTML(htmlString) {
        return new DOMParser().parseFromString(htmlString, 'text/html');
    }

    // Optimized Levenshtein Distance implementation using a two-row approach
    function optimizedLevenshtein(a, b) {
        if (a === b) return 0;
        const alen = a.length;
        const blen = b.length;
        if (alen === 0) return blen;
        if (blen === 0) return alen;

        let prevRow = new Array(blen + 1);
        let currRow = new Array(blen + 1);

        for (let j = 0; j <= blen; j++) {
            prevRow[j] = j;
        }

        for (let i = 1; i <= alen; i++) {
            currRow[0] = i;
            for (let j = 1; j <= blen; j++) {
                const cost = a[i - 1] === b[j - 1] ? 0 : 1;
                currRow[j] = Math.min(
                    currRow[j - 1] + 1, // Insertion
                    prevRow[j] + 1, // Deletion
                    prevRow[j - 1] + cost // Substitution
                );
            }
            [prevRow, currRow] = [currRow, prevRow];
        }

        return prevRow[blen];
    }

    // Calculate similarity based on optimized Levenshtein distance
    function calculateSimilarity(a, b) {
        a = a.toLowerCase();
        b = b.toLowerCase();
        const distance = optimizedLevenshtein(a, b);
        return 1 - distance / Math.max(a.length, b.length);
    }

    // Cache for similarity computations to avoid duplicate calculations
    const similarityCache = new Map();
    function getCachedSimilarity(a, b) {
        const key = `${a}:${b}`;
        if (similarityCache.has(key)) {
            return similarityCache.get(key);
        }
        const similarity = calculateSimilarity(a, b);
        similarityCache.set(key, similarity);
        return similarity;
    }

    // Cache with expiration mechanism for HTTP requests
    const requestCache = new Map();
    function cachedRequest(url) {
        const now = Date.now();
        if (requestCache.has(url)) {
            const cached = requestCache.get(url);
            if (now - cached.timestamp < CONFIG.CACHE_EXPIRATION_MS) {
                log(`Cache hit for ${url}`);
                return cached.promise;
            } else {
                log(`Cache expired for ${url}`);
                requestCache.delete(url);
            }
        }
        const promise = new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
                },
                onload: function(response) {
                    if (response.status === 200) {
                        resolve(response);
                    } else {
                        reject(new Error(`Failed to fetch content: ${response.status}`));
                    }
                },
                onerror: function(error) {
                    reject(error);
                }
            });
        });
        requestCache.set(url, { promise: promise, timestamp: now });
        return promise;
    }

    // Helper: extract movieId from URL using regex
    function extractMovieId(url) {
        // Expect URL format like "https://hianime.to/bleach-806?ref=search"
        let match = url.match(/-(\d+)(?:\?|$)/);
        if (match) {
            return match[1];
        }
        return null;
    }

    // Add custom CSS for the search button
    GM_addStyle(`
        .trakt-universal-search-button {
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 10px;
            background: none;
            border: none;
            padding: 0;
            cursor: pointer;
        }
        .trakt-universal-search-button:hover {
            box-shadow: none;
        }
        .trakt-universal-search-button img {
            max-height: 30px;
            width: auto;
        }
    `);

    // Class to store content details extracted from the DOM
    class ContentInfo {
        constructor(title, year, isAnime, season, episode, episodeTitle, alternativeTitles, contentType, absoluteEpisode, seasonSubtitle) {
            this.title = title;
            this.year = year;
            this.isAnime = isAnime;
            this.season = season;
            this.episode = episode;
            this.episodeTitle = episodeTitle;
            this.alternativeTitles = alternativeTitles;
            this.contentType = contentType;
            this.absoluteEpisode = absoluteEpisode;
            this.seasonSubtitle = seasonSubtitle;
        }

        // Extract content info from the current Trakt.tv page
        static fromDOM() {
            log('Extracting content info from DOM...');
            let titleElement = null, yearElement = null, seasonSubtitle = null;
            
            // Check for mobile title with season subtitle
            const mobileTitleElement = document.querySelector('.mobile-title h2 a:not(#level-up-link)');
            const seasonSubtitleElement = document.querySelector('.mobile-title h2 a#level-up-link');
            
            if (mobileTitleElement) {
                titleElement = mobileTitleElement;
                yearElement = document.querySelector('.mobile-title h1 .year');
                if (seasonSubtitleElement) {
                    seasonSubtitle = seasonSubtitleElement.textContent.trim();
                }
            } else if (window.location.pathname.startsWith('/movies/')) {
                const movieTitleElement = document.querySelector('h1');
                if (movieTitleElement) {
                    titleElement = movieTitleElement.childNodes[0];
                    yearElement = movieTitleElement.querySelector('.year');
                }
            } else {
                titleElement = document.querySelector('h2 a[data-safe="true"]');
            }

            const episodeElement = document.querySelector('h1.episode .main-title-sxe');
            const episodeTitleElement = document.querySelector('h1.episode .main-title');
            const episodeAbsElement = document.querySelector('h1.episode .main-title-abs');

            const genreElements = document.querySelectorAll('span[itemprop="genre"]');
            const additionalStats = document.querySelector('ul.additional-stats');
            const alternativeTitleElement = document.querySelector('.additional-stats .meta-data[data-type="alternative_titles"]');

            if (titleElement) {
                const title = titleElement.textContent.trim().replace(/[:.,!?]+$/, '');
                const episodeInfo = episodeElement ? episodeElement.textContent.trim().split('x') : null;
                const season = episodeInfo ? parseInt(episodeInfo[0]) : null;
                const episode = episodeInfo ? parseInt(episodeInfo[1]) : null;
                const episodeTitle = episodeTitleElement ? episodeTitleElement.textContent.trim() : null;
                const absoluteEpisode = episodeAbsElement ? parseInt(episodeAbsElement.textContent.trim().replace(/[\(\)]/g, '')) : null;

                const genres = Array.from(genreElements).map(el => el.textContent.trim().toLowerCase());
                const isAnime = genres.includes('anime') ||
                                (additionalStats && additionalStats.textContent.toLowerCase().includes('anime')) ||
                                document.querySelector('.poster img[src*="anime"]') !== null;

                let year = null;
                if (yearElement && yearElement.textContent.trim() !== "") {
                    year = yearElement.textContent.trim();
                } else {
                    const metaFirstAired = document.querySelector('#meta-first-aired');
                    if (metaFirstAired) {
                        const date = new Date(metaFirstAired.value);
                        if (!isNaN(date)) {
                            year = date.getFullYear().toString();
                        }
                    } else if (additionalStats) {
                        const yearMatch = additionalStats.textContent.match(/(\d{4})/);
                        year = yearMatch ? yearMatch[1] : null;
                    }
                }

                const alternativeTitles = alternativeTitleElement
                    ? alternativeTitleElement.textContent.split(',').map(t => t.trim())
                    : [];

                const contentType = window.location.pathname.startsWith('/movies/') ? 'movie' : 'tv';

                log(`Title: ${title}`);
                log(`Year: ${year}`);
                log(`Is Anime: ${isAnime}`);
                log(`Season: ${season}`);
                log(`Season Subtitle: ${seasonSubtitle}`);
                log(`Episode: ${episode}`);
                log(`Episode Title: ${episodeTitle}`);
                log(`Alternative Titles: ${alternativeTitles}`);
                log(`Content Type: ${contentType}`);
                log(`Absolute Episode: ${absoluteEpisode}`);

                return new ContentInfo(title, year, isAnime, season, episode, episodeTitle, alternativeTitles, contentType, absoluteEpisode, seasonSubtitle);
            }
            log('Failed to extract content info.');
            return null;
        }
    }

    // Class to create and manage the search button
    class SearchButton {
        constructor(contentInfo) {
            this.contentInfo = contentInfo;
            this.button = this.createButton();
        }

        createButton() {
            log('Creating search button...');
            const button = document.createElement('button');
            button.className = 'btn btn-block btn-summary trakt-universal-search-button';
            button.style.display = 'none';
            const icon = document.createElement('img');
            icon.style.width = 'auto';
            icon.style.height = '50px';

            if (this.contentInfo.isAnime) {
                icon.src = `${CONFIG.HIANIME_BASE_URL}/images/logo.png`;
                icon.alt = 'Hianime Logo';
            } else {
                icon.src = 'https://img.1flix.to/xxrz/400x400/100/e4/ca/e4ca1fc10cda9cf762f7b51876dc917b/e4ca1fc10cda9cf762f7b51876dc917b.png';
                icon.alt = '1flix Logo';
            }
            button.appendChild(icon);
            return button;
        }

        addToDOM() {
            log('Adding search button to DOM...');
            const container = document.querySelector('.col-lg-4.col-md-5.action-buttons');
            if (container && !document.querySelector('.trakt-universal-search-button')) {
                container.insertBefore(this.button, container.firstChild);
                log('Search button added to DOM.');
                return true;
            }
            log('Failed to add search button to DOM.');
            return false;
        }

        updateWithContentLink(url) {
            log('Updating search button with content link...');
            this.button.addEventListener('click', () => window.open(url, '_blank'));
            this.button.style.display = 'flex';
            log('Search button updated and displayed.');
        }

        updateButtonText(text) {
            log('Updating search button text...');
            const textNode = document.createTextNode(` ${text}`);
            this.button.appendChild(textNode);
            log('Search button text updated.');
        }
    }

    // Class to search external sites for content and find the correct URL
    class ContentSearcher {
        constructor(contentInfo) {
            this.contentInfo = contentInfo;
        }

        generateSearchUrl() {
            log('Generating search URL...');
            if (this.contentInfo.isAnime) {
                // Include season subtitle in search if available
                let searchQuery = this.contentInfo.title;
                if (this.contentInfo.seasonSubtitle) {
                    searchQuery = `${this.contentInfo.title} ${this.contentInfo.seasonSubtitle}`;
                }
                
                return this.contentInfo.contentType === 'movie' ?
                    `${CONFIG.HIANIME_BASE_URL}/search?keyword=${encodeURIComponent(searchQuery)}&type=1` :
                    `${CONFIG.HIANIME_BASE_URL}/search?keyword=${encodeURIComponent(searchQuery)}&type=2`;
            } else {
                const searchTerm = this.contentInfo.contentType === 'movie' ?
                    `${this.contentInfo.title} ${this.contentInfo.year}` :
                    this.contentInfo.title;
                return `${CONFIG.FLIX_BASE_URL}/search/${searchTerm.replace(/\s+/g, '-')}`;
            }
        }

        async search() {
            log('Searching for content concurrently...');
            const searchUrl = this.generateSearchUrl();
            const pageRequests = [];
            for (let page = 1; page <= CONFIG.MAX_SEARCH_PAGES; page++) {
                const separator = this.contentInfo.isAnime ? '&' : '?';
                const pageUrl = `${searchUrl}${separator}page=${page}`;
                log(`Queuing search URL: ${pageUrl}`);
                pageRequests.push(cachedRequest(pageUrl));
            }
            let allMatches = [];
            try {
                const responses = await Promise.all(pageRequests);
                responses.forEach((response, index) => {
                    const doc = parseHTML(response.responseText);
                    const pageMatches = this.findTopMatches(doc);
                    log(`Matches on page ${index + 1}: ${JSON.stringify(pageMatches)}`);
                    allMatches = allMatches.concat(pageMatches);
                });
            } catch (error) {
                log(`Error during concurrent page requests: ${error}`);
            }
            log(`All matches: ${JSON.stringify(allMatches)}`);

            // Loop through top candidates and return the first valid content URL
            for (const match of allMatches.slice(0, CONFIG.TOP_RESULTS)) {
                try {
                    const contentUrl = await this.findContentUrl(match.url);
                    if (contentUrl) {
                        log(`Content found: ${contentUrl}`);
                        return contentUrl;
                    } else {
                        log(`No content URL found for candidate: ${match.url}`);
                    }
                } catch (error) {
                    log(`Error processing candidate ${match.url}: ${error}`);
                    continue;
                }
            }
            log(`Content not found in the top ${CONFIG.TOP_RESULTS} results`);
            this.showMessage(`Content not found. Click the button to search manually.`);
            return searchUrl;
        }

        findTopMatches(doc) {
            log('Finding top matches on search results page...');
            const contentItems = doc.querySelectorAll('.flw-item');
            log(`Found ${contentItems.length} items in search results`);
            
            // Build list of titles to check, including season subtitle combinations
            let searchTitles = [this.contentInfo.title, ...this.contentInfo.alternativeTitles];
            
            // If we have a season subtitle, add combinations
            if (this.contentInfo.seasonSubtitle) {
                searchTitles.push(`${this.contentInfo.title}: ${this.contentInfo.seasonSubtitle}`);
                searchTitles.push(`${this.contentInfo.title} ${this.contentInfo.seasonSubtitle}`);
                
                // Handle "Part 2" scenarios
                if (this.contentInfo.season && this.contentInfo.season > 1) {
                    searchTitles.push(`${this.contentInfo.title}: ${this.contentInfo.seasonSubtitle} Part 2`);
                    searchTitles.push(`${this.contentInfo.title} ${this.contentInfo.seasonSubtitle} Part 2`);
                }
            }
            
            const matches = Array.from(contentItems)
                .map(item => {
                    const titleElement = item.querySelector('.film-name a');
                    const posterElement = item.querySelector('.film-poster-img');
                    const infoElement = item.querySelector('.fd-infor');
                    if (titleElement && infoElement) {
                        const itemTitle = titleElement.textContent.trim();
                        const normalizedItemTitle = this.normalizeTitle(itemTitle);
                        
                        // Calculate best score across all search titles
                        const bestScore = Math.max(...searchTitles.map(title =>
                            getCachedSimilarity(this.normalizeTitle(title), normalizedItemTitle)
                        ));
                        
                        // Bonus for exact season/part matches
                        let seasonBonus = 0;
                        if (this.contentInfo.season) {
                            const seasonRegex = new RegExp(`(season|part)\\s*${this.contentInfo.season}`, 'i');
                            if (seasonRegex.test(itemTitle)) {
                                seasonBonus = 0.2;
                            }
                            
                            // Check for "Part 2" when we're in season 4
                            if (this.contentInfo.season === 4 && /part\s*2/i.test(itemTitle)) {
                                seasonBonus = 0.3;
                            }
                        }
                        
                        const href = titleElement.getAttribute('href');
                        const url = `${this.contentInfo.isAnime ? CONFIG.HIANIME_BASE_URL : CONFIG.FLIX_BASE_URL}${href}`;
                        let itemType, year, duration;
                        const itemTypeElement = infoElement.querySelector('.fdi-item');
                        const itemTypeText = itemTypeElement ? itemTypeElement.textContent.trim().toLowerCase() : '';
                        const seasonMatch = itemTypeText.match(/^ss (\d+)$/);
                        if (seasonMatch) {
                            itemType = 'tv';
                        } else {
                            const yearRegex = /^\d{4}$/;
                            if (yearRegex.test(itemTypeText)) {
                                year = itemTypeText;
                                itemType = 'movie';
                            } else {
                                itemType = itemTypeText;
                                year = null;
                            }
                        }
                        const durationElement = infoElement.querySelector('.fdi-duration');
                        duration = durationElement ? durationElement.textContent.trim() : null;
                        const posterUrl = posterElement ? posterElement.getAttribute('data-src') : null;
                        
                        const finalScore = Math.min(bestScore + seasonBonus, 1.0);
                        
                        log(`Item: "${itemTitle}", Score: ${bestScore}, Season Bonus: ${seasonBonus}, Final Score: ${finalScore}, Type: ${itemType}, Year: ${year}, Duration: ${duration}`);
                        
                        const isCorrectType = (
                            (this.contentInfo.contentType === 'movie' && itemType === 'movie') ||
                            (this.contentInfo.contentType === 'tv' && itemType === 'tv')
                        );
                        return {
                            title: itemTitle,
                            score: finalScore,
                            url: url,
                            type: itemType,
                            year: year,
                            duration: duration,
                            posterUrl: posterUrl,
                            isCorrectType: isCorrectType
                        };
                    }
                    return null;
                })
                .filter(match => match !== null && match.score >= CONFIG.SIMILARITY_THRESHOLD && match.isCorrectType)
                .sort((a, b) => b.score - a.score);
            log(`Filtered matches: ${JSON.stringify(matches)}`);
            return matches;
        }

        async findContentUrl(contentUrl) {
            log(`Fetching content from URL: ${contentUrl}`);
            try {
                const response = await cachedRequest(contentUrl);
                const doc = parseHTML(response.responseText);
                if (this.contentInfo.isAnime) {
                    if (this.contentInfo.contentType === 'movie') {
                        return await this.findAnimeMovieContentUrl(doc);
                    } else {
                        return await this.findAnimeSeriesContentUrl(doc, contentUrl);
                    }
                } else {
                    return await this.findNonAnimeContentUrl(doc, contentUrl);
                }
            } catch (error) {
                log(`Error fetching content: ${error}`);
                throw error;
            }
        }

        // Handle Anime Movie
        async findAnimeMovieContentUrl(doc) {
            log('Processing Anime Movie content...');
            const syncDataScript = doc.querySelector('#syncData');
            if (syncDataScript) {
                try {
                    const syncData = JSON.parse(syncDataScript.textContent);
                    if (syncData && syncData.series_url) {
                        const seriesUrl = syncData.series_url;
                        const movieId = seriesUrl.split('-').pop();
                        const watchUrl = `${CONFIG.HIANIME_BASE_URL}/watch/${seriesUrl.slice(seriesUrl.lastIndexOf('/') + 1)}?ep=${movieId}`;
                        log(`Match found: ${watchUrl}`);
                        return watchUrl;
                    } else {
                        log('Series URL not found in syncData');
                    }
                } catch (e) {
                    log("Error parsing syncData JSON: " + e);
                }
            } else {
                log('syncData script not found on the movie page');
            }
            return null;
        }

        // Handle Anime Series (TV) - Enhanced episode finding
        async findAnimeSeriesContentUrl(doc, contentUrl) {
            log('Processing Anime Series content...');
            const movieId = extractMovieId(contentUrl);
            if (!movieId) {
                log("Could not extract movieId from contentUrl");
                return null;
            }
            const apiUrl = `${CONFIG.HIANIME_BASE_URL}/ajax/v2/episode/list/${movieId}`;
            log(`Fetching episode data from API: ${apiUrl}`);
            try {
                const episodeDataResponse = await cachedRequest(apiUrl);
                const episodeData = JSON.parse(episodeDataResponse.responseText);
                log('Episode data fetched:');
                log(`Total episodes: ${episodeData ? episodeData.totalItems : 'unknown'}`);
                if (episodeData && episodeData.status && episodeData.html) {
                    const episodeDoc = parseHTML(episodeData.html);
                    const episodeLinks = episodeDoc.querySelectorAll('.ssl-item.ep-item');
                    log(`Number of episode links found: ${episodeLinks.length}`);
                    
                    const normalizedSearchTitle = this.normalizeTitle(this.contentInfo.episodeTitle || '');
                    let bestMatch = null;
                    let bestMatchScore = 0;
                    let targetEpisodeNumber = null;
                    
                    // For series with multiple parts/seasons, calculate the actual episode number
                    if (contentUrl.includes('part-2') || contentUrl.includes('2nd-season')) {
                        // This might be a continuation, try to find the actual episode
                        targetEpisodeNumber = this.contentInfo.episode;
                    } else {
                        targetEpisodeNumber = this.contentInfo.episode;
                    }
                    
                    // First pass: look for exact episode number match
                    for (let link of episodeLinks) {
                        const episodeNumber = parseInt(link.getAttribute('data-number'));
                        const episodeTitle = link.querySelector('.ep-name')?.textContent.trim() || '';
                        log(`Episode ${episodeNumber}: "${episodeTitle}"`);
                        
                        if (episodeNumber === targetEpisodeNumber) {
                            // Found exact episode number match
                            const titleMatchScore = getCachedSimilarity(normalizedSearchTitle, this.normalizeTitle(episodeTitle));
                            log(`Episode ${episodeNumber} matches target episode number. Title match score: ${titleMatchScore}`);
                            
                            // If we have an episode title, check similarity
                            if (this.contentInfo.episodeTitle) {
                                if (titleMatchScore >= 0.3) {
                                    log(`Found matching episode by number and title`);
                                    return `${CONFIG.HIANIME_BASE_URL}${link.getAttribute('href')}`;
                                }
                            } else {
                                // No episode title to match, just use episode number
                                log(`Found matching episode by number only`);
                                return `${CONFIG.HIANIME_BASE_URL}${link.getAttribute('href')}`;
                            }
                        }
                        
                        // Track best title match regardless of episode number
                        const titleMatchScore = getCachedSimilarity(normalizedSearchTitle, this.normalizeTitle(episodeTitle));
                        if (titleMatchScore > bestMatchScore) {
                            bestMatch = link;
                            bestMatchScore = titleMatchScore;
                        }
                    }
                    
                    // Second pass: if we have a strong title match, use it
                    if (bestMatch && bestMatchScore >= CONFIG.EPISODE_TITLE_SIMILARITY_THRESHOLD) {
                        log(`Using best title match with score ${bestMatchScore}`);
                        return `${CONFIG.HIANIME_BASE_URL}${bestMatch.getAttribute('href')}`;
                    }
                    
                    // Third pass: For Part 2 series, try adjusted episode numbering
                    if (contentUrl.includes('part-2') && this.contentInfo.episode) {
                        // In Part 2, episode 14 might be episode 1
                        const adjustedEpisode = this.contentInfo.episode - 13; // Assuming Part 1 had 13 episodes
                        log(`Trying adjusted episode number for Part 2: ${adjustedEpisode}`);
                        
                        for (let link of episodeLinks) {
                            const episodeNumber = parseInt(link.getAttribute('data-number'));
                            if (episodeNumber === adjustedEpisode) {
                                log(`Found episode using adjusted numbering`);
                                return `${CONFIG.HIANIME_BASE_URL}${link.getAttribute('href')}`;
                            }
                        }
                    }
                    
                    log('No matching episode found with current criteria');
                } else {
                    log('Failed to fetch episode data from API');
                }
            } catch (error) {
                log(`Error processing anime series: ${error}`);
            }
            return null;
        }

        // Handle Non-Anime content
        async findNonAnimeContentUrl(doc, originalUrl) {
            log('Processing Non-Anime content...');
            const detailPageWatch = doc.querySelector('.detail_page-watch');
            if (!detailPageWatch) {
                log('Detail page watch element not found');
                return null;
            }
            const movieId = detailPageWatch.getAttribute('data-id');
            const movieType = detailPageWatch.getAttribute('data-type');
            if (!movieId || !movieType) {
                log('Movie ID or type not found');
                return null;
            }
            log(`Movie ID: ${movieId}, Movie Type: ${movieType}`);
            if (this.contentInfo.contentType === 'movie') {
                return await this.findNonAnimeMovieContentUrl(movieId, originalUrl);
            } else {
                return await this.findNonAnimeTvContentUrl(movieId, originalUrl);
            }
        }

        // Non-Anime Movie
        async findNonAnimeMovieContentUrl(movieId, contentUrl) {
            log('Processing Non-Anime Movie content...');
            const episodeListUrl = `${CONFIG.FLIX_BASE_URL}/ajax/episode/list/${movieId}`;
            log(`Fetching episode list from: ${episodeListUrl}`);
            try {
                const episodeListResponse = await cachedRequest(episodeListUrl);
                const episodeListContent = episodeListResponse.responseText;
                log('Episode list content fetched.');
                const episodeListDoc = parseHTML(episodeListContent);
                const serverItem = episodeListDoc.querySelector('.link-item');
                if (serverItem) {
                    const serverId = serverItem.getAttribute('data-linkid');
                    const watchUrl = contentUrl.replace(/\/movie\//, '/watch-movie/') + `.${serverId}`;
                    log(`Match found: ${watchUrl}`);
                    return watchUrl;
                } else {
                    log('No server found for this movie');
                }
            } catch (error) {
                log(`Error processing Non-Anime Movie: ${error}`);
            }
            return null;
        }

        // Non-Anime TV
        async findNonAnimeTvContentUrl(movieId, contentUrl) {
            log('Processing Non-Anime TV content...');
            const seasonListUrl = `${CONFIG.FLIX_BASE_URL}/ajax/season/list/${movieId}`;
            log(`Fetching season list from: ${seasonListUrl}`);
            try {
                const seasonListResponse = await cachedRequest(seasonListUrl);
                const seasonListContent = seasonListResponse.responseText;
                log('Season list content fetched.');
                const seasonListDoc = parseHTML(seasonListContent);
                const seasonItems = seasonListDoc.querySelectorAll('.ss-item');
                for (let seasonItem of seasonItems) {
                    const seasonNumberText = seasonItem.textContent.trim();
                    const seasonNumber = parseInt(seasonNumberText.split(' ')[1]);
                    const seasonId = seasonItem.getAttribute('data-id');
                    log(`Checking Season ${seasonNumber}`);
                    if (seasonNumber === this.contentInfo.season) {
                        const episodeListUrl = `${CONFIG.FLIX_BASE_URL}/ajax/season/episodes/${seasonId}`;
                        log(`Fetching episode list from: ${episodeListUrl}`);
                        const episodeListResponse = await cachedRequest(episodeListUrl);
                        const episodeListContent = episodeListResponse.responseText;
                        log('Episode list content fetched.');
                        const episodeListDoc = parseHTML(episodeListContent);
                        const episodeItems = episodeListDoc.querySelectorAll('.eps-item');
                        for (let episodeItem of episodeItems) {
                            const titleAttr = episodeItem.getAttribute('title');
                            if (!titleAttr) continue;
                            const parts = titleAttr.split(':');
                            if (parts.length < 2) continue;
                            const episodeNumber = parseInt(parts[0].replace('Eps', '').trim());
                            const episodeTitle = parts[1].trim();
                            log(`Checking Season ${seasonNumber}, Episode ${episodeNumber}: "${episodeTitle}"`);
                            if (episodeNumber === this.contentInfo.episode) {
                                const episodeId = episodeItem.getAttribute('data-id');
                                const serverListUrl = `${CONFIG.FLIX_BASE_URL}/ajax/episode/servers/${episodeId}`;
                                log(`Fetching server list from: ${serverListUrl}`);
                                const serverListResponse = await cachedRequest(serverListUrl);
                                const serverListContent = serverListResponse.responseText;
                                log('Server list content fetched.');
                                const serverListDoc = parseHTML(serverListContent);
                                const serverItem = serverListDoc.querySelector('.link-item');
                                if (serverItem) {
                                    const serverId = serverItem.getAttribute('data-id');
                                    const watchUrl = contentUrl.replace(/\/tv\//, '/watch-tv/') + `.${serverId}`;
                                    log(`Match found: ${watchUrl}`);
                                    return watchUrl;
                                } else {
                                    log('No server found for this episode');
                                }
                            }
                        }
                    }
                }
            } catch (error) {
                log(`Error processing Non-Anime TV: ${error}`);
            }
            log('No matching episode found');
            return null;
        }

        normalizeTitle(title) {
            return title.toLowerCase()
                .replace(/[:.,!?'`]+/g, '')
                .replace(/\s+/g, ' ')
                .replace(/[^\w\s]/g, '')
                .trim();
        }

        showMessage(message) {
            const messageDiv = document.createElement('div');
            messageDiv.textContent = message;
            messageDiv.style.cssText = "position: fixed; top: 10px; left: 50%; transform: translateX(-50%); background-color: #f8d7da; color: #721c24; padding: 10px; border-radius: 5px; z-index: 9999;";
            document.body.appendChild(messageDiv);
            setTimeout(() => messageDiv.remove(), 5000);
        }
    }

    // Class to manage initialization on Trakt.tv pages
    class TraktTvHandler {
        constructor() {
            this.isInitialized = false;
            this.observer = null;
        }

        async init() {
            log('Initializing script...');
            if (this.isInitialized) {
                log("Script already initialized, skipping...");
                return;
            }
            const contentInfo = ContentInfo.fromDOM();
            if (contentInfo) {
                const searchButton = new SearchButton(contentInfo);
                if (searchButton.addToDOM()) {
                    this.isInitialized = true;
                    log("Script initialization complete.");
                    const contentSearcher = new ContentSearcher(contentInfo);
                    const result = await contentSearcher.search();
                    if (result) {
                        searchButton.updateWithContentLink(result);
                        if (result === contentSearcher.generateSearchUrl()) {
                            searchButton.updateButtonText("Search Manually");
                        }
                    }
                    if (this.observer) {
                        this.observer.disconnect();
                        log('DOM observer disconnected.');
                    }
                } else {
                    log("Failed to add search button to DOM. Retrying in 1 second...");
                    setTimeout(() => this.init(), 1000);
                }
            } else {
                log("Content info not found. Retrying in 1 second...");
                setTimeout(() => this.init(), 1000);
            }
        }

        setupObserver() {
            log('Setting up DOM observer...');
            const debouncedInit = debounce(() => {
                log("DOM mutation detected, attempting to initialize...");
                this.init();
            }, CONFIG.DEBOUNCE_DELAY);
            this.observer = new MutationObserver(() => {
                if (!this.isInitialized) {
                    debouncedInit();
                }
            });
            this.observer.observe(document.body, { childList: true, subtree: true });
            log('DOM observer set up.');
        }
    }

    // Initialize only on Trakt.tv show or movie pages
    if (window.location.hostname === 'trakt.tv') {
        if (window.location.pathname.startsWith('/shows/') || window.location.pathname.startsWith('/movies/')) {
            log('Running on a Trakt.tv show or movie page.');
            setTimeout(() => {
                const traktHandler = new TraktTvHandler();
                traktHandler.init();
                traktHandler.setupObserver();
                log("Script setup complete on Trakt.tv");
            }, 1000);
        } else {
            log("Not on a show or movie page, script not initialized.");
        }
    }
})();