您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add buttons on top of target element to generate thumbs and open path with enhanced error handling and performance
// ==UserScript== // @name Emby Functions Enhanced // @namespace http://tampermonkey.net/ // @version 2.0 // @description Add buttons on top of target element to generate thumbs and open path with enhanced error handling and performance // @author Wayne // @match http://192.168.0.47:10074/* // @grant none // @license MIT // ==/UserScript== (function () { "use strict"; // Configuration const CONFIG = { EMBY_LOCAL_ENDPOINT: "http://192.168.0.47:10162/generate_thumb", DOPUS_LOCAL_ENDPOINT: "http://localhost:10074/open?path=", TOAST_DURATION: 5000, REQUEST_TIMEOUT: 30000, RETRY_ATTEMPTS: 3, RETRY_DELAY: 1000 }; const SELECTORS = { VIDEO_OSD: "body > div.view.flex.flex-direction-column.page.focuscontainer-x.view-videoosd-videoosd.darkContentContainer.graphicContentContainer > div.videoOsdBottom.flex.videoOsd-nobuttonmargin.videoOsdBottom-video.videoOsdBottom-hidden.hide > div.videoOsdBottom-maincontrols > div.flex.flex-direction-row.align-items-center.justify-content-center.videoOsdPositionContainer.videoOsdPositionContainer-vertical.videoOsd-hideWithOpenTab.videoOsd-hideWhenLocked.focuscontainer-x > div.flex.align-items-center.videoOsdPositionText.flex-shrink-zero.secondaryText.videoOsd-customFont-x0", MEDIA_SOURCES: ".mediaSources" }; // State management const state = { buttonsInserted: false, saveButtonAdded: false, currentPath: null, pendingRequests: new Set(), lastUrl: location.href }; // Utility functions const debounce = (func, wait) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; }; const throttle = (func, limit) => { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; }; const sanitizePath = (path) => path?.trim().replace(/[<>:"|?*]/g, '_') || ''; const validatePath = (path) => path && typeof path === 'string' && path.trim().length > 0; // Reset state when URL or content changes function resetState() { state.buttonsInserted = false; state.saveButtonAdded = false; state.currentPath = null; console.log("State reset - checking for elements..."); } // Check for URL changes (SPA navigation) function checkUrlChange() { if (location.href !== state.lastUrl) { console.log("URL changed:", state.lastUrl, "->", location.href); state.lastUrl = location.href; resetState(); // Small delay to let new content load setTimeout(() => { addSaveButtonIfReady(); insertButtons(); }, 100); } } // Enhanced toast system function showToast(message, type = 'info', duration = CONFIG.TOAST_DURATION) { const typeStyles = { info: { background: '#333', color: '#fff' }, success: { background: '#4CAF50', color: '#fff' }, error: { background: '#f44336', color: '#fff' }, warning: { background: '#ff9800', color: '#fff' } }; let container = document.getElementById("userscript-toast-container"); if (!container) { container = document.createElement("div"); container.id = "userscript-toast-container"; Object.assign(container.style, { position: "fixed", top: "20px", right: "20px", display: "flex", flexDirection: "column", gap: "10px", zIndex: "10000", pointerEvents: "none" }); document.body.appendChild(container); } const toast = document.createElement("div"); toast.textContent = message; Object.assign(toast.style, { ...typeStyles[type], padding: "12px 16px", borderRadius: "8px", boxShadow: "0 4px 12px rgba(0,0,0,0.3)", fontSize: "14px", fontFamily: "Arial, sans-serif", maxWidth: "300px", wordWrap: "break-word", opacity: "0", transform: "translateX(100%)", transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", pointerEvents: "auto" }); container.appendChild(toast); // Animate in requestAnimationFrame(() => { toast.style.opacity = "1"; toast.style.transform = "translateX(0)"; }); // Auto-remove setTimeout(() => { toast.style.opacity = "0"; toast.style.transform = "translateX(100%)"; setTimeout(() => { if (toast.parentNode) { toast.remove(); } }, 300); }, duration); return toast; } // Enhanced HTTP request with retry logic async function makeRequest(url, options = {}) { const requestId = Date.now() + Math.random(); state.pendingRequests.add(requestId); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT); const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response; } catch (error) { if (error.name === 'AbortError') { throw new Error('Request timed out'); } throw error; } finally { state.pendingRequests.delete(requestId); } } async function makeRequestWithRetry(url, options = {}, maxRetries = CONFIG.RETRY_ATTEMPTS) { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await makeRequest(url, options); } catch (error) { if (attempt === maxRetries) { throw error; } console.warn(`Request attempt ${attempt + 1} failed:`, error.message); await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY * (attempt + 1))); } } } // Save text functionality // function addSaveButtonIfReady() { // const target = document.querySelector(SELECTORS.VIDEO_OSD); // if (!target || state.saveButtonAdded) return; // const existingBtn = document.querySelector("#saveTextButton"); // if (existingBtn) { // state.saveButtonAdded = true; // return; // } // // === Save Text Button === // const saveBtn = document.createElement("button"); // saveBtn.id = "saveTextButton"; // saveBtn.textContent = "💾 Save Text"; // Object.assign(saveBtn.style, { // backgroundColor: "#4CAF50", // color: "white", // border: "none", // padding: "6px 10px", // marginLeft: "10px", // borderRadius: "6px", // cursor: "pointer", // fontSize: "13px", // fontWeight: "500", // transition: "all 0.2s ease", // boxShadow: "0 2px 4px rgba(0,0,0,0.2)" // }); // saveBtn.addEventListener("mouseenter", () => { // saveBtn.style.backgroundColor = "#45a049"; // saveBtn.style.transform = "translateY(-1px)"; // }); // saveBtn.addEventListener("mouseleave", () => { // saveBtn.style.backgroundColor = "#4CAF50"; // saveBtn.style.transform = "translateY(0)"; // }); // saveBtn.addEventListener("click", () => { // try { // const text = target.textContent.trim(); // if (!text) { // showToast("No text found to save", "warning"); // return; // } // window.savedVideoText = text; // console.log("Saved text:", text); // showToast(`Text saved: ${text.substring(0, 50)}...`, "success"); // } catch (error) { // console.error("Error saving text:", error); // showToast("Failed to save text", "error"); // } // }); // // === Show Text Button === // const showBtn = document.createElement("button"); // showBtn.id = "showTextButton"; // showBtn.textContent = "👁 Show Text"; // Object.assign(showBtn.style, { // backgroundColor: "#2196F3", // color: "white", // border: "none", // padding: "6px 10px", // marginLeft: "6px", // borderRadius: "6px", // cursor: "pointer", // fontSize: "13px", // fontWeight: "500", // transition: "all 0.2s ease", // boxShadow: "0 2px 4px rgba(0,0,0,0.2)" // }); // showBtn.addEventListener("mouseenter", () => { // showBtn.style.backgroundColor = "#1E88E5"; // showBtn.style.transform = "translateY(-1px)"; // }); // showBtn.addEventListener("mouseleave", () => { // showBtn.style.backgroundColor = "#2196F3"; // showBtn.style.transform = "translateY(0)"; // }); // showBtn.addEventListener("click", () => { // const savedText = window.savedVideoText; // if (savedText) { // showToast(`Saved text: ${savedText.substring(0, 100)}...`, "info"); // } else { // showToast("No saved text found", "warning"); // } // }); // // Insert both buttons after the target // target.parentNode.insertBefore(saveBtn, target.nextSibling); // saveBtn.parentNode.insertBefore(showBtn, saveBtn.nextSibling); // state.saveButtonAdded = true; // } // Path element finder with fallback function findPathElement() { const mediaSource = document.querySelector(SELECTORS.MEDIA_SOURCES); if (!mediaSource) return null; // Try multiple selectors as fallback const selectors = [ "div:nth-child(2) > div > div:first-child", "div:first-child > div > div:first-child", "div div div:first-child" ]; for (const selector of selectors) { const element = mediaSource.querySelector(selector); if (element && element.textContent?.trim()) { return element; } } return null; } // Thumbnail generation functions function createThumbnailHandler(mode, description) { return async (path) => { const sanitizedPath = sanitizePath(path); if (!validatePath(sanitizedPath)) { showToast("Invalid path provided", "error"); return; } const loadingToast = showToast(`⌛ ${description} for ${sanitizedPath}...`, "info"); try { const encodedPath = encodeURIComponent(sanitizedPath); const url = `${CONFIG.EMBY_LOCAL_ENDPOINT}?path=${encodedPath}&mode=${mode}`; console.log(`Generating ${mode} thumb:`, sanitizedPath); await makeRequestWithRetry(url); loadingToast.remove(); showToast(`✅ ${description} completed successfully`, "success"); console.log(`${mode} thumb generated successfully`); } catch (error) { loadingToast.remove(); const errorMsg = `Failed to generate ${mode} thumbnail: ${error.message}`; console.error(errorMsg, error); showToast(errorMsg, "error"); } }; } // Path opening function async function openPath(path) { const sanitizedPath = sanitizePath(path); if (!validatePath(sanitizedPath)) { showToast("Invalid path provided", "error"); return; } try { const encodedPath = encodeURIComponent(sanitizedPath); const url = `${CONFIG.DOPUS_LOCAL_ENDPOINT}${encodedPath}`; await makeRequestWithRetry(url); showToast("📁 Path opened in Directory Opus", "success"); console.log("Opened in Directory Opus:", sanitizedPath); } catch (error) { const errorMsg = `Failed to open path: ${error.message}`; console.error(errorMsg, error); showToast(errorMsg, "error"); } } // Button factory function createButton(label, onClick, color = "#2196F3") { const btn = document.createElement("button"); btn.textContent = label; Object.assign(btn.style, { marginRight: "8px", marginBottom: "4px", padding: "8px 12px", borderRadius: "6px", backgroundColor: color, color: "white", border: "none", cursor: "pointer", fontSize: "13px", fontWeight: "500", transition: "all 0.2s ease", boxShadow: "0 2px 4px rgba(0,0,0,0.2)" }); // Hover effects btn.addEventListener("mouseenter", () => { btn.style.transform = "translateY(-1px)"; btn.style.boxShadow = "0 4px 8px rgba(0,0,0,0.3)"; }); btn.addEventListener("mouseleave", () => { btn.style.transform = "translateY(0)"; btn.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)"; }); btn.addEventListener("click", onClick); return btn; } // Main button insertion logic function insertButtons() { const target = findPathElement(); if (!target) return; const pathText = target.textContent.trim(); if (!validatePath(pathText)) return; // Check if buttons already exist for this path const existingContainer = target.parentElement.querySelector('.userscript-button-container'); if (existingContainer && state.currentPath === pathText) return; // Remove existing buttons if path changed if (existingContainer) { existingContainer.remove(); } state.currentPath = pathText; state.buttonsInserted = true; const container = document.createElement("div"); container.className = "userscript-button-container"; container.style.marginBottom = "12px"; container.style.display = "flex"; container.style.flexWrap = "wrap"; container.style.gap = "4px"; // Create thumbnail handlers const singleThumbHandler = createThumbnailHandler("single", "Generating single thumbnail"); const fullThumbHandler = createThumbnailHandler("full", "Generating full thumbnail"); const skipThumbHandler = createThumbnailHandler("skip", "Generating thumbnail (skip existing)"); // Create buttons const buttons = [ { label: "📁 Open Path", handler: () => openPath(pathText), color: "#FF9800" }, { label: "🖼️ Single Thumb", handler: () => singleThumbHandler(pathText), color: "#4CAF50" }, { label: "🎬 Full Thumb", handler: () => fullThumbHandler(pathText), color: "#2196F3" }, { label: "⏭️ Skip Existing", handler: () => skipThumbHandler(pathText), color: "#9C27B0" } ]; buttons.forEach(({ label, handler, color }) => { const btn = createButton(label, handler, color); container.appendChild(btn); }); target.parentElement.insertBefore(container, target); console.log("Buttons inserted for path:", pathText); } // Cleanup function function cleanup() { // Cancel pending requests state.pendingRequests.clear(); // Remove toast container const toastContainer = document.getElementById("userscript-toast-container"); if (toastContainer) { toastContainer.remove(); } } // Enhanced mutation observer with better performance // const debouncedAddSaveButton = debounce(addSaveButtonIfReady, 100); const debouncedInsertButtons = debounce(insertButtons, 200); const observer = new MutationObserver((mutations) => { // Check for URL changes first checkUrlChange(); let shouldCheck = false; for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && ( node.matches?.(SELECTORS.VIDEO_OSD) || node.matches?.(SELECTORS.MEDIA_SOURCES) || node.querySelector?.(SELECTORS.VIDEO_OSD) || node.querySelector?.(SELECTORS.MEDIA_SOURCES) || node.classList?.contains('page') || node.classList?.contains('view') )) { shouldCheck = true; break; } } } if (shouldCheck) break; } if (shouldCheck) { // debouncedAddSaveButton(); debouncedInsertButtons(); } }); // Initialize function init() { console.log("Emby Functions Enhanced userscript initialized"); // Initial checks // addSaveButtonIfReady(); insertButtons(); // Start observing with more comprehensive settings observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'], characterData: false }); } // Continuous checking for dynamic content setInterval(() => { checkUrlChange(); // if (!state.saveButtonAdded) addSaveButtonIfReady(); if (!document.querySelector('.userscript-button-container')) { resetState(); insertButtons(); } }, 2000); // Handle page visibility changes document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { resetState(); setTimeout(init, 100); } }); // Cleanup on page unload window.addEventListener('beforeunload', cleanup); // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址