您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
【二合一】功能1: 在详情页和媒体卡片上添加“一键搜索字幕”按钮(内置智能重试机制,更稳定)。功能2: 在各处“标为已播放”旁添加“刷新元数据”按钮。功能可独立开关。
当前为
// ==UserScript== // @name Jellyfin元数据辅助 // @namespace http://tampermonkey.net/ // @version 3.5 // @description 【二合一】功能1: 在详情页和媒体卡片上添加“一键搜索字幕”按钮(内置智能重试机制,更稳定)。功能2: 在各处“标为已播放”旁添加“刷新元数据”按钮。功能可独立开关。 // @author Gemini & mojie // @match *://*/web/* // @icon https://www.google.com/s2/favicons?sz=64&domain=jellyfin.org // @grant GM_addStyle // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; // ========================================================================================= // ——— 配置中心 ——— // ========================================================================================= // --- 功能开关 --- const ENABLE_SUBTITLE_SEARCH = true; // true: 开启“一键搜索字幕”功能, false: 关闭 const ENABLE_METADATA_REFRESH = true; // true: 开启“刷新元数据”功能, false: 关闭 // --- “刷新元数据”功能所需的 API Key --- // 获取方法: 登录(不可用) Jellyfin -> 控制台 -> API 密钥 -> 点击(+)创建 -> 复制并粘贴到下方引号之间 const MANUAL_API_KEY = '在此处粘贴你的API密钥'; // <--- 在这里替换 // ========================================================================================= // ——— 样式定义 (合并) ——— // ========================================================================================= GM_addStyle(` /* 详情页“一键搜索字幕”按钮的样式 */ .custom-detail-page-subs-btn { margin-left: 0.25em; min-width: 0; padding: 0.5em; } .custom-detail-page-subs-btn .detailButton-content { padding: 0 !important; display: flex; align-items: center; } /* 刷新元数据按钮的样式 */ .btn-item-refresh { min-width: 42px !important; padding: 0 !important; } .btn-item-refresh .material-icons { color: rgba(255,255,255,0.87); transition: transform 0.3s cubic-bezier(.34,1.56,.64,1); } .btn-item-refresh:hover .material-icons { transform: scale(1.1) rotate(-180deg); } .btn-item-refresh.is-loading .material-icons { animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `); // ========================================================================================= // ——— 核心逻辑 (合并) ——— // ========================================================================================= const CONSTANTS = { SUBTITLE: { DETAIL_PAGE_MORE_SELECTOR: 'button.detailButton[data-action="menu"][title="更多"]', CARD_MORE_SELECTOR: 'button.cardOverlayButton[data-action="menu"][title="更多"]', EDIT_SUBTITLES_SELECTOR: 'button[data-id="editsubtitles"]', SEARCH_BUTTON_SELECTOR: 'button.btnSearchSubtitles', MARKER_ATTRIBUTE: 'data-search-button-added' }, METADATA: { PLAYSTATE_BUTTON_SELECTOR: 'button[is="emby-playstatebutton"]', MARKER_ATTRIBUTE: 'data-refresh-button-added', ICON_NAME: 'sync' } }; // --- 功能1: 一键搜索字幕 --- /** * 【新】智能重试函数:尝试查找元素,如果失败则重试。 * @param {string} selector - 要查找的元素的CSS选择器。 * @param {number} retries - 重试次数上限。 * @param {number} interval - 每次重试之间的间隔(毫秒)。 * @returns {Promise<HTMLElement|null>} - 成功时返回找到的元素,失败时返回null。 */ async function findElementWithRetry(selector, retries = 3, interval = 300) { for (let i = 0; i <= retries; i++) { const element = document.querySelector(selector); if (element) return element; // 成功找到元素 if (i < retries) { console.log(`[Jellyfin元数据辅助] 未找到元素 "${selector}", ${interval}ms后重试 (第 ${i + 1} 次)...`); await new Promise(res => setTimeout(res, interval)); } } console.error(`[Jellyfin元数据辅助] 超过最大重试次数,未能找到元素 "${selector}"。`); return null; // 超过最大重试次数 } /** * 【新】通用的“一键搜索字幕”点击逻辑(采用智能重试) * @param {HTMLElement} moreButtonElement - 触发操作的“更多”按钮元素 */ async function performSubtitleSearch(moreButtonElement) { console.log(`[Jellyfin元数据辅助] 开始执行“一键搜索字幕”...`); try { // 步骤1: 点击“更多”按钮。这是起始操作,无需重试。 if (moreButtonElement && typeof moreButtonElement.click === 'function') { moreButtonElement.click(); } else { throw new Error('步骤1失败: "更多"按钮无效'); } // 步骤2: 查找并点击“编辑字幕”按钮 (带重试机制) const editSubsBtn = await findElementWithRetry(CONSTANTS.SUBTITLE.EDIT_SUBTITLES_SELECTOR, 3, 800); if (editSubsBtn) { editSubsBtn.click(); } else { throw new Error('步骤2失败: 未能在多次尝试后找到“编辑字幕”按钮'); } // 步骤3: 查找并点击“搜索”按钮 (带重试机制) const searchBtn = await findElementWithRetry(CONSTANTS.SUBTITLE.SEARCH_BUTTON_SELECTOR, 3, 800); if (searchBtn) { searchBtn.click(); } else { throw new Error('步骤3失败: 未能在多次尝试后找到“搜索”按钮'); } console.log('[Jellyfin元数据辅助] “一键搜索字幕”任务完成!'); } catch (error) { console.error(`[Jellyfin元数据辅助] ${error.message}`); document.body.click(); // 如果出错,点击页面主体以关闭可能打开的菜单 } } /** * 在详情页的“更多”按钮旁添加字幕搜索按钮 * @param {HTMLElement} moreButton - 详情页的“更多”按钮 */ function addSubtitleButtonToDetailPage(moreButton) { const newButton = document.createElement('button'); newButton.setAttribute('is', 'emby-button'); newButton.setAttribute('type', 'button'); newButton.className = 'button-flat detailButton emby-button custom-detail-page-subs-btn'; newButton.title = '一键搜索字幕'; newButton.innerHTML = `<div class="detailButton-content"><span class="material-icons">subtitles</span></div>`; newButton.onclick = (e) => { e.stopPropagation(); performSubtitleSearch(moreButton); }; moreButton.parentNode.insertBefore(newButton, moreButton.nextSibling); moreButton.setAttribute(CONSTANTS.SUBTITLE.MARKER_ATTRIBUTE, 'true'); } /** * 在媒体卡片的“更多”按钮旁添加字幕搜索按钮 * @param {HTMLElement} moreButton - 媒体卡片上的“更多”按钮 */ function addSubtitleButtonToCard(moreButton) { const newButton = document.createElement('button'); newButton.className = moreButton.className; newButton.setAttribute('is', moreButton.getAttribute('is')); newButton.title = '一键搜索字幕'; newButton.innerHTML = `<span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover" aria-hidden="true">subtitles</span>`; newButton.onclick = (e) => { e.stopPropagation(); performSubtitleSearch(moreButton); }; moreButton.parentNode.insertBefore(newButton, moreButton.nextSibling); moreButton.setAttribute(CONSTANTS.SUBTITLE.MARKER_ATTRIBUTE, 'true'); } // --- 功能2: 刷新元数据 --- function addMetadataRefreshButton(playStateButton) { const itemId = playStateButton.getAttribute('data-id'); if (!itemId) return; const refreshButton = document.createElement('button'); refreshButton.type = 'button'; refreshButton.className = playStateButton.className + ' btn-item-refresh'; refreshButton.title = '刷新元数据'; refreshButton.innerHTML = `<span class="material-icons" aria-hidden="true">${CONSTANTS.METADATA.ICON_NAME}</span>`; refreshButton.onclick = (e) => { e.stopPropagation(); if (MANUAL_API_KEY === '在此处粘贴你的API密钥' || MANUAL_API_KEY.length < 10) { alert('错误:请先在脚本顶部的配置中心填写正确的 API Key!'); return; } refreshButton.classList.add('is-loading'); refreshButton.disabled = true; const serverUrl = window.location.origin; const requestUrl = `${serverUrl}/Items/${itemId}/Refresh?Recursive=true&ImageRefreshMode=FullRefresh&MetadataRefreshMode=FullRefresh&ReplaceAllImages=false&RegenerateTrickplay=false&ReplaceAllMetadata=true`; GM_xmlhttpRequest({ method: "POST", url: requestUrl, headers: { "X-Emby-Token": MANUAL_API_KEY }, onload: (response) => { refreshButton.classList.remove('is-loading'); refreshButton.disabled = false; if (response.status >= 200 && response.status < 300) { refreshButton.innerHTML = '<span class="material-icons" aria-hidden="true" style="color: #4CAF50;">done</span>'; setTimeout(() => { refreshButton.innerHTML = `<span class="material-icons" aria-hidden="true">${CONSTANTS.METADATA.ICON_NAME}</span>`; }, 2000); } else { alert(`刷新 Item ID ${itemId} 失败!\n状态码: ${response.status}\n可能是API Key不正确或无权限。`); } }, onerror: () => { refreshButton.classList.remove('is-loading'); refreshButton.disabled = false; alert('发送刷新请求时发生网络错误。'); } }); }; playStateButton.before(refreshButton); playStateButton.setAttribute(CONSTANTS.METADATA.MARKER_ATTRIBUTE, 'true'); } // --- 总调度函数 --- function masterScanAndApply() { if (ENABLE_SUBTITLE_SEARCH) { const detailPageQuery = `${CONSTANTS.SUBTITLE.DETAIL_PAGE_MORE_SELECTOR}:not([${CONSTANTS.SUBTITLE.MARKER_ATTRIBUTE}="true"])`; document.querySelectorAll(detailPageQuery).forEach(addSubtitleButtonToDetailPage); const cardQuery = `${CONSTANTS.SUBTITLE.CARD_MORE_SELECTOR}:not([${CONSTANTS.SUBTITLE.MARKER_ATTRIBUTE}="true"])`; document.querySelectorAll(cardQuery).forEach(addSubtitleButtonToCard); } if (ENABLE_METADATA_REFRESH) { const query = `${CONSTANTS.METADATA.PLAYSTATE_BUTTON_SELECTOR}:not([${CONSTANTS.METADATA.MARKER_ATTRIBUTE}="true"])`; document.querySelectorAll(query).forEach(addMetadataRefreshButton); } } // ========================================================================================= // ——— 启动 MutationObserver ——— // ========================================================================================= const observer = new MutationObserver(masterScanAndApply); observer.observe(document.body, { childList: true, subtree: true }); masterScanAndApply(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址