Greasy Fork is available in English.

Emby Functions Enhanced

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

  })();