Greasy Fork

来自缓存

Greasy Fork is available in English.

Emby Hide Media Configurable Tag

Add "Hide Media" and "Unhide Media" options to Emby context menu to tag/untag all versions of selected media with a configurable tag

当前为 2025-06-16 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Emby Hide Media Configurable Tag
// @namespace    http://tampermonkey.net/
// @version      2.13
// @description  Add "Hide Media" and "Unhide Media" options to Emby context menu to tag/untag all versions of selected media with a configurable tag
// @author       Baiganjia
// @match        http://127.0.0.1:8886/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const HIDE_TAG = '待批判';
    const EMBY_URL = 'http://127.0.0.1:8886'; //填你的地址
    const API_KEY = 'cc761e0a44424e639c4970dcf4b6450f';  //填你的APIKEY
    const FALLBACK_USER_ID = '00000000000000000000000000000000'; //这个应该不用改

    // Utility: Format timestamp for logs
    function getTimestamp() {
        return new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
    }

    // Utility: Get USER_ID dynamically
    function getUserId() {
        try {
            const userId = window.ApiClient?.getCurrentUserId();
            if (userId) {
                console.log(`✅ 动态获取 USER_ID: ${userId}`);
                return userId;
            }
            console.warn(`⚠️ window.ApiClient.getCurrentUserId 未定义,使用回退 USER_ID: ${FALLBACK_USER_ID}`);
            return FALLBACK_USER_ID;
        } catch (error) {
            console.warn(`❌ 获取 USER_ID 失败: ${error.message},使用回退 USER_ID: ${FALLBACK_USER_ID}`);
            return FALLBACK_USER_ID;
        }
    }

    const USER_ID = getUserId();

    // Debounce utility
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // Add "Hide Media" and "Unhide Media" options to context menu
    function addMenuOptions() {
        const actionSheet = document.querySelector('.actionSheetScroller');
        if (!actionSheet) return;

        // Add "Hide Media" option
        if (!document.querySelector('#hideMedia')) {
            const hideMenuItem = document.createElement('button');
            hideMenuItem.className = 'listItem listItem-autoactive itemAction listItemCursor listItem-hoverable actionSheetMenuItem actionSheetMenuItem-iconright';
            hideMenuItem.id = 'hideMedia';
            hideMenuItem.setAttribute('data-id', 'hideMedia');
            hideMenuItem.setAttribute('data-action', 'custom');
            hideMenuItem.innerHTML = `
                <div class="listItem-content listItem-content-bg listItemContent-touchzoom listItem-border">
                    <div class="actionSheetItemImageContainer actionSheetItemImageContainer-customsize actionSheetItemImageContainer-transparent listItemImageContainer listItemImageContainer-margin listItemImageContainer-square defaultCardBackground" style="aspect-ratio:1">
                        <i class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent md-icon listItemIcon md-icon autortl">visibility_off</i>
                    </div>
                    <div class="actionsheetListItemBody actionsheetListItemBody-iconright listItemBody listItemBody-1-lines">
                        <div class="listItemBodyText actionSheetItemText listItemBodyText-nowrap listItemBodyText-lf">隐藏媒体</div>
                    </div>
                </div>
            `;
            hideMenuItem.addEventListener('click', hideSelectedMedia);
            actionSheet.querySelector('.actionsheetScrollSlider').appendChild(hideMenuItem);
        }

        // Add "Unhide Media" option
        if (!document.querySelector('#unhideMedia')) {
            const unhideMenuItem = document.createElement('button');
            unhideMenuItem.className = 'listItem listItem-autoactive itemAction listItemCursor listItem-hoverable actionSheetMenuItem actionSheetMenuItem-iconright';
            unhideMenuItem.id = 'unhideMedia';
            unhideMenuItem.setAttribute('data-id', 'unhideMedia');
            unhideMenuItem.setAttribute('data-action', 'custom');
            unhideMenuItem.innerHTML = `
                <div class="listItem-content listItem-content-bg listItemContent-touchzoom listItem-border">
                    <div class="actionSheetItemImageContainer actionSheetItemImageContainer-customsize actionSheetItemImageContainer-transparent listItemImageContainer listItemImageContainer-margin listItemImageContainer-square defaultCardBackground" style="aspect-ratio:1">
                        <i class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent md-icon listItemIcon md-icon autortl">visibility</i>
                    </div>
                    <div class="actionsheetListItemBody actionsheetListItemBody-iconright listItemBody listItemBody-1-lines">
                        <div class="listItemBodyText actionSheetItemText listItemBodyText-nowrap listItemBodyText-lf">取消隐藏</div>
                    </div>
                </div>
            `;
            unhideMenuItem.addEventListener('click', unhideSelectedMedia);
            actionSheet.querySelector('.actionsheetScrollSlider').appendChild(unhideMenuItem);
        }
    }

    // Get related ItemIds from MediaSources
    async function getRelatedItemIds(mediaId) {
        console.group(`[${getTimestamp()}] 获取媒体 ${mediaId} 的相关 ItemIds`);
        try {
            const url = `${EMBY_URL}/Users/${USER_ID}/Items/${mediaId}?api_key=${API_KEY}&Fields=MediaSources,Name`;
            console.log('🔗 请求URL:', url);
            const response = await fetch(url, {
                method: 'GET',
                headers: { 'Accept': 'application/json' }
            });
            if (!response.ok) {
                console.warn('❌ API响应错误:', response.status, response.statusText);
                throw new Error(`获取 MediaSources 失败: ${response.status}`);
            }
            const data = await response.json();
            console.log('📄 API响应:', JSON.stringify(data, null, 2));
            const mediaSources = data?.MediaSources || [];
            const itemIds = mediaSources.map(source => ({
                Id: source.ItemId,
                Name: source.Name,
                Type: source.Type
            }));
            if (itemIds.length === 0) {
                console.warn(`⚠️ 未找到 MediaSources for 媒体 ${mediaId},退回到 mediaId`);
                itemIds.push({ Id: mediaId, Name: data?.Name || '未知', Type: 'Default' });
            }
            console.table('📋 相关 ItemIds:', itemIds);
            console.log(`✅ 媒体 ${mediaId} 对应的 ItemIds: ${itemIds.map(item => item.Id).join(', ')}`);
            return itemIds.map(item => item.Id);
        } catch (error) {
            console.warn(`❌ 无法获取媒体 ${mediaId} 的相关 ItemIds:`, error.message);
            return [mediaId]; // Fallback to original mediaId
        } finally {
            console.groupEnd();
        }
    }

    // Add configurable tag to a media item
    async function addTagToMedia(mediaId) {
        console.group(`[${getTimestamp()}] 为媒体 ${mediaId} 添加标签`);
        try {
            const url = `${EMBY_URL}/Items/${mediaId}/Tags/Add?api_key=${API_KEY}`;
            console.log('🔗 请求URL:', url);
            const requestBody = { Tags: [{ Name: HIDE_TAG }] };
            console.log('📤 请求体:', JSON.stringify(requestBody, null, 2));
            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'Accept': '*/*',
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(requestBody)
            });
            if (!response.ok) {
                console.warn('❌ Tags格式响应:', response.status, response.statusText, await response.text());
                throw new Error(`添加标签失败 (Tags format): ${response.status}`);
            }
            console.log(`✅ 媒体 ${mediaId} 通过 Tags 格式成功添加“${HIDE_TAG}”标签`);
            return true;
        } catch (error) {
            console.warn(`❌ 为媒体 ${mediaId} 使用 Tags 格式添加标签失败:`, error.message);
            try {
                const fallbackUrl = `${EMBY_URL}/Items/${mediaId}/Tags/Add?api_key=${API_KEY}`;
                console.log('🔗 备用请求URL:', fallbackUrl);
                const fallbackBody = { TagItems: [HIDE_TAG] };
                console.log('📤 备用请求体:', JSON.stringify(fallbackBody, null, 2));
                const fallbackResponse = await fetch(fallbackUrl, {
                    method: 'POST',
                    headers: {
                        'Accept': '*/*',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(fallbackBody)
                });
                if (!fallbackResponse.ok) {
                    console.warn('❌ TagItems格式响应:', fallbackResponse.status, fallbackResponse.statusText, await fallbackResponse.text());
                    throw new Error(`添加标签失败 (TagItems format): ${fallbackResponse.status}`);
                }
                console.log(`✅ 媒体 ${mediaId} 通过 TagItems 格式成功添加“${HIDE_TAG}”标签`);
                return true;
            } catch (fallbackError) {
                console.error(`❌ 为媒体 ${mediaId} 添加标签失败:`, fallbackError.message);
                return false;
            }
        } finally {
            console.groupEnd();
        }
    }

    // Remove configurable tag from a media item
    async function removeTagFromMedia(mediaId) {
        console.group(`[${getTimestamp()}] 为媒体 ${mediaId} 移除标签`);
        try {
            const url = `${EMBY_URL}/Items/${mediaId}/Tags/Delete?api_key=${API_KEY}`;
            console.log('🔗 请求URL:', url);
            const requestBody = { Tags: [{ Name: HIDE_TAG, Id: "" }] };
            console.log('📤 请求体:', JSON.stringify(requestBody, null, 2));
            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'Accept': '*/*',
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(requestBody)
            });
            if (!response.ok) {
                console.warn('❌ Tags格式响应:', response.status, response.statusText, await response.text());
                throw new Error(`移除标签失败 (Tags format): ${response.status}`);
            }
            console.log(`✅ 媒体 ${mediaId} 通过 Tags 格式成功移除“${HIDE_TAG}”标签`);
            return true;
        } catch (error) {
            console.warn(`❌ 为媒体 ${mediaId} 使用 Tags 格式移除标签失败:`, error.message);
            try {
                const fallbackUrl = `${EMBY_URL}/Items/${mediaId}/Tags/Delete?api_key=${API_KEY}`;
                console.log('🔗 备用请求URL:', fallbackUrl);
                const fallbackBody = { TagItems: [HIDE_TAG] };
                console.log('📤 备用请求体:', JSON.stringify(fallbackBody, null, 2));
                const fallbackResponse = await fetch(fallbackUrl, {
                    method: 'POST',
                    headers: {
                        'Accept': '*/*',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(fallbackBody)
                });
                if (!fallbackResponse.ok) {
                    console.warn('❌ TagItems格式响应:', fallbackResponse.status, fallbackResponse.statusText, await fallbackResponse.text());
                    throw new Error(`移除标签失败 (TagItems format): ${fallbackResponse.status}`);
                }
                console.log(`✅ 媒体 ${mediaId} 通过 TagItems 格式成功移除“${HIDE_TAG}”标签`);
                return true;
            } catch (fallbackError) {
                console.error(`❌ 为媒体 ${mediaId} 移除标签失败:`, fallbackError.message);
                return false;
            }
        } finally {
            console.groupEnd();
        }
    }

    // Handle "Hide Media" action
    async function hideSelectedMedia(event) {
        console.group(`[${getTimestamp()}] 处理隐藏媒体操作`);
        const button = event.target.closest('button');
        let originalText, buttonDisabled;
        if (button) {
            originalText = button.textContent;
            button.textContent = '处理中...';
            button.disabled = true;
            buttonDisabled = true;
            console.log('🔘 按钮状态: 处理中');
        }

        try {
            let selectedItems = document.querySelectorAll('input[type=checkbox]:checked');
            let context = 'multi-select';
            if (selectedItems.length === 0) {
                const targetCard = event.target.closest('.card');
                if (targetCard) {
                    console.log('📍 右键菜单目标卡片:', targetCard.outerHTML);
                    selectedItems = [targetCard];
                    context = 'single-select';
                } else {
                    console.warn('⚠️ 未找到选中的媒体项目');
                    alert('请先选择至少一个媒体项目!');
                    return;
                }
            }

            console.log(`📊 选中的项目 (${context}): ${selectedItems.length}`);
            console.table('📋 勾选的媒体:', Array.from(selectedItems).map((item, index) => {
                const card = item.closest('.card') || item;
                const titleElement = card.querySelector('.cardText, .cardContent, [title], .listItemBodyText');
                const title = titleElement ? titleElement.textContent.trim() || titleElement.getAttribute('title') : '未知标题';
                return { Index: index + 1, Title: title };
            }));

            let successCount = 0;
            let failureCount = 0;

            const itemIdPromises = Array.from(selectedItems).map(async (item, index) => {
                console.group(`[${getTimestamp()}] 处理媒体 #${index + 1}`);
                const card = item.closest('.card') || item;
                const titleElement = card.querySelector('.cardText, .cardContent, [title], .listItemBodyText');
                const title = titleElement ? titleElement.textContent.trim() || titleElement.getAttribute('title') : '未知标题';
                console.log('📌 当前勾选的媒体:', title);
                console.log('📄 卡片HTML:', card.outerHTML);

                let mediaId;
                const img = card.querySelector('img[src*="/Items/"]');
                if (img) {
                    const match = img.src.match(/\/Items\/(\d+)/);
                    mediaId = match ? match[1] : null;
                }
                if (!mediaId) {
                    mediaId = card.getAttribute('data-id') || card.getAttribute('data-itemid') || card.getAttribute('data-mediaid');
                }
                if (!mediaId) {
                    const idElement = card.querySelector('[data-id], [data-itemid], [data-mediaid], button[data-action="link"]');
                    mediaId = idElement ? idElement.getAttribute('data-id') || idElement.getAttribute('data-itemid') || idElement.getAttribute('data-mediaid') : null;
                }
                if (!mediaId) {
                    console.warn('⚠️ 无法获取媒体ID for 媒体:', title);
                    console.groupEnd();
                    return { mediaId: null, relatedItemIds: [], title };
                }
                console.log(`📍 当前勾选的媒体ID: ${mediaId}`);
                const relatedItemIds = await getRelatedItemIds(mediaId);
                console.groupEnd();
                return { mediaId, relatedItemIds, title };
            });

            const itemIdResults = await Promise.all(itemIdPromises);
            console.table('📋 ItemIds 获取结果:', itemIdResults.map((result, index) => ({
                Index: index + 1,
                Title: result.title,
                MediaId: result.mediaId || '未获取',
                RelatedItemIds: result.relatedItemIds.join(', ') || '无'
            })));

            const tagPromises = itemIdResults.map(async ({ mediaId, relatedItemIds, title }) => {
                console.group(`[${getTimestamp()}] 为媒体 ${title} (ID: ${mediaId}) 添加标签`);
                if (!mediaId) {
                    console.warn('⚠️ 跳过: 无有效 MediaId');
                    failureCount++;
                    console.groupEnd();
                    return;
                }
                const itemIds = relatedItemIds.length > 0 ? relatedItemIds : [mediaId];
                console.log(`📊 处理的 ItemIds: ${itemIds.join(', ')}`);
                const tagResults = await Promise.all(itemIds.map(async (id) => {
                    console.log(`📍 为版本 ItemId ${id} 添加标签`);
                    const success = await addTagToMedia(id);
                    return success ? 1 : 0;
                }));
                successCount += tagResults.reduce((sum, val) => sum + val, 0);
                failureCount += itemIds.length - tagResults.reduce((sum, val) => sum + val, 0);
                console.groupEnd();
            });

            await Promise.all(tagPromises);

            console.log(`🎉 操作完成: 成功 ${successCount} 个,失败 ${failureCount} 个`);
            alert(`操作完成!成功为 ${successCount} 个媒体版本添加“${HIDE_TAG}”标签,${failureCount} 个失败。页面将自动刷新以应用隐藏效果。`);
            setTimeout(() => location.reload(), 2000);

            const actionSheet = document.querySelector('.actionSheet');
            if (actionSheet) actionSheet.remove();
        } catch (error) {
            console.error(`❌ 操作失败:`, error.message);
            alert(`操作失败: ${error.message}`);
        } finally {
            if (button && buttonDisabled) {
                button.textContent = originalText;
                button.disabled = false;
                console.log('🔘 按钮状态: 恢复');
            }
            console.groupEnd();
        }
    }

    // Handle "Unhide Media" action
    async function unhideSelectedMedia(event) {
        console.group(`[${getTimestamp()}] 处理取消隐藏媒体操作`);
        const button = event.target.closest('button');
        let originalText, buttonDisabled;
        if (button) {
            originalText = button.textContent;
            button.textContent = '处理中...';
            button.disabled = true;
            buttonDisabled = true;
            console.log('🔘 按钮状态: 处理中');
        }

        try {
            let selectedItems = document.querySelectorAll('input[type=checkbox]:checked');
            let context = 'multi-select';
            if (selectedItems.length === 0) {
                const targetCard = event.target.closest('.card');
                if (targetCard) {
                    console.log('📍 右键菜单目标卡片:', targetCard.outerHTML);
                    selectedItems = [targetCard];
                    context = 'single-select';
                } else {
                    console.warn('⚠️ 未找到选中的媒体项目');
                    alert('请先选择至少一个媒体项目!');
                    return;
                }
            }

            console.log(`📊 选中的项目 (${context}): ${selectedItems.length}`);
            console.table('📋 勾选的媒体:', Array.from(selectedItems).map((item, index) => {
                const card = item.closest('.card') || item;
                const titleElement = card.querySelector('.cardText, .cardContent, [title], .listItemBodyText');
                const title = titleElement ? titleElement.textContent.trim() || titleElement.getAttribute('title') : '未知标题';
                return { Index: index + 1, Title: title };
            }));

            let successCount = 0;
            let failureCount = 0;

            const itemIdPromises = Array.from(selectedItems).map(async (item, index) => {
                console.group(`[${getTimestamp()}] 处理媒体 #${index + 1}`);
                const card = item.closest('.card') || item;
                const titleElement = card.querySelector('.cardText, .cardContent, [title], .listItemBodyText');
                const title = titleElement ? titleElement.textContent.trim() || titleElement.getAttribute('title') : '未知标题';
                console.log('📌 当前勾选的媒体:', title);
                console.log('📄 卡片HTML:', card.outerHTML);

                let mediaId;
                const img = card.querySelector('img[src*="/Items/"]');
                if (img) {
                    const match = img.src.match(/\/Items\/(\d+)/);
                    mediaId = match ? match[1] : null;
                }
                if (!mediaId) {
                    mediaId = card.getAttribute('data-id') || card.getAttribute('data-itemid') || card.getAttribute('data-mediaid');
                }
                if (!mediaId) {
                    const idElement = card.querySelector('[data-id], [data-itemid], [data-mediaid], button[data-action="link"]');
                    mediaId = idElement ? idElement.getAttribute('data-id') || idElement.getAttribute('data-itemid') || idElement.getAttribute('data-mediaid') : null;
                }
                if (!mediaId) {
                    console.warn('⚠️ 无法获取媒体ID for 媒体:', title);
                    console.groupEnd();
                    return { mediaId: null, relatedItemIds: [], title };
                }
                console.log(`📍 当前勾选的媒体ID: ${mediaId}`);
                const relatedItemIds = await getRelatedItemIds(mediaId);
                console.groupEnd();
                return { mediaId, relatedItemIds, title };
            });

            const itemIdResults = await Promise.all(itemIdPromises);
            console.table('📋 ItemIds 获取结果:', itemIdResults.map((result, index) => ({
                Index: index + 1,
                Title: result.title,
                MediaId: result.mediaId || '未获取',
                RelatedItemIds: result.relatedItemIds.join(', ') || '无'
            })));

            const tagPromises = itemIdResults.map(async ({ mediaId, relatedItemIds, title }) => {
                console.group(`[${getTimestamp()}] 为媒体 ${title} (ID: ${mediaId}) 移除标签`);
                if (!mediaId) {
                    console.warn('⚠️ 跳过: 无有效 MediaId');
                    failureCount++;
                    console.groupEnd();
                    return;
                }
                const itemIds = relatedItemIds.length > 0 ? relatedItemIds : [mediaId];
                console.log(`📊 处理的 ItemIds: ${itemIds.join(', ')}`);
                const tagResults = await Promise.all(itemIds.map(async (id) => {
                    console.log(`📍 为版本 ItemId ${id} 移除标签`);
                    const success = await removeTagFromMedia(id);
                    return success ? 1 : 0;
                }));
                successCount += tagResults.reduce((sum, val) => sum + val, 0);
                failureCount += itemIds.length - tagResults.reduce((sum, val) => sum + val, 0);
                console.groupEnd();
            });

            await Promise.all(tagPromises);

            console.log(`🎉 操作完成: 成功 ${successCount} 个,失败 ${failureCount} 个`);
            alert(`操作完成!成功为 ${successCount} 个媒体版本移除“${HIDE_TAG}”标签,${failureCount} 个失败。页面将自动刷新以恢复显示。`);
            setTimeout(() => location.reload(), 2000);

            const actionSheet = document.querySelector('.actionSheet');
            if (actionSheet) actionSheet.remove();
        } catch (error) {
            console.error(`❌ 操作失败:`, error.message);
            alert(`操作失败: ${error.message}`);
        } finally {
            if (button && buttonDisabled) {
                button.textContent = originalText;
                button.disabled = false;
                console.log('🔘 按钮状态: 恢复');
            }
            console.groupEnd();
        }
    }

    const debouncedAddMenuOptions = debounce(addMenuOptions, 100);

    const observer = new MutationObserver(() => {
        if (document.querySelector('.actionSheet') && (!document.querySelector('#hideMedia') || !document.querySelector('#unhideMedia'))) {
            debouncedAddMenuOptions();
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    document.addEventListener('click', () => {
        if (document.querySelector('.actionSheet')) {
            debouncedAddMenuOptions();
        }
    });
})();