豆瓣电影多平台一键直达 + 烂番茄+IMDb评分

右侧显示多平台搜索 + 烂番茄+IMDb评分(从豆瓣提取IMDb ID精准抓取),支持自主添加平台

// ==UserScript==
// @name           豆瓣电影多平台一键直达 + 烂番茄+IMDb评分
// @description    右侧显示多平台搜索 + 烂番茄+IMDb评分(从豆瓣提取IMDb ID精准抓取),支持自主添加平台
// @author         bai
// @version        2.6
// @icon           https://movie.douban.com/favicon.ico
// @grant          GM_addStyle
// @grant          GM_xmlhttpRequest
// @require        https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @include        https://movie.douban.com/subject/*
// @run-at         document-end
// @license        Apache-2.0
// @namespace      https://greasyfork.org/users/967749
// ==/UserScript==

$(function () {
    try {
        // 1. 注入页面样式
        GM_addStyle(`
            #douban_movie_multi_platform {
                margin: 10px 0;
                padding: 12px;
                background: #f8f9fa;
                border-radius: 6px;
                box-shadow: 0 1px 3px rgba(0,0,0,0.05);
                border: 1px solid #eee;
            }
            #douban_movie_multi_platform h3 {
                font-size: 16px;
                color: #333;
                border-bottom: 1px solid #e9ecef;
                padding-bottom: 6px;
                font-weight: bold;
                margin: 0 0 8px;
            }
            .movie_platforms {
                display: flex;
                flex-direction: column;
                gap: 8px;
            }
            .movie_platform {
                display: flex;
                align-items: center;
                gap: 8px;
                padding: 8px;
                border-radius: 4px;
                background: white;
                transition: background 0.2s;
                border: 1px solid #f0f0f0;
            }
            .movie_platform:hover {
                background: #f1f8fe;
            }
            .platform_icon {
                font-size: 18px;
                min-width: 22px;
                text-align: center;
            }
            .platform_name {
                font-size: 14px;
                color: #555;
                flex-shrink: 0;
                width: 60px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                font-weight: 500;
            }
            .platform_link {
                font-size: 14px;
                padding: 4px 8px;
                border: 1px solid #00a1d6;
                background: #fff;
                color: #00a1d6;
                border-radius: 4px;
                text-decoration: none;
                transition: all 0.2s;
            }
            .platform_link:hover {
                background: #00a1d6;
                color: white;
            }
            /* 烂番茄评分样式 */
            .rotten_rating {
                margin-left: auto;
                padding: 2px 6px;
                border-radius: 4px;
                font-size: 12px;
                font-weight: bold;
                cursor: pointer;
            }
            .rotten_fresh {
                background-color: #4CAF50;
                color: white;
            }
            .rotten_rotten {
                background-color: #F44336;
                color: white;
            }
            /* IMDb评分样式 */
            .imdb_rating {
                margin-left: auto;
                padding: 2px 6px;
                border-radius: 4px;
                font-size: 12px;
                font-weight: bold;
                background-color: #F5C518;
                color: #000;
                cursor: pointer;
                display: flex;
                align-items: center;
                gap: 4px;
            }
            .imdb_rating .rating_count {
                font-size: 10px;
                color: #333;
                font-weight: normal;
            }
            /* 通用加载和错误样式 */
            .rating_loading {
                color: #666;
                font-size: 12px;
                display: flex;
                align-items: center;
                gap: 4px;
            }
            .rating_loading::before {
                content: "";
                width: 8px;
                height: 8px;
                border: 2px solid #ccc;
                border-top-color: #666;
                border-radius: 50%;
                animation: spin 1s linear infinite;
            }
            .rating_error {
                color: #e53935;
                font-size: 12px;
            }
            @keyframes spin {
                to { transform: rotate(360deg); }
            }
        `);

        // 2. 提取电影核心信息
        function getMovieInfo() {
            // 提取标题和年份
            let title = $('h1').first().text().replace(/\s*\(\d{4}\)\s*$/, '').trim();
            const yearMatch = $('h1').first().text().match(/\((\d{4})\)/);
            const year = yearMatch? yearMatch[1] : '';
            if (!title) title = $('#wrapper > h1 > span').text().replace(/\s*\(\d{4}\)\s*$/, '').trim();

            // 提取导演/主演(用于搜索)
            let director = $('#info span:contains("导演")').nextAll('a').first().text().trim() || '';
            let actor = $('#info span:contains("主演")').nextAll('a').first().text().trim() || '';
            const keyword = [title, year, director || actor].filter(Boolean).join(' ');

            // 从豆瓣页面提取IMDb ID
            const imdbId = getImdbIdFromDouban();

            console.log('[电影信息] 提取结果:', { title, year, keyword, imdbId });
            return { title, year, keyword, imdbId };
        }

        // 3. 从豆瓣页面提取IMDb ID
        function getImdbIdFromDouban() {
            const imdbSpan = Array.from(document.querySelectorAll('#info span.pl'))
               .find(span => span.textContent.includes('IMDb:'));
            return imdbSpan? imdbSpan.nextSibling?.textContent?.trim() : null;
        }

        const movieInfo = getMovieInfo();
        if (!movieInfo.title) {
            console.error('无法提取电影标题,脚本终止');
            return;
        }

        // 4. 平台配置区
        const platforms = [
            {
                name: '烂番茄',
                icon: '🍅',
                url: `https://www.rottentomatoes.com/search?search=${encodeURIComponent(`${movieInfo.title} ${movieInfo.year}`)}`,
                hasRating: true,
                ratingType: 'rotten'
            },
            {
                name: 'IMDb',
                icon: '⭐',
                url: movieInfo.imdbId? `https://www.imdb.com/title/${movieInfo.imdbId}/` : `https://www.imdb.com/find?q=${encodeURIComponent(`${movieInfo.title} ${movieInfo.year}`)}`,
                hasRating: true,
                ratingType: 'imdb'
            },
            { name: 'B站', icon: '📺', url: `https://search.bilibili.com/all?keyword=${encodeURIComponent(movieInfo.keyword)}`, hasRating: false },
            { name: '抖音', icon: '♪', url: `https://www.douyin.com/search/${encodeURIComponent(movieInfo.keyword)}`, hasRating: false },
            { name: 'GAZE', icon: '📽️', url: `https://gaze.run/filter?search=${encodeURIComponent(movieInfo.keyword)}`, hasRating: false },
            { name: '努努', icon: '🎥', url: `https://nnyy.in/so?q=${encodeURIComponent(movieInfo.keyword)}`, hasRating: false },
            { name: 'Youtube', icon: '▶', url: `https://www.youtube.com/results?search_query=${encodeURIComponent(movieInfo.keyword)}`, hasRating: false }
        ];

        // 5. 生成模块HTML
        function createPlatformModule() {
            let html = `
                <div id="douban_movie_multi_platform">
                    <h3>电影资源直达</h3>
                    <div class="movie_platforms">
            `;
            platforms.forEach((plat, index) => {
                const ratingPlaceholder = plat.hasRating
                   ? `<span class="rating_loading">获取${plat.name}评分中...</span>`
                    : '';

                html += `
                    <div class="movie_platform" id="platform_${index}">
                        <div class="platform_icon">${plat.icon}</div>
                        <div class="platform_name">${plat.name}</div>
                        <a href="${plat.url}" target="_blank" class="platform_link">去搜索</a>
                        ${ratingPlaceholder}
                    </div>
                `;
            });
            html += `</div></div>`;
            return html;
        }

        // 6. 插入右侧侧边栏(增加容错处理)
        const moduleHtml = createPlatformModule();
        const aside = $('.aside');
        if (aside.length) {
            const watchArea = aside.find('h2:contains("在哪儿看这部电影")').closest('div');
            if (watchArea.length) {
                watchArea.before(moduleHtml);
            } else {
                aside.prepend(moduleHtml);
            }
        } else if ($('#content').length) {
            $('#content').prepend(moduleHtml);
        } else {
            // 终极容错:直接添加到body
            $('body').append(`<div style="position:fixed;top:20px;right:20px;z-index:9999;width:300px;">${moduleHtml}</div>`);
        }

        // 7. 字符串相似度算法
        function stringSimilarity(str1, str2) {
            const len1 = str1.length, len2 = str2.length;
            const matrix = Array.from({ length: len1 + 1 }, () => Array(len2 + 1).fill(0));

            for (let i = 0; i <= len1; i++) matrix[i][0] = i;
            for (let j = 0; j <= len2; j++) matrix[0][j] = j;

            for (let i = 1; i <= len1; i++) {
                for (let j = 1; j <= len2; j++) {
                    const cost = str1[i-1].toLowerCase() === str2[j-1].toLowerCase()? 0 : 1;
                    matrix[i][j] = Math.min(
                        matrix[i-1][j] + 1,
                        matrix[i][j-1] + 1,
                        matrix[i-1][j-1] + cost
                    );
                }
            }

            return 1 - matrix[len1][len2] / Math.max(len1, len2);
        }

        // 8. 抓取烂番茄评分
        function getRottenTomatoesRating(title, year) {
            const searchTerm = encodeURIComponent(`${title} ${year}`);
            const rtSearchUrl = `https://www.rottentomatoes.com/search?search=${searchTerm}`;

            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: rtSearchUrl,
                    timeout: 15000,
                    onload: (response) => {
                        if (response.status!== 200) {
                            resolve({ rating: null, error: '请求失败', url: null });
                            return;
                        }

                        try {
                            const tempDiv = document.createElement('div');
                            tempDiv.innerHTML = response.responseText;
                            const movieItems = tempDiv.querySelectorAll('search-page-media-row[data-qa="data-row"]');
                            let bestMatch = { rating: null, score: 0, url: null };

                            movieItems.forEach(item => {
                                const itemTitle = item.querySelector('h2')?.textContent || '';
                                const itemYear = item.getAttribute('releaseyear') || '';
                                const tomatometerScore = item.getAttribute('tomatometerscore') || '';
                                const itemUrl = item.querySelector('a[data-qa="info-name"]')?.getAttribute('href') || '';

                                let score = stringSimilarity(title, itemTitle);
                                if (year && itemYear === year) score += 0.3;

                                if (tomatometerScore && score > bestMatch.score) {
                                    bestMatch = {
                                        rating: `${tomatometerScore}%`,
                                        score: score,
                                        url: itemUrl? (itemUrl.startsWith('http')? itemUrl : `https://www.rottentomatoes.com${itemUrl}`) : null
                                    };
                                }
                            });

                            resolve(bestMatch.rating
                               ? { rating: bestMatch.rating, error: null, url: bestMatch.url }
                                : { rating: null, error: '未找到匹配电影', url: null });
                        } catch (e) {
                            console.error('烂番茄解析错误:', e);
                            resolve({ rating: null, error: '解析失败', url: null });
                        }
                    },
                    onerror: (error) => {
                        resolve({ rating: null, error: `请求错误: ${error.message}`, url: null });
                    },
                    ontimeout: () => {
                        resolve({ rating: null, error: '请求超时', url: null });
                    }
                });
            });
        }

        // 9. 抓取IMDb评分
        function getImdbRating() {
            if (movieInfo.imdbId) {
                const imdbUrl = `https://www.imdb.com/title/${movieInfo.imdbId}/`;
                return new Promise((resolve) => {
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: imdbUrl,
                        timeout: 15000,
                        onload: (response) => {
                            if (response.status!== 200) {
                                resolve({ rating: null, count: null, error: '请求失败', url: imdbUrl });
                                return;
                            }

                            try {
                                const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
                                const ratingContainer = doc.querySelector('[data-testid="hero-rating-bar__aggregate-rating__score"]');

                                if (!ratingContainer) {
                                    resolve({ rating: null, count: null, error: '未找到评分', url: imdbUrl });
                                    return;
                                }

                                const score = ratingContainer.querySelector('span:first-child')?.textContent?.trim() || null;
                                const count = ratingContainer.parentElement?.querySelector('div[class*="dwhNqC"]')?.textContent?.trim() || null;

                                resolve({
                                    rating: score? `${score}/10` : null,
                                    count: count || null,
                                    error: null,
                                    url: imdbUrl
                                });
                            } catch (e) {
                                console.error('IMDb解析错误:', e);
                                resolve({ rating: null, count: null, error: '解析失败', url: imdbUrl });
                            }
                        },
                        onerror: (error) => {
                            resolve({ rating: null, count: null, error: `请求错误: ${error.message}`, url: imdbUrl });
                        },
                        ontimeout: () => {
                            resolve({ rating: null, count: null, error: '请求超时', url: imdbUrl });
                        }
                    });
                });
            }

            // 降级逻辑
            const searchTerm = encodeURIComponent(`${movieInfo.title} ${movieInfo.year}`);
            const imdbSearchUrl = `https://www.imdb.com/find?q=${searchTerm}`;
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: imdbSearchUrl,
                    timeout: 15000,
                    onload: (response) => {
                        if (response.status!== 200) {
                            resolve({ rating: null, count: null, error: '请求失败', url: null });
                            return;
                        }

                        try {
                            const tempDiv = document.createElement('div');
                            tempDiv.innerHTML = response.responseText;
                            const movieItems = tempDiv.querySelectorAll('li.ipc-metadata-list-summary-item');
                            let bestMatch = { rating: null, count: null, score: 0, url: null };

                            movieItems.forEach(item => {
                                const itemTitle = item.querySelector('.ipc-metadata-list-summary-item__t')?.textContent || '';
                                const itemYear = item.querySelector('.ipc-metadata-list-summary-item__li')?.textContent?.match(/\d{4}/)?.[0] || '';
                                const ratingElem = item.querySelector('.ipc-rating-star--base')?.textContent || null;
                                const itemUrl = item.querySelector('a.ipc-metadata-list-summary-item__t')?.getAttribute('href') || '';

                                let score = stringSimilarity(movieInfo.title, itemTitle);
                                if (movieInfo.year && itemYear === movieInfo.year) score += 0.3;

                                if (ratingElem && score > bestMatch.score) {
                                    bestMatch = {
                                        rating: `${ratingElem.trim()}/10`,
                                        count: null,
                                        score: score,
                                        url: itemUrl? (itemUrl.startsWith('http')? itemUrl : `https://www.imdb.com${itemUrl}`) : null
                                    };
                                }
                            });

                            resolve(bestMatch.rating
                               ? { rating: bestMatch.rating, count: bestMatch.count, error: null, url: bestMatch.url }
                                : { rating: null, count: null, error: '未找到匹配电影', url: null });
                        } catch (e) {
                            console.error('IMDb搜索解析错误:', e);
                            resolve({ rating: null, count: null, error: '解析失败', url: null });
                        }
                    },
                    onerror: (error) => {
                        resolve({ rating: null, count: null, error: `请求错误: ${error.message}`, url: null });
                    },
                    ontimeout: () => {
                        resolve({ rating: null, count: null, error: '请求超时', url: null });
                    }
                });
            });
        }

        // 10. 处理评分显示(修复链接问题)
        function updateRatingDisplay(platformIndex, ratingInfo, ratingType) {
            const $platform = $(`#platform_${platformIndex}`);
            const $loading = $platform.find('.rating_loading');
            if (!$loading.length) return;

            // 确保URL格式正确
            let targetUrl = ratingInfo.url || '#';
            if (targetUrl &&!targetUrl.startsWith('http')) {
                targetUrl = 'https://www.' + targetUrl.replace(/^\/+/, '');
            }

            if (ratingInfo.rating && targetUrl) {
                if (ratingType === 'rotten') {
                    const isFresh = parseInt(ratingInfo.rating) >= 70;
                    $loading.replaceWith(`
                        <a href="${targetUrl}" target="_blank" class="rotten_rating ${isFresh? 'rotten_fresh' : 'rotten_rotten'}">
                            新鲜度 ${ratingInfo.rating}
                        </a>
                    `);
                } else if (ratingType === 'imdb') {
                    const countHtml = ratingInfo.count? `<span class="rating_count">(${ratingInfo.count})</span>` : '';
                    $loading.replaceWith(`
                        <a href="${targetUrl}" target="_blank" class="imdb_rating">
                            ${ratingInfo.rating} ${countHtml}
                        </a>
                    `);
                }
            } else {
                $loading.replaceWith(`
                    <span class="rating_error">${ratingInfo.error || '未找到评分'}</span>
                `);
            }
        }

        // 11. 获取并显示评分
        const ratingPlatforms = platforms.filter(p => p.hasRating);
        ratingPlatforms.forEach(plat => {
            const index = platforms.findIndex(p => p.name === plat.name);
            if (index === -1) return;

            if (plat.ratingType === 'rotten') {
                getRottenTomatoesRating(movieInfo.title, movieInfo.year)
                   .then(ratingInfo => updateRatingDisplay(index, ratingInfo, 'rotten'))
                   .catch(err => console.error('烂番茄评分获取失败:', err));
            } else if (plat.ratingType === 'imdb') {
                getImdbRating()
                   .then(ratingInfo => updateRatingDisplay(index, ratingInfo, 'imdb'))
                   .catch(err => console.error('IMDb评分获取失败:', err));
            }
        });
    } catch (e) {
        console.error('脚本执行错误:', e);
        // 显示错误提示
        $('body').append(`
            <div style="position:fixed;top:20px;left:20px;z-index:9999;background:red;color:white;padding:10px;">
                电影多平台脚本出错: ${e.message}
            </div>
        `);
    }
});