// ==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 = `
`;
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();
}
});
})();