Greasy Fork

imdb豆瓣直达

1.豆瓣可直接跳转imdb。可区分剧集与电影并显示必要按钮,imdb页显示家长引导。2.按钮分为3个,分别是imdb首页、系列第一部的首页和跳转该季评分页。3.功能依赖豆瓣信息栏与下方首个同系列作品内的的tt号

// ==UserScript==
// @name         imdb豆瓣直达
// @namespace    http://tampermonkey.net/
// @version      2.3.1
// @description  1.豆瓣可直接跳转imdb。可区分剧集与电影并显示必要按钮,imdb页显示家长引导。2.按钮分为3个,分别是imdb首页、系列第一部的首页和跳转该季评分页。3.功能依赖豆瓣信息栏与下方首个同系列作品内的的tt号
// @author       PH365
// @match        https://movie.douban.com/subject/*
// @match        https://www.douban.com/movie/subject/*
// @match        https://www.douban.com/tv/subject/*
// @match        https://www.imdb.com/title/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=douban.com
// @license      MIT
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    const hostname = window.location.hostname;

    if (hostname.includes('douban.com')) {
        initDoubanFeature();
    } else if (hostname.includes('imdb.com')) {
        initIMDbFeature();
    }

    // ===================================================================
    // ================== 功能 1, 2, 3: 豆瓣直达IMDB ================
    // ===================================================================
    function initDoubanFeature() {
        const CURRENT_PAGE_BUTTON_ID = 'douban-to-imdb-button';
        const SERIES_BUTTON_ID = 'douban-to-series-imdb-button';
        const SEASON_RATINGS_BUTTON_ID = 'douban-to-season-ratings-button';
        const SERIES_MENU_WRAPPER_ID = 'douban-series-menu-wrapper';
        const SERIES_MENU_BUTTON_ID = 'douban-series-menu-button';
        const SERIES_MENU_DROPDOWN_ID = 'douban-series-dropdown-menu';

        function chineseNumeralToArabic(text) {
            const map = {'一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10,'十一':11,'十二':12,'十三':13,'十四':14,'十五':15,'十六':16,'十七':17,'十八':18,'十九':19,'二十':20};
            return map[text] || null;
        }

        function getCurrentSeasonNumber() {
            const title = document.title;
            let match = title.match(/Season\s+(\d+)/i);
            if (match) return parseInt(match[1], 10);
            match = title.match(/第\s*([一二三四五六七八九十]+)\s*季/);
            if (match && match[1]) return chineseNumeralToArabic(match[1]);
            return null;
        }

        function getSeriesIMDbInfoFromList() {
            return new Promise(resolve => {
                const seriesSectionTitle = Array.from(document.querySelectorAll('h2')).find(h => h.textContent.trim().includes('同系列作品'));
                const firstSeriesLink = seriesSectionTitle?.nextElementSibling?.querySelector('a');
                if (!firstSeriesLink?.href) return resolve(null);
                GM_xmlhttpRequest({
                    method: "GET", url: firstSeriesLink.href,
                    onload: response => resolve(response.responseText.match(/tt\d{7,}/)?.[0] || null),
                    onerror: () => resolve(null)
                });
            });
        }

        function createInitialButton() {
            if (document.getElementById(CURRENT_PAGE_BUTTON_ID)) return null;
            const infoDiv = document.getElementById('info');
            if (!infoDiv) return null;
            const match = (infoDiv.textContent || '').match(/tt\d{7,}/);
            if (!match) return null;

            const currentPageId = match[0];
            let referenceElement = Array.from(infoDiv.getElementsByTagName('span')).find(span => span.textContent?.includes('IMDb:'));
            if (!referenceElement) referenceElement = infoDiv.querySelector(`a[href*="${currentPageId}"]`);
            if (!referenceElement) return null;

            GM_addStyle(`
                /* 按钮样式 */
                #${CURRENT_PAGE_BUTTON_ID}, #${SERIES_BUTTON_ID}, #${SEASON_RATINGS_BUTTON_ID} {
                    display: inline-block; margin-left: 10px; padding: 1px 8px; font-size: 13px;
                    border-radius: 4px; text-decoration: none; vertical-align: baseline; transition: filter 0.2s;
                }
                #${CURRENT_PAGE_BUTTON_ID}:hover, #${SERIES_BUTTON_ID}:hover, #${SEASON_RATINGS_BUTTON_ID}:hover {
                    text-decoration: none; filter: brightness(0.9);
                }
                #${CURRENT_PAGE_BUTTON_ID} { background-color: #ff953c; color: #fff !important; }
                #${SERIES_BUTTON_ID} { background-color: #3B7ABE; color: #fff !important; }
                #${SEASON_RATINGS_BUTTON_ID} { background-color: #28a745; color: #fff !important; }
                #${SERIES_MENU_WRAPPER_ID} { position: relative; display: inline-block; }
                #${SERIES_MENU_BUTTON_ID} {
                    display: inline-block; padding: 1px 8px; font-size: 13px; color: #fff !important;
                    background-color: #6c757d; border-radius: 4px; text-decoration: none;
                    cursor: pointer; transition: filter 0.2s;
                }
                #${SERIES_MENU_BUTTON_ID}:hover { filter: brightness(0.9); }

                /* 系列菜单智能展开的CSS */
                #${SERIES_MENU_DROPDOWN_ID} {
                    display: none; position: absolute; left: 0;
                    background-color: white; min-width: 250px;
                    border: 1px solid #ccc; border-radius: 4px;
                    box-shadow: 0 6px 12px rgba(0,0,0,0.175); z-index: 1000;
                    max-height: 300px; overflow-y: auto; text-align: left;
                    /* 默认向下展开 */
                    top: 100%; margin-top: 5px;
                }
                /* 向上展开的样式 */
                #${SERIES_MENU_DROPDOWN_ID}.is-up {
                    top: auto;
                    bottom: 100%;
                    margin-top: 0;
                    margin-bottom: 5px;
                }
                #${SERIES_MENU_DROPDOWN_ID}.is-visible { display: block; }
                #${SERIES_MENU_DROPDOWN_ID} a {
                    color: #333; padding: 8px 15px; text-decoration: none;
                    display: block; font-size: 13px; white-space: nowrap;
                    transition: background-color 0.2s;
                }
                #${SERIES_MENU_DROPDOWN_ID} a:hover { background-color: #f5f5f5; text-decoration: none; }
            `);

            const button = document.createElement('a');
            button.id = CURRENT_PAGE_BUTTON_ID;
            button.textContent = '主页';
            button.href = `https://www.imdb.com/title/${currentPageId}`;
            button.target = '_blank';
            button.title = '跳转到当前页对应的 IMDb 页面';
            referenceElement.after(button);

            return { button, imdbId: currentPageId };
        }

        async function createAdditionalButtons(initialButton, currentPageId) {
            const infoDiv = document.getElementById('info');
            if (!infoDiv) return;

            const isTVShow = infoDiv.textContent.includes('集数');
            const hasSeriesSection = !!Array.from(document.querySelectorAll('h2')).find(h => h.textContent.trim().includes('同系列作品'));

            let lastButton = initialButton;

            if (hasSeriesSection) {
                const seriesId = await getSeriesIMDbInfoFromList();
                const seasonNumber = getCurrentSeasonNumber();
                if (seriesId && seriesId !== currentPageId && (!isTVShow || (isTVShow && seasonNumber > 1))) {
                    const seriesButton = document.createElement('a');
                    seriesButton.id = SERIES_BUTTON_ID;
                    seriesButton.textContent = '系列页';
                    seriesButton.href = `https://www.imdb.com/title/${seriesId}`;
                    seriesButton.target = '_blank';
                    seriesButton.title = '跳转到整个系列的 IMDb 页面';
                    lastButton.after(seriesButton);
                    lastButton = seriesButton;
                }
            }

            if (isTVShow) {
                const seasonNumber = getCurrentSeasonNumber() ?? 1;
                const seriesButton = document.getElementById(SERIES_BUTTON_ID);
                const idForRatings = seriesButton ? seriesButton.href.match(/tt\d{7,}/)[0] : currentPageId;
                const seasonRatingsButton = document.createElement('a');
                seasonRatingsButton.id = SEASON_RATINGS_BUTTON_ID;
                seasonRatingsButton.textContent = '本季评分';
                seasonRatingsButton.href = `https://www.imdb.com/title/${idForRatings}/episodes/?season=${seasonNumber}&ref_=ttep`;
                seasonRatingsButton.target = '_blank';
                seasonRatingsButton.title = `跳转到 IMDb S${seasonNumber} 评分页面`;
                lastButton.after(seasonRatingsButton);
            }

            if(hasSeriesSection) {
                const seriesMenuRow = createSeriesMenuRow();
                if (seriesMenuRow) {
                    let imdbLine = Array.from(infoDiv.children).find(el => el.textContent.includes('IMDb'));
                    if (imdbLine) {
                        // 寻找IMDb行末尾的 <br> 标签作为插入点
                        let imdbLineBreak = null;
                        let currentNode = imdbLine;
                         while(currentNode.nextSibling) {
                            if(currentNode.nodeName === 'BR') {
                                imdbLineBreak = currentNode;
                                break;
                            }
                            currentNode = currentNode.nextSibling;
                        }
                        if (imdbLineBreak) {
                            imdbLineBreak.after(seriesMenuRow);
                        } else {
                            infoDiv.appendChild(seriesMenuRow);
                        }
                    } else {
                         infoDiv.appendChild(seriesMenuRow);
                    }
                }
            }
        }

        /**
         * 创建“系列菜单”的完整行,并绑定定位逻辑
         */
        function createSeriesMenuRow() {
            const seriesSectionTitle = Array.from(document.querySelectorAll('h2')).find(h => h.textContent.trim().includes('同系列作品'));
            if (!seriesSectionTitle) return null;
            const links = seriesSectionTitle.nextElementSibling?.querySelectorAll('dl > dd > a');
            if (!links || links.length === 0) return null;

            const fragment = document.createDocumentFragment();

            const label = document.createElement('span');
            label.className = 'pl';
            label.textContent = '系列菜单';
            fragment.appendChild(label);
            fragment.appendChild(document.createTextNode(': '));

            const menuWrapper = document.createElement('span');
            menuWrapper.id = SERIES_MENU_WRAPPER_ID;

            const menuButton = document.createElement('a');
            menuButton.id = SERIES_MENU_BUTTON_ID;
            menuButton.textContent = '点击展开';
            menuButton.href = 'javascript:void(0);';

            const dropdown = document.createElement('div');
            dropdown.id = SERIES_MENU_DROPDOWN_ID;
            links.forEach(link => {
                const item = document.createElement('a');
                item.href = link.href;
                item.textContent = link.textContent.trim();
                item.target = '_blank';
                dropdown.appendChild(item);
            });

            menuWrapper.appendChild(menuButton);
            menuWrapper.appendChild(dropdown);
            fragment.appendChild(menuWrapper);
            fragment.appendChild(document.createElement('br'));

            // 定位点击交互逻辑
            menuButton.addEventListener('click', (event) => {
                event.stopPropagation();

                // 切换菜单的显示状态
                dropdown.classList.toggle('is-visible');

                // 如果菜单是隐藏的,则什么都不做
                if (!dropdown.classList.contains('is-visible')) {
                    return;
                }

                // 如果菜单是可见的,则开始计算位置
                dropdown.classList.remove('is-up'); // 先重置为默认向下

                const buttonRect = menuButton.getBoundingClientRect();
                const dropdownHeight = dropdown.offsetHeight;

                // 计算按钮下方和上方的可用空间
                const spaceBelow = window.innerHeight - buttonRect.bottom;
                const spaceAbove = buttonRect.top;

                // 如果下方空间不足,并且上方空间比下方空间更大,则向上展开
                if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
                    dropdown.classList.add('is-up');
                }
            });

            // 点击外部区域关闭菜单
            document.addEventListener('click', () => {
                if (dropdown.classList.contains('is-visible')) {
                    dropdown.classList.remove('is-visible');
                    dropdown.classList.remove('is-up'); // 关闭时重置方向
                }
            });

            return fragment;
        }

        function run() {
            const initialData = createInitialButton();
            if (initialData) {
                createAdditionalButtons(initialData.button, initialData.imdbId);
                return true;
            }
            return false;
        }

        if (run()) return;
        const observer = new MutationObserver((mutations, obs) => {
            if (run()) obs.disconnect();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ===================================================================
    // ================= 功能 4: IMDb页面添加家长指引按钮 =================
    // ===================================================================
    function initIMDbFeature() {
        const BUTTON_ID = 'imdb-parental-guide-button';

        function createIMDbButton() {
            if (document.getElementById(BUTTON_ID)) return true;
            const anchor = document.querySelector('[data-testid="hero-subnav-bar-right-block"]');
            if (!anchor) return false;

            const urlMatch = window.location.href.match(/(tt\d{7,})/);
            if (!urlMatch || !urlMatch[0]) return false;

            const ttNumber = urlMatch[0];
            const parentalGuideUrl = `https://www.imdb.com/title/${ttNumber}/parentalguide/?ref_=tt_stry_pg`;

            const button = document.createElement('a');
            button.id = BUTTON_ID;
            button.className = 'ipc-chip ipc-chip--on-base';
            button.textContent = 'Parental Guide';
            button.href = parentalGuideUrl;
            button.title = '跳转到家长指引页面';

            GM_addStyle(`
                #${BUTTON_ID} { margin-right: 8px !important; color: #FFFFFF !important; font-weight: bold !important; background-color: #4A4A4A !important; transition: background-color 0.2s; }
                #${BUTTON_ID}:hover { background-color: #606060 !important; color: #FFFFFF !important; }
            `);

            anchor.prepend(button);
            return true;
        }

        const observer = new MutationObserver((mutations, obs) => {
            if (createIMDbButton()) obs.disconnect();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }
})();