Greasy Fork

Greasy Fork is available in English.

Trakt.tv Universal Search (Anime and Non-Anime)

Search for anime on hianime.to and non-anime content on 1flix.to from Trakt.tv

当前为 2024-09-11 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Trakt.tv Universal Search (Anime and Non-Anime)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Search for anime on hianime.to and non-anime content on 1flix.to from Trakt.tv
// @author       konvar
// @match        https://trakt.tv/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      hianime.to
// @connect      1flix.to
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const HIANIME_BASE_URL = 'https://hianime.to';
    const FLIX_BASE_URL = 'https://1flix.to';
    const TOP_RESULTS = 10;
    const SIMILARITY_THRESHOLD = 0.4;
    const EPISODE_TITLE_SIMILARITY_THRESHOLD = 0.8;
    const MAX_SEARCH_PAGES = 1;

    GM_addStyle(`
        .trakt-universal-search-button {
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 10px;
            /* Remove default button styles */
            background: none;
            border: none;
            padding: 0;
            cursor: pointer; /* Maintain pointer cursor */
        }
        .trakt-universal-search-button:hover {
            /* Prevent white glow on hover */
            box-shadow: none;
        }
        .trakt-universal-search-button img {
            max-height: 30px;
            width: auto;
        }
    `);

    class ContentInfo {
        constructor(title, year, isAnime, season, episode, episodeTitle, alternativeTitles, contentType, absoluteEpisode) {
            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;
        }

        static fromDOM() {
            let titleElement, yearElement;
            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('.genres .btn');
            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;
                if (yearElement) {
                    year = yearElement.textContent.trim();
                } 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';

                return new ContentInfo(title, year, isAnime, season, episode, episodeTitle, alternativeTitles, contentType, absoluteEpisode);
            }
            return null;
        }
    }

    class SearchButton {
        constructor(contentInfo) {
            this.contentInfo = contentInfo;
            this.button = this.createButton();
        }

        createButton() {
            const button = document.createElement('button'); // Changed from <a> to <button>
            button.className = 'btn btn-block btn-summary trakt-universal-search-button'; // Updated class name
            button.style.display = 'none';

            const icon = document.createElement('img');
            icon.style.width = 'auto';
            icon.style.height = '50px';

            if (this.contentInfo.isAnime) {
                icon.src = `${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() {
            const container = document.querySelector('.col-lg-4.col-md-5.action-buttons');
            if (container && !document.querySelector('.trakt-universal-search-button')) { // Updated class name
                container.insertBefore(this.button, container.firstChild);
                return true;
            }
            return false;
        }

        updateWithContentLink(url) {
            this.button.addEventListener('click', () => {
                window.open(url, '_blank');
            });
            this.button.style.display = 'flex';
        }


        updateButtonText(text) {
            const textNode = document.createTextNode(` ${text}`);
            this.button.appendChild(textNode);
        }
    }

    class ContentSearcher {
        constructor(contentInfo) {
            this.contentInfo = contentInfo;
        }

        generateSearchUrl() {
            if (this.contentInfo.isAnime) {
                if (this.contentInfo.contentType === 'movie') {
                    return `${HIANIME_BASE_URL}/search?keyword=${encodeURIComponent(this.contentInfo.title)}&type=1`;
                } else {
                    return `${HIANIME_BASE_URL}/search?keyword=${encodeURIComponent(this.contentInfo.title)}&type=2`;
                }
            } else {
                const searchTerm = this.contentInfo.contentType === 'movie' ?
                    `${this.contentInfo.title} ${this.contentInfo.year}` :
                    this.contentInfo.title;
                return `${FLIX_BASE_URL}/search/${searchTerm.replace(/\s+/g, '-')}`;
            }
        }

        async search() {
            let page = 1;
            let allMatches = [];
            const searchUrl = this.generateSearchUrl();

            while (page <= MAX_SEARCH_PAGES) {
                const pageUrl = `${searchUrl}${this.contentInfo.isAnime ? '&' : '?'}page=${page}`;
                try {
                    const response = await this.makeRequest(pageUrl);
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, 'text/html');
                    const pageMatches = this.findTopMatches(doc);

                    allMatches = allMatches.concat(pageMatches);

                    if (pageMatches.length === 0) break;
                    page++;
                } catch (error) {
                    break;
                }
            }

            for (const match of allMatches.slice(0, TOP_RESULTS)) {
                const contentUrl = await this.findContentUrl(match.url);
                if (contentUrl) {
                    return contentUrl;
                }
            }

            this.showMessage(`Content not found. Click the button to search manually.`);
            return searchUrl;
        }

        findTopMatches(doc) {
            const contentItems = doc.querySelectorAll('.flw-item');

            const allTitles = [this.contentInfo.title, ...this.contentInfo.alternativeTitles];
            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);
                    const bestScore = Math.max(...allTitles.map(title =>
                        this.calculateMatchScore(this.normalizeTitle(title), normalizedItemTitle)
                    ));
                    const href = titleElement.getAttribute('href');
                    const url = `${this.contentInfo.isAnime ? HIANIME_BASE_URL : 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 isCorrectType = (
                        (this.contentInfo.contentType === 'movie' && itemType === 'movie') ||
                        (this.contentInfo.contentType === 'tv' && itemType === 'tv')
                    );

                    return {
                        title: itemTitle,
                        score: bestScore,
                        url: url,
                        type: itemType,
                        year: year,
                        duration: duration,
                        posterUrl: posterUrl,
                        isCorrectType: isCorrectType
                    };
                }
                return null;
            }).filter(match => match !== null && match.score >= SIMILARITY_THRESHOLD && match.isCorrectType)
              .sort((a, b) => b.score - a.score);

            return matches;
        }

        async findContentUrl(contentUrl) {
            try {
                const response = await this.makeRequest(contentUrl);
                const parser = new DOMParser();
                const doc = parser.parseFromString(response.responseText, 'text/html');

                if (this.contentInfo.isAnime && this.contentInfo.contentType === 'movie') {
                    const syncDataScript = doc.querySelector('#syncData');
                    if (syncDataScript) {
                        const syncData = JSON.parse(syncDataScript.textContent);
                        const seriesUrl = syncData.series_url;
                        if (seriesUrl) {
                            const movieId = seriesUrl.split('-').pop();
                            const watchUrl = `${HIANIME_BASE_URL}/watch/${seriesUrl.slice(seriesUrl.lastIndexOf('/') + 1)}?ep=${movieId}`;
                            return watchUrl;
                        }
                    }
                } else if (this.contentInfo.isAnime) {
                    const movieId = contentUrl.split('-').pop();
                    const apiUrl = `${HIANIME_BASE_URL}/ajax/v2/episode/list/${movieId}`;

                    const episodeDataResponse = await this.makeRequest(apiUrl);
                    const episodeData = JSON.parse(episodeDataResponse.responseText);


                    if (episodeData.status && episodeData.html) {
                        const episodeDoc = parser.parseFromString(episodeData.html, 'text/html');
                        const episodeLinks = episodeDoc.querySelectorAll('.ssl-item.ep-item');

                        const normalizedSearchTitle = this.normalizeTitle(this.contentInfo.episodeTitle);
                        let bestMatch = null;
                        let bestMatchScore = 0;

                        for (let i = 0; i < episodeLinks.length; i++) {
                            const link = episodeLinks[i];
                            const episodeNumber = parseInt(link.getAttribute('data-number'));
                            const episodeTitle = link.querySelector('.ep-name')?.textContent.trim() || '';

                            const normalizedEpisodeTitle = this.normalizeTitle(episodeTitle);
                            const titleMatchScore = this.calculateMatchScore(normalizedSearchTitle, normalizedEpisodeTitle);

                            if (episodeNumber === this.contentInfo.episode || episodeNumber === this.contentInfo.absoluteEpisode) {
                                if (titleMatchScore >= 0.3) {
                                    return `${HIANIME_BASE_URL}${link.getAttribute('href')}`;
                                }
                            }

                            if (titleMatchScore >= EPISODE_TITLE_SIMILARITY_THRESHOLD && (episodeNumber === this.contentInfo.episode || episodeNumber === this.contentInfo.absoluteEpisode)) {
                                return `${HIANIME_BASE_URL}${link.getAttribute('href')}`;
                            }

                            if (titleMatchScore > bestMatchScore) {
                                bestMatch = link;
                                bestMatchScore = titleMatchScore;
                            }
                        }

                        if (bestMatch && bestMatchScore >= EPISODE_TITLE_SIMILARITY_THRESHOLD) {
                            return `${HIANIME_BASE_URL}${bestMatch.getAttribute('href')}`;
                        }
                    }
                } else {
                    const detailPageWatch = doc.querySelector('.detail_page-watch');
                    if (!detailPageWatch) {
                        return null;
                    }

                    const movieId = detailPageWatch.getAttribute('data-id');
                    const movieType = detailPageWatch.getAttribute('data-type');

                    if (!movieId || !movieType) {
                        return null;
                    }

                    if (this.contentInfo.contentType === 'movie') {
                        const episodeListUrl = `${FLIX_BASE_URL}/ajax/episode/list/${movieId}`;
                        const episodeListResponse = await this.makeRequest(episodeListUrl);
                        const episodeListContent = episodeListResponse.responseText;

                        const episodeListDoc = parser.parseFromString(episodeListContent, 'text/html');
                        const serverItem = episodeListDoc.querySelector('.link-item');

                        if (serverItem) {
                            const serverId = serverItem.getAttribute('data-linkid');
                            const watchUrl = contentUrl.replace(/\/movie\//, '/watch-movie/') + `.${serverId}`;
                            return watchUrl;
                        }
                    } else {
                        const seasonListUrl = `${FLIX_BASE_URL}/ajax/season/list/${movieId}`;
                        const seasonListResponse = await this.makeRequest(seasonListUrl);
                        const seasonListContent = seasonListResponse.responseText;

                        const seasonListDoc = parser.parseFromString(seasonListContent, 'text/html');
                        const seasonItems = seasonListDoc.querySelectorAll('.ss-item');

                        for (let seasonItem of seasonItems) {
                            const seasonNumber = parseInt(seasonItem.textContent.trim().split(' ')[1]);
                            const seasonId = seasonItem.getAttribute('data-id');

                            if (seasonNumber === this.contentInfo.season) {
                                const episodeListUrl = `${FLIX_BASE_URL}/ajax/season/episodes/${seasonId}`;
                                const episodeListResponse = await this.makeRequest(episodeListUrl);
                                const episodeListContent = episodeListResponse.responseText;

                                const episodeListDoc = parser.parseFromString(episodeListContent, 'text/html');
                                const episodeItems = episodeListDoc.querySelectorAll('.eps-item');

                                for (let episodeItem of episodeItems) {
                                    const episodeNumber = parseInt(episodeItem.getAttribute('title').split(':')[0].replace('Eps', '').trim());
                                    const episodeTitle = episodeItem.getAttribute('title').split(':')[1].trim();

                                    if (episodeNumber === this.contentInfo.episode) {
                                        const episodeId = episodeItem.getAttribute('data-id');

                                        const serverListUrl = `${FLIX_BASE_URL}/ajax/episode/servers/${episodeId}`;
                                        const serverListResponse = await this.makeRequest(serverListUrl);
                                        const serverListContent = serverListResponse.responseText;

                                        const serverListDoc = parser.parseFromString(serverListContent, 'text/html');
                                        const serverItem = serverListDoc.querySelector('.link-item');

                                        if (serverItem) {
                                            const serverId = serverItem.getAttribute('data-id');
                                            const watchUrl = contentUrl.replace(/\/tv\//, '/watch-tv/') + `.${serverId}`;
                                            return watchUrl;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } catch (error) {
            }
            return null;
        }

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

        calculateMatchScore(searchTitle, itemTitle) {
            const words1 = searchTitle.split(' ');
            const words2 = itemTitle.split(' ');
            const commonWords = words1.filter(word => words2.includes(word));
            return commonWords.length / Math.max(words1.length, words2.length);
        }

        makeRequest(url) {
            return 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);
                    }
                });
            });
        }

        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 TraktTvHandler {
        constructor() {
            this.isInitialized = false;
        }

        async init() {
            if (this.isInitialized) {
                return;
            }

            const contentInfo = ContentInfo.fromDOM();
            if (contentInfo) {
                const searchButton = new SearchButton(contentInfo);
                if (searchButton.addToDOM()) {
                    this.isInitialized = true;

                    const contentSearcher = new ContentSearcher(contentInfo);
                    const result = await contentSearcher.search();
                    if (result) {
                        searchButton.updateWithContentLink(result);
                        if (result === contentSearcher.generateSearchUrl()) {
                            searchButton.updateButtonText("Search Manually");
                        }
                    }
                } else {
                    setTimeout(() => this.init(), 1000);
                }
            } else {
                setTimeout(() => this.init(), 1000);
            }
        }

        setupObserver() {
            const observer = new MutationObserver((mutations) => {
                if (!this.isInitialized) {
                    for (let mutation of mutations) {
                        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                            this.init();
                            break;
                        }
                    }
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        }
    }

    if (window.location.hostname === 'trakt.tv') {
        if (window.location.pathname.startsWith('/shows/') || window.location.pathname.startsWith('/movies/')) {
            setTimeout(() => {
                const traktHandler = new TraktTvHandler();
                traktHandler.init();
                traktHandler.setupObserver();
            }, 1000);
        }
    }
})();