您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
【二合一】功能1: 在详情页添加“一键搜索字幕”按钮。功能2: 在各处“标为已播放”旁添加“刷新元数据”按钮。功能可独立开关。
当前为
// ==UserScript== // @name Jellyfin元数据辅助 // @namespace http://tampermonkey.net/ // @version 3.0 // @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-search-subs-btn { margin-left: 0.25em; min-width: 0; padding: 0.5em; } .custom-search-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); } } `); // ========================================================================================= // ——— 核心逻辑 (合并) ——— // ========================================================================================= // --- 功能1: 一键搜索字幕 --- const SUBTITLE_SEARCH_CONSTANTS = { MORE_BUTTON_SELECTOR: 'button[is="emby-button"][title="更多"]', EDIT_SUBTITLES_SELECTOR: 'button[data-id="editsubtitles"]', SEARCH_BUTTON_SELECTOR: 'button.btnSearchSubtitles', MARKER_ATTRIBUTE: 'data-search-button-added' }; function simulateUserClick(element) { if (!element || typeof element.dispatchEvent !== 'function') return false; const eventOptions = { bubbles: true, cancelable: true, view: document.defaultView }; ['mousedown', 'mouseup', 'click'].forEach(type => element.dispatchEvent(new MouseEvent(type, eventOptions))); return true; } function addSubtitleSearchButton(moreButton) { const newButton = document.createElement('button'); newButton.setAttribute('is', 'emby-button'); newButton.setAttribute('type', 'button'); newButton.className = 'button-flat detailButton emby-button custom-search-subs-btn'; newButton.title = '一键搜索字幕'; newButton.innerHTML = `<div class="detailButton-content"><span class="material-icons">subtitles</span></div>`; newButton.onclick = async () => { console.log(`[工具箱] 开始执行“一键搜索字幕”...`); const delay = ms => new Promise(res => setTimeout(res, ms)); try { if (!simulateUserClick(document.querySelector(SUBTITLE_SEARCH_CONSTANTS.MORE_BUTTON_SELECTOR))) throw new Error(`步骤1失败`); await delay(800); if (!simulateUserClick(document.querySelector(SUBTITLE_SEARCH_CONSTANTS.EDIT_SUBTITLES_SELECTOR))) throw new Error(`步骤2失败`); await delay(800); if (!simulateUserClick(document.querySelector(SUBTITLE_SEARCH_CONSTANTS.SEARCH_BUTTON_SELECTOR))) throw new Error(`步骤3失败`); console.log('[工具箱] “一键搜索字幕”任务完成!'); } catch (error) { console.error(error.message); document.body.click(); } }; moreButton.parentNode.insertBefore(newButton, moreButton.nextSibling); moreButton.setAttribute(SUBTITLE_SEARCH_CONSTANTS.MARKER_ATTRIBUTE, 'true'); } // --- 功能2: 刷新元数据 --- const METADATA_REFRESH_CONSTANTS = { PLAYSTATE_BUTTON_SELECTOR: 'button[is="emby-playstatebutton"]', MARKER_ATTRIBUTE: 'data-refresh-button-added', ICON_NAME: 'sync' }; 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">${METADATA_REFRESH_CONSTANTS.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">${METADATA_REFRESH_CONSTANTS.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(METADATA_REFRESH_CONSTANTS.MARKER_ATTRIBUTE, 'true'); } // --- 总调度函数 --- function masterScanAndApply() { // 如果开启了“一键搜索字幕”功能,则执行扫描和添加 if (ENABLE_SUBTITLE_SEARCH) { const query = `${SUBTITLE_SEARCH_CONSTANTS.MORE_BUTTON_SELECTOR}:not([${SUBTITLE_SEARCH_CONSTANTS.MARKER_ATTRIBUTE}="true"])`; document.querySelectorAll(query).forEach(addSubtitleSearchButton); } // 如果开启了“刷新元数据”功能,则执行扫描和添加 if (ENABLE_METADATA_REFRESH) { const query = `${METADATA_REFRESH_CONSTANTS.PLAYSTATE_BUTTON_SELECTOR}:not([${METADATA_REFRESH_CONSTANTS.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或关注我们的公众号极客氢云获取最新地址