您需要先安装一个扩展,例如 篡改猴、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.2
- // @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 GM.xmlHttpRequest
- // @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=",
- DOPUS_LOCAL_ENDPOINT: "http://127.0.0.1:58000",
- 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)));
- }
- }
- }
- // 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");
- }
- };
- }
- function sendDataToLocalServer(data, path) {
- let url = `http://127.0.0.1:58000/${path}/`
- GM.xmlHttpRequest({
- method: "POST",
- url: url,
- data: JSON.stringify(data),
- headers: {
- "Content-Type": "application/json"
- }
- });
- }
- // 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 data = {
- full_path: path
- };
- sendDataToLocalServer(data, "openFolder")
- // await makeRequestWithRetry(url);
- showToast("📁 Path opened in Directory Opus", "success");
- console.log("Opened in Directory Opus");
- } 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;
- // 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)");
- // Insert buttons using insertAdjacentHTML
- target.insertAdjacentHTML('beforeBegin', `
- <div class="userscript-button-container" style="margin-bottom: 12px; display: flex; flex-wrap: wrap; gap: 4px;">
- <button id="openPathBtn" style="background-color: #FF9800;">📁 Open Path</button>
- <button id="singleThumbBtn" style="background-color: #4CAF50;">🖼️ Single Thumb</button>
- <button id="fullThumbBtn" style="background-color: #2196F3;">🎬 Full Thumb</button>
- <button id="skipExistingBtn" style="background-color: #9C27B0;">⏭️ Skip Existing</button>
- </div>
- `);
- // Add event listeners to the newly created buttons
- const container = target.previousElementSibling;
- const openPathBtn = container.querySelector('#openPathBtn');
- const singleThumbBtn = container.querySelector('#singleThumbBtn');
- const fullThumbBtn = container.querySelector('#fullThumbBtn');
- const skipExistingBtn = container.querySelector('#skipExistingBtn');
- openPathBtn.addEventListener("click", () => openPath(pathText), false);
- singleThumbBtn.addEventListener("click", () => singleThumbHandler(pathText), false);
- fullThumbBtn.addEventListener("click", () => fullThumbHandler(pathText), false);
- skipExistingBtn.addEventListener("click", () => skipThumbHandler(pathText), false);
- 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或关注我们的公众号极客氢云获取最新地址