From e84a897c2795ec72fbde84c3ecfe07dc65ff1772 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 9 Jun 2026 13:03:34 -0400 Subject: [PATCH] feat: add IIntroProvider for Wholphin/cinema-mode compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Configuration/PluginConfiguration.cs | 5 ++ .../Configuration/config.html | 22 +++++ ...fin.Plugin.CinemaTrailers4Jellyfins.csproj | 4 +- .../PluginServiceRegistrator.cs | 2 + .../Services/TrailerIntroProvider.cs | 85 +++++++++++++++++++ build.yaml | 10 +-- 6 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerIntroProvider.cs diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/PluginConfiguration.cs index 0f040ef..7b3d1a4 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/PluginConfiguration.cs @@ -38,5 +38,10 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins.Configuration /// Maximum trailers to keep on disk. Oldest are deleted first when exceeded. 0 = unlimited. public int MaxTotalTrailers { get; set; } = 50; + + // ── IIntroProvider (Cinema Mode / Wholphin) ─────────────────────────── + + /// Number of trailers to inject before each movie via IIntroProvider. 0 = disabled. + public int TrailersPerMovie { get; set; } = 1; } } diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/config.html b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/config.html index b072592..a39f565 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/config.html +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Configuration/config.html @@ -217,6 +217,26 @@ + +
+

Cinema Mode Integration

+

+ When enabled, this plugin registers as an IIntroProvider and + injects downloaded trailers before movies — compatible with Jellyfin's built-in + cinema mode support and clients like Wholphin. + The output folder must be added as a Jellyfin Movies library + and scanned before trailers appear. +

+
+ + +
+ Number of trailers to play before each movie. Set to 0 to disable. + Default: 1. +
+
+
+

Advanced

@@ -264,6 +284,7 @@ document.getElementById('lang-' + code).checked = langs.length === 0 || langs.indexOf(code) !== -1; }); document.getElementById('max-total-trailers').value = config.MaxTotalTrailers ?? 50; + document.getElementById('trailers-per-movie').value = config.TrailersPerMovie ?? 1; Dashboard.hideLoadingMsg(); }); }); @@ -291,6 +312,7 @@ // If all are checked treat it as "no preference" (empty string) config.AllowedLanguages = checkedLangs.length === allLangCodes.length ? '' : checkedLangs.join(','); config.MaxTotalTrailers = parseInt(document.getElementById('max-total-trailers').value, 10) || 50; + config.TrailersPerMovie = parseInt(document.getElementById('trailers-per-movie').value, 10) || 0; ApiClient.updatePluginConfiguration(pluginId, config) .then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj index 8197966..941f042 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Jellyfin.Plugin.CinemaTrailers4Jellyfins.csproj @@ -3,8 +3,8 @@ net9.0 Jellyfin.Plugin.CinemaTrailers4Jellyfins - 1.0.0.0 - 1.0.0.0 + 1.0.0.2 + 1.0.0.2 enable false false diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/PluginServiceRegistrator.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/PluginServiceRegistrator.cs index 91aee9a..a8a51c2 100644 --- a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/PluginServiceRegistrator.cs +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/PluginServiceRegistrator.cs @@ -1,6 +1,7 @@ using Jellyfin.Plugin.CinemaTrailers4Jellyfins.ScheduledTasks; using Jellyfin.Plugin.CinemaTrailers4Jellyfins.Services; using MediaBrowser.Controller; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -15,6 +16,7 @@ namespace Jellyfin.Plugin.CinemaTrailers4Jellyfins serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } } diff --git a/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerIntroProvider.cs b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerIntroProvider.cs new file mode 100644 index 0000000..cf67432 --- /dev/null +++ b/Jellyfin.Plugin.CinemaTrailers4Jellyfins/Services/TrailerIntroProvider.cs @@ -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 _logger; + + public string Name => "CinemaTrailers4Jellyfins"; + + public TrailerIntroProvider(ILibraryManager libraryManager, ILogger logger) + { + _libraryManager = libraryManager; + _logger = logger; + } + + public Task> GetIntros(BaseItem item, User user) + { + var config = Plugin.Instance?.Configuration; + if (config == null || config.TrailersPerMovie <= 0) + return Task.FromResult(Enumerable.Empty()); + + if (item is not Movie) + return Task.FromResult(Enumerable.Empty()); + + var outputFolder = config.DownloadFolder?.TrimEnd( + Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (string.IsNullOrEmpty(outputFolder)) + return Task.FromResult(Enumerable.Empty()); + + // 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()); + + // 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() + .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()); + } + + 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>(selected); + } + } +} diff --git a/build.yaml b/build.yaml index 9f20386..98ea84d 100644 --- a/build.yaml +++ b/build.yaml @@ -1,5 +1,5 @@ --- -version: 1.0.0.0 +version: 1.0.0.2 name: CinemaTrailers4Jellyfins guid: b581493e-1046-40ed-b6dc-cb8027624984 description: > @@ -12,10 +12,10 @@ category: General owner: 514mart targetAbi: 10.11.0.0 changelog: - - Initial release based on Trailers4Jellyfin — TMDB/YouTube trailer downloads, - scheduled task, language/date filters, and trailer rotation, repackaged so - each trailer ships inside its own fake-movie folder (placeholder video + - locked NFO + trailer) for use with a Cinema Mode pre-roll plugin + - Add IIntroProvider support — downloaded trailers now play as pre-rolls before + movies in Jellyfin clients with cinema mode (Wholphin, etc.); configurable + trailers-per-movie count; recursion-safe so the intro items themselves never + trigger a second round of intro injection dotnetProjects: - name: Jellyfin.Plugin.CinemaTrailers4Jellyfins