From b8967b7bb8a31a1b70970dc243f567d3aa931e28 Mon Sep 17 00:00:00 2001 From: Fovty Date: Wed, 5 Nov 2025 17:05:59 +0000 Subject: [PATCH 1/3] fix: Make hover listener attachment more robust - Change from Set tracking movie IDs to WeakSet tracking DOM elements - Fix issue where duplicate movie IDs across different views prevented listener attachment - Simplify implementation by removing complex verification/retry logic - Improve MutationObserver to only trigger when movie cards are added - Each card element now gets listeners regardless of duplicate movie IDs - WeakSet provides automatic garbage collection for removed elements - Clean up console logging for better diagnostics --- .../Api/HoverTrailerController.cs | 80 +++++++++++++------ 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs b/Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs index 7bd20fd..dc67319 100644 --- a/Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs +++ b/Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs @@ -435,7 +435,7 @@ private static string GetHoverTrailerScript(Configuration.PluginConfiguration co let currentCardElement; let isPlaying = false; let resizeHandler; - let attachedCards = new Set(); // Track cards that already have listeners + let attachedCards = new WeakSet(); // Track actual card elements that already have listeners let mutationDebounce = null; function log(message, ...args) {{ @@ -499,7 +499,7 @@ function extractYouTubeVideoId(url) {{ /youtube\.com\/embed\/([^&\?]+)/, /youtube\.com\/v\/([^&\?]+)/ ]; - + for (const pattern of patterns) {{ const match = url.match(pattern); if (match && match[1]) {{ @@ -507,7 +507,7 @@ function extractYouTubeVideoId(url) {{ return match[1]; }} }} - + log('Failed to extract video ID from URL:', url); return null; }} @@ -529,7 +529,7 @@ function createYouTubePreview(embedUrl, cardElement) {{ // Apply the same logic as local videos const youtubeAspectRatio = 16 / 9; // YouTube standard aspect ratio const cardAspectRatio = cardRect.width / cardRect.height; - + if (youtubeAspectRatio > cardAspectRatio) {{ // Video is wider than card, fit to width containerWidth = cardRect.width; @@ -539,11 +539,11 @@ function createYouTubePreview(embedUrl, cardElement) {{ containerHeight = cardRect.height; containerWidth = Math.round(cardRect.height * youtubeAspectRatio); }} - + // Apply percentage scaling containerWidth = Math.round(containerWidth * (PREVIEW_SIZE_PERCENTAGE / 100)); containerHeight = Math.round(containerHeight * (PREVIEW_SIZE_PERCENTAGE / 100)); - + log(`YouTube FitContent dimensions: ${{containerWidth}}x${{containerHeight}} (${{PREVIEW_SIZE_PERCENTAGE}}% of calculated fit)`); }} else {{ // Manual mode uses configured width/height @@ -619,7 +619,7 @@ function createYouTubePreview(embedUrl, cardElement) {{ setTimeout(() => {{ try {{ const volumePercent = ENABLE_PREVIEW_AUDIO ? PREVIEW_VOLUME : 0; - + // Set playback quality using IFrame API (2025 method) if (REMOTE_VIDEO_QUALITY !== 'adaptive') {{ iframe.contentWindow.postMessage(JSON.stringify({{ @@ -629,7 +629,7 @@ function createYouTubePreview(embedUrl, cardElement) {{ }}), '*'); log('YouTube quality set to: ' + REMOTE_VIDEO_QUALITY); }} - + if (volumePercent === 0) {{ // Keep muted if volume is 0 or audio is disabled log('YouTube iframe kept muted (volume=0 or audio disabled)'); @@ -722,7 +722,7 @@ function createVideoPreview(trailerPath, cardElement) {{ video.muted = !ENABLE_PREVIEW_AUDIO; video.loop = true; video.preload = 'metadata'; - + // Set volume based on configuration (0-100 range converted to 0.0-1.0) if (ENABLE_PREVIEW_AUDIO) {{ video.volume = PREVIEW_VOLUME / 100.0; @@ -770,14 +770,14 @@ function showPreview(element, movieId) {{ // For remote YouTube trailers, convert to embed URL const youtubeUrl = trailerInfo.Path; log('Original YouTube URL:', youtubeUrl); - + // Extract video ID from YouTube URL const videoId = extractYouTubeVideoId(youtubeUrl); if (videoId) {{ // Use youtube-nocookie.com to avoid Error 153 and privacy issues // Enable JS API for volume control and quality setting // ALWAYS start muted to prevent loud initial audio, then unmute via API - + videoSource = `https://www.youtube-nocookie.com/embed/${{videoId}}?` + `autoplay=1` + `&mute=1` + // Always start muted to prevent loud audio spike @@ -951,7 +951,7 @@ function hidePreview() {{ videoToStop.src = ''; videoToStop.load(); }} - + if (iframeToStop) {{ log('Stopping iframe (YouTube)'); iframeToStop.src = 'about:blank'; @@ -977,21 +977,26 @@ function hidePreview() {{ previewToRemove.parentNode.removeChild(previewToRemove); }} }}, 300); + }} }} function attachHoverListeners() {{ const movieCards = document.querySelectorAll('[data-type=""Movie""], .card[data-itemtype=""Movie""]'); - let newCardsCount = 0; + movieCards.forEach(card => {{ + // Skip if this card element already has listeners attached + if (attachedCards.has(card)) return; + const movieId = card.getAttribute('data-id') || card.getAttribute('data-itemid'); - if (!movieId) return; - - // Skip if this card already has listeners attached - if (attachedCards.has(movieId)) return; + if (!movieId) {{ + log('Warning: Found movie card without ID'); + return; + }} - attachedCards.add(movieId); + // Mark this card element as having listeners + attachedCards.add(card); newCardsCount++; card.addEventListener('mouseenter', (e) => {{ @@ -1016,7 +1021,7 @@ function attachHoverListeners() {{ }}); if (newCardsCount > 0) {{ - log('Attached hover listeners to', newCardsCount, 'new movie cards'); + console.log(`[HoverTrailer] Attached hover listeners to ${{newCardsCount}} new movie cards`); }} }} @@ -1028,12 +1033,37 @@ function attachHoverListeners() {{ }} // Re-attach listeners when navigation occurs (debounced) - const observer = new MutationObserver(() => {{ - // Debounce to prevent excessive re-attachment - clearTimeout(mutationDebounce); - mutationDebounce = setTimeout(() => {{ - attachHoverListeners(); - }}, 500); // Wait 500ms after last DOM change + const observer = new MutationObserver((mutations) => {{ + // Check if any mutations added movie cards + let hasMovieCardChanges = false; + for (const mutation of mutations) {{ + if (mutation.addedNodes.length > 0) {{ + for (const node of mutation.addedNodes) {{ + if (node.nodeType === 1) {{ // Element node + // Check if it's a movie card or contains movie cards + if (node.matches && (node.matches('[data-type=""Movie""]') || node.matches('.card[data-itemtype=""Movie""]'))) {{ + hasMovieCardChanges = true; + break; + }} + if (node.querySelector && node.querySelector('[data-type=""Movie""], .card[data-itemtype=""Movie""]')) {{ + hasMovieCardChanges = true; + break; + }} + }} + }} + }} + if (hasMovieCardChanges) break; + }} + + // Only process if movie cards were added + if (hasMovieCardChanges) {{ + // Debounce to prevent excessive re-attachment + clearTimeout(mutationDebounce); + mutationDebounce = setTimeout(() => {{ + log('DOM mutation detected, re-attaching listeners...'); + attachHoverListeners(); + }}, 500); + }} }}); observer.observe(document.body, {{ From f04877e714a3a02768e02db0511dff113a8f4ea6 Mon Sep 17 00:00:00 2001 From: Fovty Date: Wed, 5 Nov 2025 17:08:07 +0000 Subject: [PATCH 2/3] refactor: Remove outdated customization settings and performance notes from README --- README.md | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/README.md b/README.md index ef250f7..72df7f0 100644 --- a/README.md +++ b/README.md @@ -67,26 +67,6 @@ https://raw.githubusercontent.com/Fovty/HoverTrailer/master/manifest.json Access plugin settings through: **Jellyfin Admin Dashboard** → **Plugins** → **HoverTrailer** → **Settings** -### Hover Preview Customization - -| Setting | Description | Default | Range | -|---------|-------------|---------|-------| -| **Horizontal Offset** | X-axis position adjustment | 0px | -2000 to +2000px | -| **Vertical Offset** | Y-axis position adjustment | 0px | -2000 to +2000px | -| **Sizing Mode** | How to size the preview | Fit to Video Content | Fixed/Fit to Content | -| **Width** | Manual width (Fixed mode) | 400px | 100-2000px | -| **Height** | Manual height (Fixed mode) | 225px | 100-2000px | -| **Content Scale** | Percentage scaling (Fit mode) | 200% | 50-1500% | -| **Preview Opacity** | Transparency level | 1.0 | 0.1-1.0 | -| **Border Radius** | Corner rounding | 10px | 0-50px | - -### Audio & Debug Settings - -| Setting | Description | Default | -|---------|-------------|---------| -| **Enable Preview Audio** | Play audio with trailers | ✅ Enabled | -| **Enable Debug Mode** | Detailed console logging | ✅ Enabled | - ## Troubleshooting ### Trailers Not Playing @@ -152,18 +132,6 @@ volumes: - /path/to/jellyfin/config/index.html:/jellyfin/jellyfin-web/index.html ``` -**How It Works:** -HoverTrailer uses reflection-based integration to detect File Transformation at runtime. If the plugin is available, HoverTrailer registers a transformation handler that injects the script tag into index.html as it's served to browsers. If File Transformation isn't available, HoverTrailer falls back to direct file modification (which may require permissions as described in Options 2-3). - -### Performance Issues -1. **Adjust sizing settings**: - - Lower "Content Scale" percentage - - Use "Fixed" sizing mode for consistent performance - -2. **Network considerations**: - - Remote trailers require internet connectivity - - Local trailers provide better performance - ## Development ### Building from Source @@ -176,16 +144,6 @@ cd hovertrailer dotnet build --configuration Release --property:TreatWarningsAsErrors=false ``` -### Project Structure -``` -Fovty.Plugin.HoverTrailer/ -├── Api/ # API controllers -├── Configuration/ # Plugin configuration -├── Exceptions/ # Custom exceptions -├── Helpers/ # Utility classes -└── Models/ # Data models -``` - ## Contributing 1. Fork the repository From 7178c33492f2e48efce569b494aa56b6eb179378 Mon Sep 17 00:00:00 2001 From: Fovty Date: Wed, 5 Nov 2025 17:10:21 +0000 Subject: [PATCH 3/3] chore: Update version to 0.2.1.1 in Directory.Build.props --- Directory.Build.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d8bdf01..9bd64a2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 0.2.1.0 - 0.2.1.0 - 0.2.1.0 + 0.2.1.1 + 0.2.1.1 + 0.2.1.1