// ==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 // @downloadURL 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 = `
visibility_off
隐藏媒体
`; 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(); } }); })();