Emby Hide Media Configurable Tag

Add a "Hide Media" option to Emby context menu to tag all versions of selected media with a configurable tag

// ==UserScript==
// @name         Emby Hide Media Configurable Tag
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  Add a "Hide Media" option to Emby context menu to tag 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: Change HIDE_TAG to your desired tag name
    const HIDE_TAG = '待批判'; // Modify this to any tag, e.g., '隐藏', '待删除'
    // Emby server URL and API key
    const EMBY_URL = 'http://127.0.0.1:8886';
    const API_KEY = 'c81ce450fe4c4b2db8ac0d592d6192ef';

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

    // Function to add "Hide Media" option to context menu
    function addHideMediaOption() {
        const actionSheet = document.querySelector('.actionSheetScroller');
        if (!actionSheet || document.querySelector('#hideMedia')) return;

        const menuItem = document.createElement('button');
        menuItem.className = 'listItem listItem-autoactive itemAction listItemCursor listItem-hoverable actionSheetMenuItem actionSheetMenuItem-iconright';
        menuItem.id = 'hideMedia';
        menuItem.setAttribute('data-id', 'hideMedia');
        menuItem.setAttribute('data-action', 'custom');
        menuItem.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>
        `;
        menuItem.addEventListener('click', hideSelectedMedia);
        actionSheet.querySelector('.actionsheetScrollSlider').appendChild(menuItem);
    }

    // Function to get TmdbId for a given mediaId
    async function getTmdbId(mediaId) {
        try {
            const response = await fetch(`${EMBY_URL}/Items/${mediaId}?Fields=ProviderIds&api_key=${API_KEY}`, {
                method: 'GET',
                headers: { 'Accept': 'application/json' }
            });
            if (!response.ok) throw new Error(`获取 TmdbId 失败: ${response.status}`);
            const data = await response.json();
            const tmdbId = data?.ProviderIds?.Tmdb;
            if (!tmdbId) throw new Error('TmdbId 未找到');
            console.log(`媒体 ${mediaId} 的 TmdbId: ${tmdbId}`);
            return tmdbId;
        } catch (error) {
            console.warn(`无法获取媒体 ${mediaId} 的 TmdbId:`, error);
            return null;
        }
    }

    // Function to get all ItemIds for a given TmdbId
    async function getItemIdsByTmdbId(tmdbId) {
        try {
            const response = await fetch(`${EMBY_URL}/Items?ProviderIds.Tmdb=${tmdbId}&IncludeItemTypes=Movie&api_key=${API_KEY}`, {
                method: 'GET',
                headers: { 'Accept': 'application/json' }
            });
            if (!response.ok) throw new Error(`查询 TmdbId ${tmdbId} 的版本失败: ${response.status}`);
            const data = await response.json();
            const itemIds = data?.Items?.map(item => item.Id) || [];
            console.log(`TmdbId ${tmdbId} 对应的 ItemIds: ${itemIds.join(', ')}`);
            return itemIds;
        } catch (error) {
            console.warn(`无法查询 TmdbId ${tmdbId} 的版本:`, error);
            return [];
        }
    }

    // Function to add configurable tag to a media item
    async function addTagToMedia(mediaId) {
        try {
            const response = await fetch(`${EMBY_URL}/Items/${mediaId}/Tags/Add?api_key=${API_KEY}`, {
                method: 'POST',
                headers: {
                    'Accept': '*/*',
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ Tags: [{ Name: HIDE_TAG }] })
            });
            if (!response.ok) throw new Error(`添加标签失败 (Tags format): ${response.status}`);
            console.log(`媒体 ${mediaId} 通过 Tags 格式成功添加“${HIDE_TAG}”标签`);
            return true;
        } catch (error) {
            console.warn(`为媒体 ${mediaId} 使用 Tags 格式添加标签失败:`, error);
            // Fallback to TagItems format
            try {
                const fallbackResponse = await fetch(`${EMBY_URL}/Items/${mediaId}/Tags/Add?api_key=${API_KEY}`, {
                    method: 'POST',
                    headers: {
                        'Accept': '*/*',
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ TagItems: [HIDE_TAG] })
                });
                if (!fallbackResponse.ok) throw new Error(`添加标签失败 (TagItems format): ${fallbackResponse.status}`);
                console.log(`媒体 ${mediaId} 通过 TagItems 格式成功添加“${HIDE_TAG}”标签`);
                return true;
            } catch (fallbackError) {
                console.error(`为媒体 ${mediaId} 添加标签失败:`, fallbackError);
                return false;
            }
        }
    }

    // Function to handle "Hide Media" action
    async function hideSelectedMedia() {
        // Try multiple selectors to find selected items
        let selectedItems = document.querySelectorAll('.selectionCommandsPanel input[type=checkbox]:checked');
        let context = 'multi-select';
        if (selectedItems.length === 0) {
            // Fallback for single selection or context menu
            selectedItems = document.querySelectorAll('.card[data-id].selected, .card[data-id]:has(input[type=checkbox]:checked), .card[data-id][data-context]');
            context = 'single-select';
        }

        if (selectedItems.length === 0) {
            console.warn('未找到选中的媒体项目');
            alert('请先选择至少一个媒体项目!');
            return;
        }

        console.log(`选中的项目 (${context}):`, selectedItems.length);
        let successCount = 0;
        let failureCount = 0;

        for (const item of selectedItems) {
            // Get data-id from card or parent
            const card = item.closest('.card') || item;
            const mediaId = card.getAttribute('data-id');
            if (!mediaId) {
                console.warn('无法获取媒体ID:', card);
                failureCount++;
                continue;
            }

            console.log(`处理媒体ID: ${mediaId}`);
            // Get TmdbId for the media
            const tmdbId = await getTmdbId(mediaId);
            let itemIds = [mediaId]; // Fallback to single mediaId if TmdbId fails

            // If TmdbId is available, get all related ItemIds
            if (tmdbId) {
                const relatedItemIds = await getItemIdsByTmdbId(tmdbId);
                if (relatedItemIds.length > 0) {
                    itemIds = relatedItemIds;
                }
            }

            // Add tag to each ItemId
            for (const id of itemIds) {
                console.log(`为版本 ItemId ${id} 添加标签`);
                const success = await addTagToMedia(id);
                if (success) {
                    successCount++;
                } else {
                    failureCount++;
                }
            }
        }

        // Show completion message and trigger page refresh
        alert(`操作完成!成功为 ${successCount} 个媒体版本添加“${HIDE_TAG}”标签,${failureCount} 个失败。页面将自动刷新以应用隐藏效果。`);
        setTimeout(() => {
            location.reload();
        }, 1000); // Delay refresh by 1 second to allow user to read message

        // Close the action sheet
        const actionSheet = document.querySelector('.actionSheet');
        if (actionSheet) actionSheet.remove();
    }

    // Debounced function to add menu item
    const debouncedAddHideMediaOption = debounce(addHideMediaOption, 100);

    // Observe actionSheet container
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length) {
                const actionSheet = document.querySelector('.actionSheet');
                if (actionSheet) {
                    debouncedAddHideMediaOption();
                }
            }
        }
    });

    // Start observing with limited scope
    observer.observe(document.body, { childList: true, subtree: false });

    // Ensure menu is added when actionSheet appears
    document.addEventListener('click', () => {
        if (document.querySelector('.actionSheet')) {
            debouncedAddHideMediaOption();
        }
    });
})();