Emby Functions Enhanced

Add buttons on top of target element to generate thumbs and open path with enhanced error handling and performance

  1. // ==UserScript==
  2. // @name Emby Functions Enhanced
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2
  5. // @description Add buttons on top of target element to generate thumbs and open path with enhanced error handling and performance
  6. // @author Wayne
  7. // @match http://192.168.0.47:10074/*
  8. // @grant GM.xmlHttpRequest
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. "use strict";
  14.  
  15. // Configuration
  16. const CONFIG = {
  17. EMBY_LOCAL_ENDPOINT: "http://192.168.0.47:10162/generate_thumb",
  18. // DOPUS_LOCAL_ENDPOINT: "http://localhost:10074/open?path=",
  19. DOPUS_LOCAL_ENDPOINT: "http://127.0.0.1:58000",
  20. TOAST_DURATION: 5000,
  21. REQUEST_TIMEOUT: 30000,
  22. RETRY_ATTEMPTS: 3,
  23. RETRY_DELAY: 1000
  24. };
  25.  
  26. const SELECTORS = {
  27. 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",
  28. MEDIA_SOURCES: ".mediaSources"
  29. };
  30.  
  31. // State management
  32. const state = {
  33. buttonsInserted: false,
  34. saveButtonAdded: false,
  35. currentPath: null,
  36. pendingRequests: new Set(),
  37. lastUrl: location.href
  38. };
  39.  
  40. // Utility functions
  41. const debounce = (func, wait) => {
  42. let timeout;
  43. return (...args) => {
  44. clearTimeout(timeout);
  45. timeout = setTimeout(() => func(...args), wait);
  46. };
  47. };
  48.  
  49. const throttle = (func, limit) => {
  50. let inThrottle;
  51. return function(...args) {
  52. if (!inThrottle) {
  53. func.apply(this, args);
  54. inThrottle = true;
  55. setTimeout(() => { inThrottle = false; }, limit);
  56. }
  57. };
  58. };
  59.  
  60. const sanitizePath = (path) => path?.trim().replace(/[<>:"|?*]/g, '_') || '';
  61. const validatePath = (path) => path && typeof path === 'string' && path.trim().length > 0;
  62.  
  63. // Reset state when URL or content changes
  64. function resetState() {
  65. state.buttonsInserted = false;
  66. state.saveButtonAdded = false;
  67. state.currentPath = null;
  68. console.log("State reset - checking for elements...");
  69. }
  70.  
  71. // Check for URL changes (SPA navigation)
  72. function checkUrlChange() {
  73. if (location.href !== state.lastUrl) {
  74. console.log("URL changed:", state.lastUrl, "->", location.href);
  75. state.lastUrl = location.href;
  76. resetState();
  77. // Small delay to let new content load
  78. setTimeout(() => {
  79. //addSaveButtonIfReady();
  80. insertButtons();
  81. }, 100);
  82. }
  83. }
  84.  
  85. // Enhanced toast system
  86. function showToast(message, type = 'info', duration = CONFIG.TOAST_DURATION) {
  87. const typeStyles = {
  88. info: { background: '#333', color: '#fff' },
  89. success: { background: '#4CAF50', color: '#fff' },
  90. error: { background: '#f44336', color: '#fff' },
  91. warning: { background: '#ff9800', color: '#fff' }
  92. };
  93.  
  94. let container = document.getElementById("userscript-toast-container");
  95. if (!container) {
  96. container = document.createElement("div");
  97. container.id = "userscript-toast-container";
  98. Object.assign(container.style, {
  99. position: "fixed",
  100. top: "20px",
  101. right: "20px",
  102. display: "flex",
  103. flexDirection: "column",
  104. gap: "10px",
  105. zIndex: "10000",
  106. pointerEvents: "none"
  107. });
  108. document.body.appendChild(container);
  109. }
  110.  
  111. const toast = document.createElement("div");
  112. toast.textContent = message;
  113. Object.assign(toast.style, {
  114. ...typeStyles[type],
  115. padding: "12px 16px",
  116. borderRadius: "8px",
  117. boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
  118. fontSize: "14px",
  119. fontFamily: "Arial, sans-serif",
  120. maxWidth: "300px",
  121. wordWrap: "break-word",
  122. opacity: "0",
  123. transform: "translateX(100%)",
  124. transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
  125. pointerEvents: "auto"
  126. });
  127.  
  128. container.appendChild(toast);
  129.  
  130. // Animate in
  131. requestAnimationFrame(() => {
  132. toast.style.opacity = "1";
  133. toast.style.transform = "translateX(0)";
  134. });
  135.  
  136. // Auto-remove
  137. setTimeout(() => {
  138. toast.style.opacity = "0";
  139. toast.style.transform = "translateX(100%)";
  140. setTimeout(() => {
  141. if (toast.parentNode) {
  142. toast.remove();
  143. }
  144. }, 300);
  145. }, duration);
  146.  
  147. return toast;
  148. }
  149.  
  150. // Enhanced HTTP request with retry logic
  151. async function makeRequest(url, options = {}) {
  152. const requestId = Date.now() + Math.random();
  153. state.pendingRequests.add(requestId);
  154.  
  155. try {
  156. const controller = new AbortController();
  157. const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT);
  158.  
  159. const response = await fetch(url, {
  160. ...options,
  161. signal: controller.signal
  162. });
  163.  
  164. clearTimeout(timeoutId);
  165.  
  166. if (!response.ok) {
  167. throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  168. }
  169.  
  170. return response;
  171. } catch (error) {
  172. if (error.name === 'AbortError') {
  173. throw new Error('Request timed out');
  174. }
  175. throw error;
  176. } finally {
  177. state.pendingRequests.delete(requestId);
  178. }
  179. }
  180.  
  181. async function makeRequestWithRetry(url, options = {}, maxRetries = CONFIG.RETRY_ATTEMPTS) {
  182. for (let attempt = 0; attempt <= maxRetries; attempt++) {
  183. try {
  184. return await makeRequest(url, options);
  185. } catch (error) {
  186. if (attempt === maxRetries) {
  187. throw error;
  188. }
  189.  
  190. console.warn(`Request attempt ${attempt + 1} failed:`, error.message);
  191. await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY * (attempt + 1)));
  192. }
  193. }
  194. }
  195.  
  196. // Path element finder with fallback
  197. function findPathElement() {
  198. const mediaSource = document.querySelector(SELECTORS.MEDIA_SOURCES);
  199. if (!mediaSource) return null;
  200.  
  201. // Try multiple selectors as fallback
  202. const selectors = [
  203. "div:nth-child(2) > div > div:first-child",
  204. "div:first-child > div > div:first-child",
  205. "div div div:first-child"
  206. ];
  207.  
  208. for (const selector of selectors) {
  209. const element = mediaSource.querySelector(selector);
  210. if (element && element.textContent?.trim()) {
  211. return element;
  212. }
  213. }
  214.  
  215. return null;
  216. }
  217.  
  218. // Thumbnail generation functions
  219. function createThumbnailHandler(mode, description) {
  220. return async (path) => {
  221. const sanitizedPath = sanitizePath(path);
  222. if (!validatePath(sanitizedPath)) {
  223. showToast("Invalid path provided", "error");
  224. return;
  225. }
  226.  
  227. const loadingToast = showToast(`⌛ ${description} for ${sanitizedPath}...`, "info");
  228.  
  229. try {
  230. const encodedPath = encodeURIComponent(sanitizedPath);
  231. const url = `${CONFIG.EMBY_LOCAL_ENDPOINT}?path=${encodedPath}&mode=${mode}`;
  232.  
  233. console.log(`Generating ${mode} thumb:`, sanitizedPath);
  234.  
  235. await makeRequestWithRetry(url);
  236.  
  237. loadingToast.remove();
  238. showToast(`✅ ${description} completed successfully`, "success");
  239. console.log(`${mode} thumb generated successfully`);
  240.  
  241. } catch (error) {
  242. loadingToast.remove();
  243. const errorMsg = `Failed to generate ${mode} thumbnail: ${error.message}`;
  244. console.error(errorMsg, error);
  245. showToast(errorMsg, "error");
  246. }
  247. };
  248. }
  249.  
  250. function sendDataToLocalServer(data, path) {
  251. let url = `http://127.0.0.1:58000/${path}/`
  252. GM.xmlHttpRequest({
  253. method: "POST",
  254. url: url,
  255. data: JSON.stringify(data),
  256. headers: {
  257. "Content-Type": "application/json"
  258. }
  259. });
  260. }
  261.  
  262. // Path opening function
  263. async function openPath(path) {
  264. // const sanitizedPath = sanitizePath(path);
  265. // if (!validatePath(sanitizedPath)) {
  266. // showToast("Invalid path provided", "error");
  267. // return;
  268. // }
  269.  
  270. try {
  271. // const encodedPath = encodeURIComponent(sanitizedPath);
  272. const data = {
  273. full_path: path
  274. };
  275.  
  276. sendDataToLocalServer(data, "openFolder")
  277.  
  278. // await makeRequestWithRetry(url);
  279.  
  280.  
  281. showToast("📁 Path opened in Directory Opus", "success");
  282. console.log("Opened in Directory Opus");
  283.  
  284. } catch (error) {
  285. const errorMsg = `Failed to open path: ${error.message}`;
  286. console.error(errorMsg, error);
  287. showToast(errorMsg, "error");
  288. }
  289. }
  290.  
  291. // Button factory
  292. function createButton(label, onClick, color = "#2196F3") {
  293. const btn = document.createElement("button");
  294. btn.textContent = label;
  295.  
  296. Object.assign(btn.style, {
  297. marginRight: "8px",
  298. marginBottom: "4px",
  299. padding: "8px 12px",
  300. borderRadius: "6px",
  301. backgroundColor: color,
  302. color: "white",
  303. border: "none",
  304. cursor: "pointer",
  305. fontSize: "13px",
  306. fontWeight: "500",
  307. transition: "all 0.2s ease",
  308. boxShadow: "0 2px 4px rgba(0,0,0,0.2)"
  309. });
  310.  
  311. // Hover effects
  312. btn.addEventListener("mouseenter", () => {
  313. btn.style.transform = "translateY(-1px)";
  314. btn.style.boxShadow = "0 4px 8px rgba(0,0,0,0.3)";
  315. });
  316.  
  317. btn.addEventListener("mouseleave", () => {
  318. btn.style.transform = "translateY(0)";
  319. btn.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
  320. });
  321.  
  322. btn.addEventListener("click", onClick);
  323. return btn;
  324. }
  325.  
  326. // Main button insertion logic
  327.  
  328. function insertButtons() {
  329. const target = findPathElement();
  330. if (!target) return;
  331. const pathText = target.textContent.trim();
  332. if (!validatePath(pathText)) return;
  333.  
  334. // Check if buttons already exist for this path
  335. const existingContainer = target.parentElement.querySelector('.userscript-button-container');
  336. if (existingContainer && state.currentPath === pathText) return;
  337.  
  338. // Remove existing buttons if path changed
  339. if (existingContainer) {
  340. existingContainer.remove();
  341. }
  342.  
  343. state.currentPath = pathText;
  344. state.buttonsInserted = true;
  345.  
  346. // Create thumbnail handlers
  347. const singleThumbHandler = createThumbnailHandler("single", "Generating single thumbnail");
  348. const fullThumbHandler = createThumbnailHandler("full", "Generating full thumbnail");
  349. const skipThumbHandler = createThumbnailHandler("skip", "Generating thumbnail (skip existing)");
  350.  
  351. // Insert buttons using insertAdjacentHTML
  352. target.insertAdjacentHTML('beforeBegin', `
  353. <div class="userscript-button-container" style="margin-bottom: 12px; display: flex; flex-wrap: wrap; gap: 4px;">
  354. <button id="openPathBtn" style="background-color: #FF9800;">📁 Open Path</button>
  355. <button id="singleThumbBtn" style="background-color: #4CAF50;">🖼️ Single Thumb</button>
  356. <button id="fullThumbBtn" style="background-color: #2196F3;">🎬 Full Thumb</button>
  357. <button id="skipExistingBtn" style="background-color: #9C27B0;">⏭️ Skip Existing</button>
  358. </div>
  359. `);
  360.  
  361. // Add event listeners to the newly created buttons
  362. const container = target.previousElementSibling;
  363. const openPathBtn = container.querySelector('#openPathBtn');
  364. const singleThumbBtn = container.querySelector('#singleThumbBtn');
  365. const fullThumbBtn = container.querySelector('#fullThumbBtn');
  366. const skipExistingBtn = container.querySelector('#skipExistingBtn');
  367.  
  368. openPathBtn.addEventListener("click", () => openPath(pathText), false);
  369. singleThumbBtn.addEventListener("click", () => singleThumbHandler(pathText), false);
  370. fullThumbBtn.addEventListener("click", () => fullThumbHandler(pathText), false);
  371. skipExistingBtn.addEventListener("click", () => skipThumbHandler(pathText), false);
  372.  
  373. console.log("Buttons inserted for path:", pathText);
  374. }
  375.  
  376. // Cleanup function
  377. function cleanup() {
  378. // Cancel pending requests
  379. state.pendingRequests.clear();
  380.  
  381. // Remove toast container
  382. const toastContainer = document.getElementById("userscript-toast-container");
  383. if (toastContainer) {
  384. toastContainer.remove();
  385. }
  386. }
  387.  
  388. // Enhanced mutation observer with better performance
  389. // const debouncedAddSaveButton = debounce(addSaveButtonIfReady, 100);
  390. const debouncedInsertButtons = debounce(insertButtons, 200);
  391.  
  392. const observer = new MutationObserver((mutations) => {
  393. // Check for URL changes first
  394. checkUrlChange();
  395.  
  396. let shouldCheck = false;
  397.  
  398. for (const mutation of mutations) {
  399. if (mutation.type === 'childList') {
  400. for (const node of mutation.addedNodes) {
  401. if (node.nodeType === Node.ELEMENT_NODE && (
  402. node.matches?.(SELECTORS.VIDEO_OSD) ||
  403. node.matches?.(SELECTORS.MEDIA_SOURCES) ||
  404. node.querySelector?.(SELECTORS.VIDEO_OSD) ||
  405. node.querySelector?.(SELECTORS.MEDIA_SOURCES) ||
  406. node.classList?.contains('page') ||
  407. node.classList?.contains('view')
  408. )) {
  409. shouldCheck = true;
  410. break;
  411. }
  412. }
  413. }
  414. if (shouldCheck) break;
  415. }
  416.  
  417. if (shouldCheck) {
  418. // debouncedAddSaveButton();
  419. debouncedInsertButtons();
  420. }
  421. });
  422.  
  423. // Initialize
  424. function init() {
  425. console.log("Emby Functions Enhanced userscript initialized");
  426.  
  427. // Initial checks
  428. // addSaveButtonIfReady();
  429. insertButtons();
  430.  
  431. // Start observing with more comprehensive settings
  432. observer.observe(document.body, {
  433. childList: true,
  434. subtree: true,
  435. attributes: true,
  436. attributeFilter: ['class', 'style'],
  437. characterData: false
  438. });
  439. }
  440.  
  441. // Continuous checking for dynamic content
  442. setInterval(() => {
  443. checkUrlChange();
  444. // if (!state.saveButtonAdded) addSaveButtonIfReady();
  445. if (!document.querySelector('.userscript-button-container')) {
  446. resetState();
  447. insertButtons();
  448. }
  449. }, 2000);
  450.  
  451. // Handle page visibility changes
  452. document.addEventListener('visibilitychange', () => {
  453. if (document.visibilityState === 'visible') {
  454. resetState();
  455. setTimeout(init, 100);
  456. }
  457. });
  458.  
  459. // Cleanup on page unload
  460. window.addEventListener('beforeunload', cleanup);
  461.  
  462. // Initialize when DOM is ready
  463. if (document.readyState === 'loading') {
  464. document.addEventListener('DOMContentLoaded', init);
  465. } else {
  466. init();
  467. }
  468.  
  469. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址