Emby媒体信息获取助手

按需手动获取Emby媒体信息,支持所有媒体类型,代码精简高效

// ==UserScript==
// @name         Emby媒体信息获取助手
// @namespace    http://tampermonkey.net/
// @version      0.0.5
// @description  按需手动获取Emby媒体信息,支持所有媒体类型,代码精简高效
// @license      MIT
// @author       王大锤
// @match        *://*/web/index.html*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    // 配置与缓存
    const CONFIG = {
        cacheExpiry: 24 * 60 * 60 * 1000, // 24小时
        logEnabled: true
    };

    // 简化的日志工具
    const log = {
        info: function (...args) {
            if (CONFIG.logEnabled) console.log('%c[信息]', 'color: blue;', ...args);
        },
        error: function (...args) {
            if (CONFIG.logEnabled) console.log('%c[错误]', 'color: red;', ...args);
        }
    };

    // 缓存工具
    const cache = {
        get: function (key) {
            try {
                const item = localStorage.getItem(key);
                if (!item) return null;

                const parsedItem = JSON.parse(item);
                if (parsedItem.expiry && parsedItem.expiry < Date.now()) {
                    localStorage.removeItem(key);
                    return null;
                }

                return parsedItem.value;
            } catch (error) {
                return null;
            }
        },
        set: function (key, value, expiry = CONFIG.cacheExpiry) {
            try {
                const item = {
                    value: value,
                    expiry: Date.now() + expiry
                };
                localStorage.setItem(key, JSON.stringify(item));
                return true;
            } catch (error) {
                return false;
            }
        },
        remove: function (key) {
            localStorage.removeItem(key);
        }
    };

    // 工具函数
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function extractItemId() {
        try {
            const hash = window.location.hash;
            const idMatch = /id=([^&]+)/.exec(hash);

            if (idMatch && idMatch[1]) {
                return idMatch[1];
            }
            return null;
        } catch (error) {
            log.error('提取ItemID失败:', error);
            return null;
        }
    }

    // 获取Emby API客户端
    function getApiClient() {
        if (typeof ApiClient !== 'undefined') return ApiClient;
        if (typeof window.ApiClient !== 'undefined') return window.ApiClient;
        return null;
    }

    // 显示状态消息
    function showStatusMessage(message, type = 'info') {
        let statusDiv = document.getElementById('emby-status-info');

        if (!statusDiv) {
            statusDiv = document.createElement('div');
            statusDiv.id = 'emby-status-info';
            statusDiv.style.cssText = 'position: fixed; top: 10px; right: 10px; padding: 10px; background: rgba(0,0,0,0.7); color: white; z-index: 9999; border-radius: 5px; max-width: 300px;';
            document.body.appendChild(statusDiv);
        }

        const bgColor = type === 'error' ? 'rgba(180,0,0,0.8)' : 'rgba(0,0,0,0.7)';
        statusDiv.style.background = bgColor;
        statusDiv.textContent = message;

        // 自动隐藏消息(错误消息或包含"完成"字样的消息)
        if (type === 'error' || message.includes('完成')) {
            setTimeout(() => {
                if (statusDiv && statusDiv.parentNode) {
                    document.body.removeChild(statusDiv);
                }
            }, 5000);
        }
    }

    // 使用Emby内置API获取媒体信息
    async function getMediaInfo(itemId) {
        // 检查缓存
        const cacheKey = `media_info_${itemId}`;
        const cachedData = cache.get(cacheKey);

        if (cachedData) {
            log.info('使用缓存的媒体信息:', itemId);
            showMediaInfoResult(cachedData);
            return cachedData;
        }

        // 添加状态提示
        showStatusMessage(`正在获取媒体信息...`);

        // 获取API客户端
        const apiClient = getApiClient();
        if (!apiClient) {
            showStatusMessage('无法获取Emby API客户端', 'error');
            return null;
        }

        try {
            // 使用Emby内置API获取媒体信息
            const playbackInfo = await apiClient.getPlaybackInfo(itemId, {});

            // 获取更详细的项目信息
            const userId = apiClient._serverInfo ? apiClient._serverInfo.UserId : apiClient.getCurrentUserId();
            const itemInfo = await apiClient.getItem(userId, itemId);

            // 合并所有信息
            const fullInfo = {
                playbackInfo: playbackInfo,
                itemInfo: itemInfo
            };

            // 缓存结果
            cache.set(cacheKey, fullInfo);

            // 显示结果
            showMediaInfoResult(fullInfo);
            return fullInfo;

        } catch (error) {
            log.error('获取媒体信息出错:', error);
            showStatusMessage(`获取媒体信息失败: ${error.message}`, 'error');
            return null;
        }
    }

    // 获取电视节目的剧集信息
    async function getTvShowEpisodes(itemId) {
        try {
            const apiClient = getApiClient();
            if (!apiClient) {
                throw new Error('无法获取Emby API客户端');
            }

            const userId = apiClient._serverInfo ? apiClient._serverInfo.UserId : apiClient.getCurrentUserId();

            // 获取电视节目详情
            showStatusMessage(`正在获取电视节目信息...`);

            // 获取项目信息
            const itemInfo = await apiClient.getItem(userId, itemId);
            log.info('获取到的项目信息:', itemInfo);

            // 检查项目类型
            if (itemInfo.Type !== 'Series' && itemInfo.Type !== 'Season') {
                // 如果不是电视剧或季,使用电影处理方式
                log.info('检测到项目类型:', itemInfo.Type);
                await getMediaInfo(itemId);
                return [];
            }

            // 获取季信息或剧集信息
            let episodes = [];

            if (itemInfo.Type === 'Series') {
                // 获取电视剧的季列表
                showStatusMessage(`正在获取季信息...`);
                const seasons = await apiClient.getItems(userId, {
                    parentId: itemId,
                    includeItemTypes: 'Season'
                });

                if (!seasons || !seasons.Items || seasons.Items.length === 0) {
                    throw new Error('无法获取季信息,或者该剧没有季');
                }

                log.info(`找到 ${seasons.Items.length} 个季`);

                // 获取每个季的剧集
                let processed = 0;
                for (const season of seasons.Items) {
                    processed++;
                    showStatusMessage(`正在获取剧集信息 (季 ${processed}/${seasons.Items.length})...`);

                    try {
                        const episodesResult = await apiClient.getItems(userId, {
                            parentId: season.Id,
                            includeItemTypes: 'Episode'
                        });

                        if (episodesResult && episodesResult.Items) {
                            episodes.push(...episodesResult.Items);
                        }
                    } catch (error) {
                        log.error(`获取季 ${season.Name} 的剧集失败:`, error);
                    }

                    // 限流
                    if (processed < seasons.Items.length) {
                        await sleep(300);
                    }
                }
            } else if (itemInfo.Type === 'Season') {
                // 如果直接是季,获取该季的剧集
                showStatusMessage(`正在获取剧集信息...`);
                const episodesResult = await apiClient.getItems(userId, {
                    parentId: itemId,
                    includeItemTypes: 'Episode'
                });

                if (episodesResult && episodesResult.Items) {
                    episodes = episodesResult.Items;
                }
            }

            log.info(`找到 ${episodes.length} 个剧集`);
            return episodes;

        } catch (error) {
            log.error('获取电视节目信息出错:', error);
            showStatusMessage(`获取电视节目信息失败: ${error.message}`, 'error');
            return [];
        }
    }

    // 显示媒体信息结果
    function showMediaInfoResult(data) {
        let statusDiv = document.getElementById('emby-status-info');

        if (!statusDiv) {
            statusDiv = document.createElement('div');
            statusDiv.id = 'emby-status-info';
            statusDiv.style.cssText = 'position: fixed; top: 10px; right: 10px; padding: 10px; background: rgba(0,0,0,0.7); color: white; z-index: 9999; border-radius: 5px; max-width: 80%; max-height: 80vh; overflow-y: auto;';
            document.body.appendChild(statusDiv);
        }

        // 提取重要信息
        const mediaSource = data.playbackInfo.MediaSources && data.playbackInfo.MediaSources.length > 0
            ? data.playbackInfo.MediaSources[0] : {};
        const itemInfo = data.itemInfo || {};
        const mediaPath = mediaSource.Path || '未知路径';
        const mediaContainer = mediaSource.Container || '未知容器';

        // 获取媒体流信息
        let videoInfo = '未知';
        let audioInfo = '未知';
        let subtitleInfo = '';

        if (mediaSource.MediaStreams && mediaSource.MediaStreams.length > 0) {
            for (const stream of mediaSource.MediaStreams) {
                if (stream.Type === 'Video') {
                    videoInfo = `${stream.Codec || '未知'} (${stream.Width || '?'}x${stream.Height || '?'})`;
                    if (stream.BitRate) {
                        videoInfo += `, ${Math.round(stream.BitRate / 1000)} kbps`;
                    }
                } else if (stream.Type === 'Audio') {
                    audioInfo = `${stream.Codec || '未知'} (${stream.ChannelLayout || stream.Channels || '?'} 声道)`;
                    if (stream.Language) {
                        audioInfo += `, ${stream.Language}`;
                    }
                } else if (stream.Type === 'Subtitle') {
                    subtitleInfo += `${subtitleInfo ? '<br>' : ''}· ${stream.Language || '未知语言'} (${stream.Codec || '未知格式'})`;
                }
            }
        }

        // 构建基本信息HTML
        let infoHtml = `
            <div>
                <h3 style="margin-top: 0;">媒体信息获取成功</h3>
                <p><strong>名称:</strong> ${itemInfo.Name || '未知'}</p>
                <p><strong>路径:</strong> ${mediaPath}</p>
                <p><strong>容器格式:</strong> ${mediaContainer}</p>
                <p><strong>视频:</strong> ${videoInfo}</p>
                <p><strong>音频:</strong> ${audioInfo}</p>
        `;

        // 如果有字幕,添加字幕信息
        if (subtitleInfo) {
            infoHtml += `<p><strong>字幕:</strong><br>${subtitleInfo}</p>`;
        }

        // 添加详细信息和关闭按钮
        infoHtml += `
                <details>
                    <summary>详细信息</summary>
                    <div style="max-height: 40vh; overflow-y: auto;">
                        <pre style="white-space: pre-wrap; word-break: break-all; font-size: 12px;">${JSON.stringify(data, null, 2)}</pre>
                    </div>
                </details>
                <button id="close-emby-info" style="margin-top: 10px; padding: 5px 10px;">关闭</button>
            </div>
        `;

        statusDiv.innerHTML = infoHtml;

        document.getElementById('close-emby-info').addEventListener('click', function () {
            if (statusDiv && statusDiv.parentNode) {
                document.body.removeChild(statusDiv);
            }
        });
    }

    // 显示电视节目剧集信息摘要
    function showMediaInfoSummary(episodes) {
        if (!episodes || episodes.length === 0) return;

        let statusDiv = document.getElementById('emby-status-info');

        if (!statusDiv) {
            statusDiv = document.createElement('div');
            statusDiv.id = 'emby-status-info';
            statusDiv.style.cssText = 'position: fixed; top: 10px; right: 10px; padding: 10px; background: rgba(0,0,0,0.8); color: white; z-index: 9999; border-radius: 5px; max-width: 80%; max-height: 80vh; overflow-y: auto;';
            document.body.appendChild(statusDiv);
        }

        // 构建HTML
        let episodesHtml = '';
        episodes.slice(0, 20).forEach((episode) => {
            episodesHtml += `
                <div style="margin-bottom: 8px; padding: 5px; background: rgba(255,255,255,0.1); border-radius: 3px;">
                    <div><strong>${episode.IndexNumber || '?'}.${episode.Name || '未知'}</strong></div>
                    <div style="font-size: 12px;">${episode.Id}</div>
                    <button class="get-episode-info" data-id="${episode.Id}" style="margin-top: 5px; padding: 3px 8px; background: #2196F3; border: none; border-radius: 3px; color: white; cursor: pointer;">获取信息</button>
                </div>
            `;
        });

        if (episodes.length > 20) {
            episodesHtml += `<div>... 还有 ${episodes.length - 20} 集未显示</div>`;
        }

        statusDiv.innerHTML = `
            <div>
                <h3 style="margin-top: 0;">电视节目信息 (共 ${episodes.length} 集)</h3>
                <div style="max-height: 60vh; overflow-y: auto; margin: 10px 0;">
                    ${episodesHtml}
                </div>
                <div style="margin-top: 10px;">
                    <button id="get-all-episodes" style="padding: 5px 10px; margin-right: 10px; background: #4CAF50; border: none; border-radius: 3px; color: white; cursor: pointer;">获取所有剧集信息</button>
                    <button id="close-emby-info" style="padding: 5px 10px; background: #607D8B; border: none; border-radius: 3px; color: white; cursor: pointer;">关闭</button>
                </div>
            </div>
        `;

        // 绑定事件
        document.getElementById('close-emby-info').addEventListener('click', function () {
            if (statusDiv && statusDiv.parentNode) {
                document.body.removeChild(statusDiv);
            }
        });

        // 获取单个剧集信息
        document.querySelectorAll('.get-episode-info').forEach(button => {
            button.addEventListener('click', async function () {
                const episodeId = this.getAttribute('data-id');
                if (episodeId) {
                    await getMediaInfo(episodeId);
                }
            });
        });

        // 获取所有剧集信息
        document.getElementById('get-all-episodes').addEventListener('click', async function () {
            this.disabled = true;
            this.textContent = '处理中...';

            let processed = 0;
            const totalEpisodes = episodes.length;

            for (const episode of episodes) {
                processed++;
                showStatusMessage(`处理剧集 ${processed}/${totalEpisodes}...`);

                try {
                    await getMediaInfo(episode.Id);
                    // 适当延迟,防止请求过快
                    await sleep(300);
                } catch (error) {
                    log.error(`处理剧集 ${episode.Name} 出错:`, error);
                }

                // 每处理5个剧集暂停一下,防止请求过多
                if (processed % 5 === 0 && processed < totalEpisodes) {
                    await sleep(1000);
                }
            }

            showStatusMessage(`所有剧集处理完成 (${processed}/${totalEpisodes})`);
            this.textContent = '已完成';
        });
    }

    // 清除媒体信息缓存
    function clearMediaInfoCache() {
        const prefix = 'media_info_';
        let count = 0;

        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key && key.startsWith(prefix)) {
                localStorage.removeItem(key);
                count++;
            }
        }

        log.info(`已清除 ${count} 条媒体信息缓存`);
    }

    // 添加操作按钮
    function addActionButtons() {
        // 移除现有按钮(如果存在)
        const existingButtons = document.querySelectorAll('.emby-custom-button');
        existingButtons.forEach(button => {
            if (button && button.parentNode) {
                button.parentNode.removeChild(button);
            }
        });

        // 添加刷新按钮
        const refreshButton = document.createElement('button');
        refreshButton.textContent = '获取媒体信息';
        refreshButton.className = 'emby-custom-button';
        refreshButton.style.cssText = 'position: fixed; bottom: 20px; right: 20px; padding: 10px; background: #00a8ff; color: white; border: none; border-radius: 5px; cursor: pointer; z-index: 9999;';

        refreshButton.addEventListener('click', async function () {
            // 移除现有的媒体信息显示
            const existingInfo = document.getElementById('emby-status-info');
            if (existingInfo && existingInfo.parentNode) {
                document.body.removeChild(existingInfo);
            }

            const itemId = extractItemId();

            if (!itemId) {
                showStatusMessage('无法识别当前项目ID,请确保您在媒体详情页面', 'error');
                return;
            }

            // 清除此项目的缓存
            const cacheKey = `media_info_${itemId}`;
            cache.remove(cacheKey);

            try {
                // 获取API客户端
                const apiClient = getApiClient();
                if (!apiClient) {
                    showStatusMessage('无法获取Emby API客户端', 'error');
                    return;
                }

                const userId = apiClient._serverInfo ? apiClient._serverInfo.UserId : apiClient.getCurrentUserId();

                // 获取项目信息
                showStatusMessage('正在获取项目信息...');
                const itemInfo = await apiClient.getItem(userId, itemId);

                log.info('项目类型:', itemInfo.Type);

                // 根据项目类型决定处理方式
                if (itemInfo.Type === 'Series' || itemInfo.Type === 'Season') {
                    // 电视剧系列或季
                    const episodes = await getTvShowEpisodes(itemId);

                    if (episodes.length > 0) {
                        showMediaInfoSummary(episodes);
                    } else {
                        showStatusMessage('没有找到剧集信息', 'error');
                    }
                } else {
                    // 电影、单集或其他类型
                    await getMediaInfo(itemId);
                }
            } catch (error) {
                log.error('处理媒体信息出错:', error);
                showStatusMessage(`处理媒体信息失败: ${error.message}`, 'error');

                // 出错时尝试直接获取媒体信息
                try {
                    await getMediaInfo(itemId);
                } catch (fallbackError) {
                    log.error('备用方法也失败:', fallbackError);
                }
            }
        });

        document.body.appendChild(refreshButton);

        // 添加清除缓存按钮
        const clearCacheButton = document.createElement('button');
        clearCacheButton.textContent = '清除缓存';
        clearCacheButton.className = 'emby-custom-button';
        clearCacheButton.style.cssText = 'position: fixed; bottom: 20px; right: 150px; padding: 10px; background: #f44336; color: white; border: none; border-radius: 5px; cursor: pointer; z-index: 9999;';

        clearCacheButton.addEventListener('click', function () {
            clearMediaInfoCache();
            showStatusMessage('媒体信息缓存已清除');
        });

        document.body.appendChild(clearCacheButton);
    }

    // 初始化
    function init() {
        log.info('Emby媒体信息获取助手-精简版已启动');

        // 添加按钮
        setTimeout(() => {
            addActionButtons();

            // 监听URL变化
            let lastUrl = window.location.href;
            setInterval(() => {
                const currentUrl = window.location.href;
                if (currentUrl !== lastUrl) {
                    lastUrl = currentUrl;
                    addActionButtons();
                }
            }, 1000);
        }, 1000);
    }

    // 启动脚本
    init();
})();