feat: add IIntroProvider for Wholphin/cinema-mode compatibility
All checks were successful
Publish Release / release (push) Successful in 1m34s

Registers TrailerIntroProvider as IIntroProvider. Queries fake-movie
items in the output folder and returns their local trailer extras
(LocalTrailers) as IntroInfo — mirroring jellyfin-plugin-cinemamode's
proven pattern so Wholphin plays the actual trailer, not the 3-second
black placeholder.

Recursion guard: items whose path starts with the output folder are
excluded from intro injection, so Wholphin's getIntros call on the
intro item itself returns empty. Server-scoped query bypasses library
visibility restrictions so hidden trailer libraries still work.

Adds TrailersPerMovie config option (default 1, 0 = disabled).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin
2026-06-09 13:03:34 -04:00
parent 3868876401
commit e84a897c27
6 changed files with 121 additions and 7 deletions

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services
{
public class TrailerIntroProvider : IIntroProvider
{
private static readonly Random _rng = new();
private readonly ILibraryManager _libraryManager;
private readonly ILogger<TrailerIntroProvider> _logger;
public string Name => "CinemaTrailers4Jellyfins";
public TrailerIntroProvider(ILibraryManager libraryManager, ILogger<TrailerIntroProvider> logger)
{
_libraryManager = libraryManager;
_logger = logger;
}
public Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user)
{
var config = Plugin.Instance?.Configuration;
if (config == null || config.TrailersPerMovie <= 0)
return Task.FromResult(Enumerable.Empty<IntroInfo>());
if (item is not Movie)
return Task.FromResult(Enumerable.Empty<IntroInfo>());
var outputFolder = config.DownloadFolder?.TrimEnd(
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (string.IsNullOrEmpty(outputFolder))
return Task.FromResult(Enumerable.Empty<IntroInfo>());
// Break potential recursion: don't inject intros before items that live inside
// our output folder. This covers both the fake-movie files and their trailer
// extras, so Wholphin can't trigger a getIntros loop on the intro item itself.
if (item.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true)
return Task.FromResult(Enumerable.Empty<IntroInfo>());
// Server-scoped query (no User parameter) so library visibility settings don't
// prevent trailer discovery even when the output library is hidden from users.
var fakeMovies = _libraryManager
.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Movie },
Recursive = true,
})
.OfType<Movie>()
.Where(m =>
m.Path?.StartsWith(outputFolder, StringComparison.OrdinalIgnoreCase) == true
&& m.LocalTrailers.Count > 0)
.ToList();
if (fakeMovies.Count == 0)
{
_logger.LogDebug(
"|CinemaTrailers4Jellyfins| No indexed trailers found in {Folder}. "
+ "Ensure the output folder is added as a Jellyfin Movies library and scanned.",
outputFolder);
return Task.FromResult(Enumerable.Empty<IntroInfo>());
}
var selected = fakeMovies
.OrderBy(_ => _rng.Next())
.Take(config.TrailersPerMovie)
.Select(m =>
{
var t = m.LocalTrailers.ElementAt(_rng.Next(m.LocalTrailers.Count));
return new IntroInfo { ItemId = t.Id, Path = t.Path };
})
.ToList();
return Task.FromResult<IEnumerable<IntroInfo>>(selected);
}
}
}