From 441d9c828c8d2f7b099bb5f76c51b7b735f4ac69 Mon Sep 17 00:00:00 2001 From: Fovty Date: Mon, 19 Jan 2026 12:52:00 +0000 Subject: [PATCH] fix: Add Series hover trailer support (closes #12) - Update GetTrailerInfo API endpoint to use BaseItem instead of Movie - Use IHasTrailers interface for trailer detection (works with Movies and Series) - Update client script selectors to include Series cards - Update MutationObserver to detect Series card additions - Rename movieId parameter to itemId for consistency --- .../Api/HoverTrailerController.cs | 138 +++++++++--------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs b/Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs index b92c8b3..bff9073 100644 --- a/Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs +++ b/Fovty.Plugin.HoverTrailer/Api/HoverTrailerController.cs @@ -113,36 +113,36 @@ public ActionResult GetClientScript() } /// - /// Gets trailer information for a specific movie. + /// Gets trailer information for a specific item (Movie or Series). /// - /// The movie ID. + /// The item ID. /// The trailer information. - [HttpGet("TrailerInfo/{movieId}")] + [HttpGet("TrailerInfo/{itemId}")] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public ActionResult GetTrailerInfo([FromRoute] Guid movieId) + public ActionResult GetTrailerInfo([FromRoute] Guid itemId) { var requestId = GenerateRequestId(); try { - if (movieId == Guid.Empty) + if (itemId == Guid.Empty) { - LoggingHelper.LogWarning(_logger, "Invalid movie ID provided: {MovieId}", movieId); - var invalidError = new ErrorResponse("INVALID_ARGUMENT", "Movie ID cannot be empty") + LoggingHelper.LogWarning(_logger, "Invalid item ID provided: {ItemId}", itemId); + var invalidError = new ErrorResponse("INVALID_ARGUMENT", "Item ID cannot be empty") { RequestId = requestId }; return BadRequest(invalidError); } - var movie = _libraryManager.GetItemById(movieId) as Movie; - if (movie == null) + var item = _libraryManager.GetItemById(itemId); + if (item == null) { - LoggingHelper.LogDebug(_logger, "Movie not found with ID: {MovieId}", movieId); - var notFoundError = new ErrorResponse("MOVIE_NOT_FOUND", "Movie not found", $"No movie found with ID: {movieId}") + LoggingHelper.LogDebug(_logger, "Item not found with ID: {ItemId}", itemId); + var notFoundError = new ErrorResponse("ITEM_NOT_FOUND", "Item not found", $"No item found with ID: {itemId}") { RequestId = requestId }; @@ -150,9 +150,9 @@ public ActionResult GetTrailerInfo([FromRoute] Guid movieId) } // Multi-source trailer detection with priority: Local → Remote → Downloaded - LoggingHelper.LogDebug(_logger, "Starting multi-source trailer detection for movie: {MovieName} (ID: {MovieId})", movie.Name, movieId); - LoggingHelper.LogDebug(_logger, "Movie path: {MoviePath}", movie.Path ?? "null"); - LoggingHelper.LogDebug(_logger, "Movie directory: {MovieDirectory}", movie.Path != null ? System.IO.Path.GetDirectoryName(movie.Path) ?? "null" : "null"); + LoggingHelper.LogDebug(_logger, "Starting multi-source trailer detection for item: {ItemName} (ID: {ItemId})", item.Name, itemId); + LoggingHelper.LogDebug(_logger, "Item path: {ItemPath}", item.Path ?? "null"); + LoggingHelper.LogDebug(_logger, "Item directory: {ItemDirectory}", item.Path != null ? System.IO.Path.GetDirectoryName(item.Path) ?? "null" : "null"); TrailerInfo? trailerInfo = null; @@ -160,19 +160,19 @@ public ActionResult GetTrailerInfo([FromRoute] Guid movieId) LoggingHelper.LogDebug(_logger, "Step 1: Checking for local trailers..."); IEnumerable localTrailers; - if (movie is IHasTrailers hasTrailers) + if (item is IHasTrailers hasTrailers) { // Use LocalTrailers property which matches Jellyfin's native trailer selection localTrailers = hasTrailers.LocalTrailers; - LoggingHelper.LogDebug(_logger, "Using LocalTrailers property: Found {LocalTrailerCount} local trailers for movie: {MovieName}", - localTrailers.Count(), movie.Name); + LoggingHelper.LogDebug(_logger, "Using LocalTrailers property: Found {LocalTrailerCount} local trailers for item: {ItemName}", + localTrailers.Count(), item.Name); } else { - // Fallback to GetExtras if movie doesn't implement IHasTrailers - localTrailers = movie.GetExtras(new[] { ExtraType.Trailer }); - LoggingHelper.LogDebug(_logger, "Using GetExtras fallback: Found {LocalTrailerCount} local trailers for movie: {MovieName}", - localTrailers.Count(), movie.Name); + // Fallback to GetExtras if item doesn't implement IHasTrailers + localTrailers = item.GetExtras(new[] { ExtraType.Trailer }); + LoggingHelper.LogDebug(_logger, "Using GetExtras fallback: Found {LocalTrailerCount} local trailers for item: {ItemName}", + localTrailers.Count(), item.Name); } // Log detailed information about each local trailer found @@ -188,7 +188,7 @@ public ActionResult GetTrailerInfo([FromRoute] Guid movieId) if (localTrailer != null) { - LoggingHelper.LogDebug(_logger, "Found local trailer for movie: {MovieName} (ID: {MovieId})", movie.Name, movieId); + LoggingHelper.LogDebug(_logger, "Found local trailer for item: {ItemName} (ID: {ItemId})", item.Name, itemId); LoggingHelper.LogDebug(_logger, "Local trailer details - ID: {TrailerId}, Name: {TrailerName}, Path: {TrailerPath}", localTrailer.Id, localTrailer.Name, localTrailer.Path); @@ -204,23 +204,23 @@ public ActionResult GetTrailerInfo([FromRoute] Guid movieId) Source = "Local File" }; - LoggingHelper.LogDebug(_logger, "Successfully created local trailer info for movie: {MovieName} (ID: {MovieId})", - movie.Name, movieId); + LoggingHelper.LogDebug(_logger, "Successfully created local trailer info for item: {ItemName} (ID: {ItemId})", + item.Name, itemId); return Ok(trailerInfo); } // Step 2: Check for remote trailers if no local trailer found LoggingHelper.LogDebug(_logger, "Step 2: No local trailer found, checking for remote trailers..."); - if (movie.RemoteTrailers?.Any() == true) + if (item.RemoteTrailers?.Any() == true) { - var remoteTrailer = movie.RemoteTrailers.LastOrDefault(); - LoggingHelper.LogDebug(_logger, "Found remote trailer for movie: {MovieName} (ID: {MovieId})", movie.Name, movieId); + var remoteTrailer = item.RemoteTrailers.LastOrDefault(); + LoggingHelper.LogDebug(_logger, "Found remote trailer for item: {ItemName} (ID: {ItemId})", item.Name, itemId); trailerInfo = new TrailerInfo { - Id = movieId, // Use movie ID since remote trailers don't have their own ID - Name = remoteTrailer.Name ?? $"{movie.Name} - Trailer", + Id = itemId, // Use item ID since remote trailers don't have their own ID + Name = remoteTrailer.Name ?? $"{item.Name} - Trailer", Path = remoteTrailer.Url, RunTimeTicks = null, // Remote trailers typically don't have runtime info HasSubtitles = false, // Remote trailers typically don't have subtitle info @@ -229,21 +229,21 @@ public ActionResult GetTrailerInfo([FromRoute] Guid movieId) Source = GetTrailerSource(remoteTrailer.Url) }; - LoggingHelper.LogDebug(_logger, "Successfully created remote trailer info for movie: {MovieName} (ID: {MovieId}), Source: {Source}", - movie.Name, movieId, trailerInfo.Source); + LoggingHelper.LogDebug(_logger, "Successfully created remote trailer info for item: {ItemName} (ID: {ItemId}), Source: {Source}", + item.Name, itemId, trailerInfo.Source); return Ok(trailerInfo); } // Step 3: No trailers found (local or remote) - LoggingHelper.LogDebug(_logger, "No local or remote trailers found for movie: {MovieName} (ID: {MovieId})", movie.Name, movieId); + LoggingHelper.LogDebug(_logger, "No local or remote trailers found for item: {ItemName} (ID: {ItemId})", item.Name, itemId); - // Also check if there are any files in the movie directory that might be trailers (for debugging) - var movieDir = System.IO.Path.GetDirectoryName(movie.Path); - if (!string.IsNullOrEmpty(movieDir) && System.IO.Directory.Exists(movieDir)) + // Also check if there are any files in the item directory that might be trailers (for debugging) + var itemDir = System.IO.Path.GetDirectoryName(item.Path); + if (!string.IsNullOrEmpty(itemDir) && System.IO.Directory.Exists(itemDir)) { - var files = System.IO.Directory.GetFiles(movieDir, "*", System.IO.SearchOption.TopDirectoryOnly); - LoggingHelper.LogDebug(_logger, "Files in movie directory {MovieDir}: {Files}", - movieDir, string.Join(", ", files.Select(System.IO.Path.GetFileName))); + var files = System.IO.Directory.GetFiles(itemDir, "*", System.IO.SearchOption.TopDirectoryOnly); + LoggingHelper.LogDebug(_logger, "Files in item directory {ItemDir}: {Files}", + itemDir, string.Join(", ", files.Select(System.IO.Path.GetFileName))); // Look for potential trailer files var potentialTrailers = files.Where(f => @@ -263,8 +263,8 @@ public ActionResult GetTrailerInfo([FromRoute] Guid movieId) } } - var error = new ErrorResponse("TRAILER_NOT_FOUND", "No trailer found for this movie", - $"Movie '{movie.Name}' does not have any local or remote trailers available") + var error = new ErrorResponse("TRAILER_NOT_FOUND", "No trailer found for this item", + $"Item '{item.Name}' does not have any local or remote trailers available") { RequestId = requestId }; @@ -272,13 +272,13 @@ public ActionResult GetTrailerInfo([FromRoute] Guid movieId) } catch (UnauthorizedAccessException ex) { - LoggingHelper.LogError(_logger, ex, "Unauthorized access getting trailer info for movie {MovieId}", movieId); + LoggingHelper.LogError(_logger, ex, "Unauthorized access getting trailer info for item {ItemId}", itemId); var error = ErrorResponse.FromException(ex, requestId); return StatusCode(403, error); } catch (Exception ex) { - LoggingHelper.LogError(_logger, ex, "Unexpected error getting trailer info for movie {MovieId}", movieId); + LoggingHelper.LogError(_logger, ex, "Unexpected error getting trailer info for item {ItemId}", itemId); var error = ErrorResponse.FromException(ex, requestId); return StatusCode(500, error); } @@ -841,16 +841,16 @@ function createVideoPreview(trailerPath, cardElement) {{ return container; }} - function showPreview(element, movieId) {{ - if (currentPreview || isPlaying) return; - - log('Showing preview for movie:', movieId); - - // Show loading toast - showToast('Loading trailer...', 'loading'); - - // Get trailer info from API - fetch(`${{API_BASE_URL}}/HoverTrailer/TrailerInfo/${{movieId}}`) + function showPreview(element, itemId) {{ + if (currentPreview || isPlaying) return; + + log('Showing preview for item:', itemId); + + // Show loading toast + showToast('Loading trailer...', 'loading'); + + // Get trailer info from API + fetch(`${{API_BASE_URL}}/HoverTrailer/TrailerInfo/${{itemId}}`) .then(response => {{ if (!response.ok) {{ if (response.status === 404) {{ @@ -1106,16 +1106,16 @@ function hidePreview() {{ }} function attachHoverListeners() {{ - const movieCards = document.querySelectorAll('[data-type=""Movie""], .card[data-itemtype=""Movie""]'); + const itemCards = document.querySelectorAll('[data-type=""Movie""], [data-type=""Series""], .card[data-itemtype=""Movie""], .card[data-itemtype=""Series""]'); let newCardsCount = 0; - movieCards.forEach(card => {{ + itemCards.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) {{ - log('Warning: Found movie card without ID'); + const itemId = card.getAttribute('data-id') || card.getAttribute('data-itemid'); + if (!itemId) {{ + log('Warning: Found card without ID'); return; }} @@ -1128,7 +1128,7 @@ function attachHoverListeners() {{ clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => {{ - showPreview(card, movieId); + showPreview(card, itemId); }}, HOVER_DELAY); }}); @@ -1145,7 +1145,7 @@ function attachHoverListeners() {{ }}); if (newCardsCount > 0) {{ - console.log(`[HoverTrailer] Attached hover listeners to ${{newCardsCount}} new movie cards`); + console.log(`[HoverTrailer] Attached hover listeners to ${{newCardsCount}} new cards`); }} }} @@ -1158,29 +1158,29 @@ function attachHoverListeners() {{ // Re-attach listeners when navigation occurs (debounced) const observer = new MutationObserver((mutations) => {{ - // Check if any mutations added movie cards - let hasMovieCardChanges = false; + // Check if any mutations added item cards (Movie or Series) + let hasItemCardChanges = 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; + // Check if it's an item card or contains item cards + if (node.matches && (node.matches('[data-type=""Movie""], [data-type=""Series""]') || node.matches('.card[data-itemtype=""Movie""], .card[data-itemtype=""Series""]'))) {{ + hasItemCardChanges = true; break; }} - if (node.querySelector && node.querySelector('[data-type=""Movie""], .card[data-itemtype=""Movie""]')) {{ - hasMovieCardChanges = true; + if (node.querySelector && node.querySelector('[data-type=""Movie""], [data-type=""Series""], .card[data-itemtype=""Movie""], .card[data-itemtype=""Series""]')) {{ + hasItemCardChanges = true; break; }} }} }} }} - if (hasMovieCardChanges) break; + if (hasItemCardChanges) break; }} - // Only process if movie cards were added - if (hasMovieCardChanges) {{ + // Only process if item cards were added + if (hasItemCardChanges) {{ // Debounce to prevent excessive re-attachment clearTimeout(mutationDebounce); mutationDebounce = setTimeout(() => {{