From d4a3e36e9eeeeb03edd5da8e27b046e27210a0d7 Mon Sep 17 00:00:00 2001 From: troberts Date: Sun, 22 Jun 2025 13:14:30 -0700 Subject: [PATCH 1/6] initial working version, with hardcoded list of libraries --- Jellyfin.Plugin.CinemaMode/IntroProvider.cs | 162 +++++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Plugin.CinemaMode/IntroProvider.cs b/Jellyfin.Plugin.CinemaMode/IntroProvider.cs index 2a5c135..624977e 100644 --- a/Jellyfin.Plugin.CinemaMode/IntroProvider.cs +++ b/Jellyfin.Plugin.CinemaMode/IntroProvider.cs @@ -14,25 +14,185 @@ public class IntroProvider : IIntroProvider public readonly ILogger Logger; + // Add property to specify the target library name + public string TargetLibraryName { get; set; } = "Movies"; // Default to "Movies" + public IntroProvider(ILogger logger) { this.Logger = logger; + this.Logger.LogInformation($"CinemaMode IntroProvider initialized with target library: '{TargetLibraryName}'"); } public Task> GetIntros(BaseItem item, User user) { + this.Logger.LogDebug($"GetIntros called for item: '{item.Name}' (ID: {item.Id}) by user: '{user.Username}'"); + // Check item type, for now just pre roll movies if (item is not MediaBrowser.Controller.Entities.Movies.Movie) { + this.Logger.LogDebug($"Skipping intros for item '{item.Name}' - not a movie (type: {item.GetType().Name})"); return Task.FromResult(Enumerable.Empty()); } + this.Logger.LogDebug($"Item '{item.Name}' is a movie, proceeding with library check"); + + // Check if the item belongs to the target library + if (!string.IsNullOrEmpty(TargetLibraryName)) + { + this.Logger.LogDebug($"Library filtering enabled. Target library: '{TargetLibraryName}'"); + + var library = GetLibraryFromItem(item); + if (library == null) + { + this.Logger.LogWarning($"Could not determine library for item '{item.Name}' (Path: {item.Path})"); + return Task.FromResult(Enumerable.Empty()); + } + + this.Logger.LogDebug($"Item '{item.Name}' found in library: '{library.Name}'"); + + if (!library.Name.Equals(TargetLibraryName, System.StringComparison.OrdinalIgnoreCase)) + { + this.Logger.LogInformation($"Skipping intros for item '{item.Name}' - in library '{library.Name}' but target is '{TargetLibraryName}'"); + return Task.FromResult(Enumerable.Empty()); + } + + this.Logger.LogInformation($"Item '{item.Name}' matches target library '{TargetLibraryName}', proceeding with intros"); + } + else + { + this.Logger.LogDebug("Library filtering disabled (TargetLibraryName is null or empty), proceeding with intros for all movies"); + } + + this.Logger.LogDebug($"Creating IntroManager and getting intros for item '{item.Name}'"); IntroManager introManager = new IntroManager(this.Logger); - return Task.FromResult(introManager.Get(item, user)); + var intros = introManager.Get(item, user); + var introList = intros.ToList(); + + this.Logger.LogInformation($"Found {introList.Count} intros for item '{item.Name}' in library '{TargetLibraryName}'"); + foreach (var intro in introList) + { + this.Logger.LogDebug($"Intro: ItemId={intro.ItemId}, Path={intro.Path}"); + } + + return Task.FromResult(introList.AsEnumerable()); + } + + private MediaBrowser.Controller.Entities.CollectionFolder GetLibraryFromItem(BaseItem item) + { + this.Logger.LogDebug($"Getting library for item: '{item.Name}' (Path: {item.Path})"); + + try + { + // Get all libraries and find the one that contains this item + var libraries = Plugin.LibraryManager.GetVirtualFolders(); + this.Logger.LogDebug($"Found {libraries.Count()} total libraries"); + + foreach (var library in libraries) + { + this.Logger.LogDebug($"Checking library: '{library.Name}' (ID: {library.ItemId}, Type: {library.CollectionType})"); + + if (library.CollectionType.ToString().Equals("movies", System.StringComparison.OrdinalIgnoreCase)) + { + this.Logger.LogDebug($"Library '{library.Name}' is a movie library, checking if item belongs to it"); + + // Get the library folder + var libraryFolder = Plugin.LibraryManager.GetItemById(library.ItemId) as MediaBrowser.Controller.Entities.CollectionFolder; + if (libraryFolder == null) + { + this.Logger.LogWarning($"Could not get library folder for library '{library.Name}' (ID: {library.ItemId})"); + continue; + } + + // Check if the item belongs to this library + if (IsItemInLibrary(item, libraryFolder)) + { + this.Logger.LogDebug($"Item '{item.Name}' belongs to library '{library.Name}'"); + return libraryFolder; + } + else + { + this.Logger.LogDebug($"Item '{item.Name}' does not belong to library '{library.Name}'"); + } + } + else + { + this.Logger.LogDebug($"Library '{library.Name}' is not a movie library (type: {library.CollectionType}), skipping"); + } + } + + this.Logger.LogWarning($"Could not find a movie library containing item '{item.Name}'"); + } + catch (System.Exception ex) + { + this.Logger.LogError($"Error getting library for item {item.Name}: {ex.Message}"); + this.Logger.LogError($"Stack trace: {ex.StackTrace}"); + } + return null; + } + + private System.Guid GetLibraryIdFromItem(BaseItem item) + { + this.Logger.LogDebug($"GetLibraryIdFromItem called for item: '{item.Name}' (ID: {item.Id}, ParentId: {item.ParentId}, Path: {item.GetInternalMetadataPath()})"); + + if (item.ParentId != System.Guid.Empty) + { + this.Logger.LogDebug($"Item '{item.Name}' has parent ID: {item.ParentId}, getting parent item"); + + var parent = Plugin.LibraryManager.GetItemById(item.ParentId); + if (parent != null) + { + this.Logger.LogDebug($"Found parent: '{parent.Name}' (ID: {parent.Id}, Type: {parent.GetType().Name})"); + + if (parent is MediaBrowser.Controller.Entities.CollectionFolder) + { + this.Logger.LogDebug($"Parent '{parent.Name}' is a CollectionFolder (library), returning its ID: {parent.Id}"); + return parent.Id; + } + else + { + this.Logger.LogDebug($"Parent '{parent.Name}' is not a CollectionFolder, recursively checking its parent"); + return GetLibraryIdFromItem(parent); + } + } + else + { + this.Logger.LogWarning($"Could not find parent item with ID: {item.ParentId} for item '{item.Name}'"); + } + } + else + { + this.Logger.LogDebug($"Item '{item.Name}' has no parent (ParentId is Guid.Empty)"); + } + + this.Logger.LogDebug($"No library found for item '{item.Name}', returning Guid.Empty"); + return System.Guid.Empty; + } + + private bool IsItemInLibrary(BaseItem item, BaseItem libraryFolder) + { + this.Logger.LogDebug($"Checking if item '{item.Name}' (ID: {item.Id}) is in library folder '{libraryFolder.Name}' (ID: {libraryFolder.Id})"); + + try + { + // Use InternalItemsQuery to check if the item is in the specific library + var query = new InternalItemsQuery(); + query.AncestorIds = new System.Guid[] { libraryFolder.Id }; + var libraryItems = Plugin.LibraryManager.GetItemList(query); + + bool isInLibrary = libraryItems.Any(libItem => libItem.Id == item.Id); + this.Logger.LogDebug($"Item '{item.Name}' found in library '{libraryFolder.Name}': {isInLibrary} (Library contains {libraryItems.Count} items)"); + return isInLibrary; + } + catch (System.Exception ex) + { + this.Logger.LogError($"Error checking if item '{item.Name}' is in library '{libraryFolder.Name}': {ex.Message}"); + return false; + } } public IEnumerable GetAllIntroFiles() { + this.Logger.LogDebug("GetAllIntroFiles called - not implemented"); // not implemented return Enumerable.Empty(); } From b8fa4254582a7e0eacbd6fc0d8e7f2fce76e3949 Mon Sep 17 00:00:00 2001 From: troberts Date: Sun, 22 Jun 2025 13:26:44 -0700 Subject: [PATCH 2/6] Update IntroProvider.cs Cleaned up excess code Added comments --- Jellyfin.Plugin.CinemaMode/IntroProvider.cs | 135 ++++++++++---------- 1 file changed, 71 insertions(+), 64 deletions(-) diff --git a/Jellyfin.Plugin.CinemaMode/IntroProvider.cs b/Jellyfin.Plugin.CinemaMode/IntroProvider.cs index 624977e..cdb4ac1 100644 --- a/Jellyfin.Plugin.CinemaMode/IntroProvider.cs +++ b/Jellyfin.Plugin.CinemaMode/IntroProvider.cs @@ -8,26 +8,46 @@ namespace Jellyfin.Plugin.CinemaMode { + /// + /// Provides intro content (trailers, pre-rolls) for movies in Jellyfin. + /// Supports filtering by specific library names. + /// public class IntroProvider : IIntroProvider { public string Name { get; } = "CinemaMode"; - public readonly ILogger Logger; + private readonly ILogger Logger; - // Add property to specify the target library name - public string TargetLibraryName { get; set; } = "Movies"; // Default to "Movies" + /// + /// The name of the library to filter intros for. + /// If null or empty, intros are provided for all movies. + /// + public string TargetLibraryName { get; set; } = "Movies"; public IntroProvider(ILogger logger) { this.Logger = logger; + + // Log initialization with debug information this.Logger.LogInformation($"CinemaMode IntroProvider initialized with target library: '{TargetLibraryName}'"); + this.Logger.LogDebug("Debug logging enabled for CinemaMode IntroProvider"); + this.Logger.LogDebug($"Logger type: {logger.GetType().Name}"); + this.Logger.LogDebug($"Target library name: '{TargetLibraryName}'"); + this.Logger.LogDebug($"Plugin instance available: {Plugin.Instance != null}"); } + /// + /// Gets intro content for the specified item and user. + /// Only provides intros for movies in the target library (if specified). + /// + /// The item to get intros for + /// The user requesting intros + /// Collection of intro information public Task> GetIntros(BaseItem item, User user) { this.Logger.LogDebug($"GetIntros called for item: '{item.Name}' (ID: {item.Id}) by user: '{user.Username}'"); - // Check item type, for now just pre roll movies + // Only process movies if (item is not MediaBrowser.Controller.Entities.Movies.Movie) { this.Logger.LogDebug($"Skipping intros for item '{item.Name}' - not a movie (type: {item.GetType().Name})"); @@ -36,35 +56,22 @@ public Task> GetIntros(BaseItem item, User user) this.Logger.LogDebug($"Item '{item.Name}' is a movie, proceeding with library check"); - // Check if the item belongs to the target library + // Apply library filtering if enabled if (!string.IsNullOrEmpty(TargetLibraryName)) { - this.Logger.LogDebug($"Library filtering enabled. Target library: '{TargetLibraryName}'"); - - var library = GetLibraryFromItem(item); - if (library == null) + if (!IsItemInTargetLibrary(item)) { - this.Logger.LogWarning($"Could not determine library for item '{item.Name}' (Path: {item.Path})"); return Task.FromResult(Enumerable.Empty()); } - - this.Logger.LogDebug($"Item '{item.Name}' found in library: '{library.Name}'"); - - if (!library.Name.Equals(TargetLibraryName, System.StringComparison.OrdinalIgnoreCase)) - { - this.Logger.LogInformation($"Skipping intros for item '{item.Name}' - in library '{library.Name}' but target is '{TargetLibraryName}'"); - return Task.FromResult(Enumerable.Empty()); - } - - this.Logger.LogInformation($"Item '{item.Name}' matches target library '{TargetLibraryName}', proceeding with intros"); } else { this.Logger.LogDebug("Library filtering disabled (TargetLibraryName is null or empty), proceeding with intros for all movies"); } + // Get intros from IntroManager this.Logger.LogDebug($"Creating IntroManager and getting intros for item '{item.Name}'"); - IntroManager introManager = new IntroManager(this.Logger); + var introManager = new IntroManager(this.Logger); var intros = introManager.Get(item, user); var introList = intros.ToList(); @@ -77,13 +84,45 @@ public Task> GetIntros(BaseItem item, User user) return Task.FromResult(introList.AsEnumerable()); } + /// + /// Checks if the item belongs to the target library. + /// + /// The item to check + /// True if the item is in the target library, false otherwise + private bool IsItemInTargetLibrary(BaseItem item) + { + this.Logger.LogDebug($"Library filtering enabled. Target library: '{TargetLibraryName}'"); + + var library = GetLibraryFromItem(item); + if (library == null) + { + this.Logger.LogWarning($"Could not determine library for item '{item.Name}' (Path: {item.Path})"); + return false; + } + + this.Logger.LogDebug($"Item '{item.Name}' found in library: '{library.Name}'"); + + if (!library.Name.Equals(TargetLibraryName, System.StringComparison.OrdinalIgnoreCase)) + { + this.Logger.LogInformation($"Skipping intros for item '{item.Name}' - in library '{library.Name}' but target is '{TargetLibraryName}'"); + return false; + } + + this.Logger.LogInformation($"Item '{item.Name}' matches target library '{TargetLibraryName}', proceeding with intros"); + return true; + } + + /// + /// Finds the library that contains the specified item. + /// + /// The item to find the library for + /// The library folder containing the item, or null if not found private MediaBrowser.Controller.Entities.CollectionFolder GetLibraryFromItem(BaseItem item) { this.Logger.LogDebug($"Getting library for item: '{item.Name}' (Path: {item.Path})"); try { - // Get all libraries and find the one that contains this item var libraries = Plugin.LibraryManager.GetVirtualFolders(); this.Logger.LogDebug($"Found {libraries.Count()} total libraries"); @@ -95,7 +134,6 @@ private MediaBrowser.Controller.Entities.CollectionFolder GetLibraryFromItem(Bas { this.Logger.LogDebug($"Library '{library.Name}' is a movie library, checking if item belongs to it"); - // Get the library folder var libraryFolder = Plugin.LibraryManager.GetItemById(library.ItemId) as MediaBrowser.Controller.Entities.CollectionFolder; if (libraryFolder == null) { @@ -103,7 +141,6 @@ private MediaBrowser.Controller.Entities.CollectionFolder GetLibraryFromItem(Bas continue; } - // Check if the item belongs to this library if (IsItemInLibrary(item, libraryFolder)) { this.Logger.LogDebug($"Item '{item.Name}' belongs to library '{library.Name}'"); @@ -130,51 +167,18 @@ private MediaBrowser.Controller.Entities.CollectionFolder GetLibraryFromItem(Bas return null; } - private System.Guid GetLibraryIdFromItem(BaseItem item) - { - this.Logger.LogDebug($"GetLibraryIdFromItem called for item: '{item.Name}' (ID: {item.Id}, ParentId: {item.ParentId}, Path: {item.GetInternalMetadataPath()})"); - - if (item.ParentId != System.Guid.Empty) - { - this.Logger.LogDebug($"Item '{item.Name}' has parent ID: {item.ParentId}, getting parent item"); - - var parent = Plugin.LibraryManager.GetItemById(item.ParentId); - if (parent != null) - { - this.Logger.LogDebug($"Found parent: '{parent.Name}' (ID: {parent.Id}, Type: {parent.GetType().Name})"); - - if (parent is MediaBrowser.Controller.Entities.CollectionFolder) - { - this.Logger.LogDebug($"Parent '{parent.Name}' is a CollectionFolder (library), returning its ID: {parent.Id}"); - return parent.Id; - } - else - { - this.Logger.LogDebug($"Parent '{parent.Name}' is not a CollectionFolder, recursively checking its parent"); - return GetLibraryIdFromItem(parent); - } - } - else - { - this.Logger.LogWarning($"Could not find parent item with ID: {item.ParentId} for item '{item.Name}'"); - } - } - else - { - this.Logger.LogDebug($"Item '{item.Name}' has no parent (ParentId is Guid.Empty)"); - } - - this.Logger.LogDebug($"No library found for item '{item.Name}', returning Guid.Empty"); - return System.Guid.Empty; - } - + /// + /// Checks if an item belongs to a specific library using Jellyfin's internal item hierarchy. + /// + /// The item to check + /// The library folder to check against + /// True if the item is in the library, false otherwise private bool IsItemInLibrary(BaseItem item, BaseItem libraryFolder) { this.Logger.LogDebug($"Checking if item '{item.Name}' (ID: {item.Id}) is in library folder '{libraryFolder.Name}' (ID: {libraryFolder.Id})"); try { - // Use InternalItemsQuery to check if the item is in the specific library var query = new InternalItemsQuery(); query.AncestorIds = new System.Guid[] { libraryFolder.Id }; var libraryItems = Plugin.LibraryManager.GetItemList(query); @@ -190,10 +194,13 @@ private bool IsItemInLibrary(BaseItem item, BaseItem libraryFolder) } } + /// + /// Gets all intro files. Not implemented in this plugin. + /// + /// Empty collection public IEnumerable GetAllIntroFiles() { this.Logger.LogDebug("GetAllIntroFiles called - not implemented"); - // not implemented return Enumerable.Empty(); } } From 0402b937b8c1c1fac59eab68fd7124f782fbc73c Mon Sep 17 00:00:00 2001 From: troberts Date: Sun, 22 Jun 2025 14:39:18 -0700 Subject: [PATCH 3/6] Now can specify included libraries in plugin configuration --- .../Configuration/PluginConfiguration.cs | 2 + .../Configuration/config.html | 16 +++++ Jellyfin.Plugin.CinemaMode/IntroProvider.cs | 71 ++++++++++++++----- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/Jellyfin.Plugin.CinemaMode/Configuration/PluginConfiguration.cs b/Jellyfin.Plugin.CinemaMode/Configuration/PluginConfiguration.cs index e5575bb..43a7400 100644 --- a/Jellyfin.Plugin.CinemaMode/Configuration/PluginConfiguration.cs +++ b/Jellyfin.Plugin.CinemaMode/Configuration/PluginConfiguration.cs @@ -74,6 +74,7 @@ public class PluginConfiguration : BasePluginConfiguration public bool EnforceRatingLimitTrailers { get; set; } public int NumberOfTrailers { get; set; } public bool TrailerConsumeMode { get; set; } + public string IncludedLibraries { get; set; } public PluginConfiguration() { @@ -90,6 +91,7 @@ public PluginConfiguration() NumberOfTrailers = 2; EnforceRatingLimitTrailers = true; TrailerConsumeMode = false; + IncludedLibraries = ""; } } } diff --git a/Jellyfin.Plugin.CinemaMode/Configuration/config.html b/Jellyfin.Plugin.CinemaMode/Configuration/config.html index a49bb72..5095711 100644 --- a/Jellyfin.Plugin.CinemaMode/Configuration/config.html +++ b/Jellyfin.Plugin.CinemaMode/Configuration/config.html @@ -197,6 +197,20 @@

Seasonal Tag Definitions

+ + +
+ +

Included Libraries

+
+
+ + +
+ Comma-separated list of library names where Cinema Mode should be active. Leave empty for all libraries. +
+
+

@@ -494,6 +508,7 @@

Seasonal Tag Definitions

initSeasonalTagDefs(config.SeasonalTagDefinitions); initTrailerRules(config.TrailerSelectionRules); + $('#included-libraries').val(config.IncludedLibraries); Dashboard.hideLoadingMsg(); }); }); @@ -519,6 +534,7 @@

Seasonal Tag Definitions

config.NumberOfTrailers = parseInt(NumberOfTrailers); config.EnforceRatingLimitTrailers = document.getElementById('enforce-rating-limit-trailers').checked; config.TrailerConsumeMode = document.getElementById('trailer-rules-consume-mode').checked; + config.IncludedLibraries = $('#included-libraries').val(); ApiClient.updatePluginConfiguration(pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/Jellyfin.Plugin.CinemaMode/IntroProvider.cs b/Jellyfin.Plugin.CinemaMode/IntroProvider.cs index cdb4ac1..8e5c137 100644 --- a/Jellyfin.Plugin.CinemaMode/IntroProvider.cs +++ b/Jellyfin.Plugin.CinemaMode/IntroProvider.cs @@ -19,26 +19,65 @@ public class IntroProvider : IIntroProvider private readonly ILogger Logger; /// - /// The name of the library to filter intros for. + /// The names of the libraries to filter intros for. /// If null or empty, intros are provided for all movies. /// - public string TargetLibraryName { get; set; } = "Movies"; + public List TargetLibraryNames { get; set; } = new List(); public IntroProvider(ILogger logger) { this.Logger = logger; + // Load target library names from configuration + LoadTargetLibraryNames(); + // Log initialization with debug information - this.Logger.LogInformation($"CinemaMode IntroProvider initialized with target library: '{TargetLibraryName}'"); + this.Logger.LogInformation($"CinemaMode IntroProvider initialized with target libraries: [{string.Join(", ", TargetLibraryNames)}]"); this.Logger.LogDebug("Debug logging enabled for CinemaMode IntroProvider"); this.Logger.LogDebug($"Logger type: {logger.GetType().Name}"); - this.Logger.LogDebug($"Target library name: '{TargetLibraryName}'"); + this.Logger.LogDebug($"Target library names: [{string.Join(", ", TargetLibraryNames)}]"); this.Logger.LogDebug($"Plugin instance available: {Plugin.Instance != null}"); } + /// + /// Loads target library names from the plugin configuration. + /// Parses the comma-separated IncludedLibraries string into a list. + /// + private void LoadTargetLibraryNames() + { + try + { + if (Plugin.Instance?.Configuration?.IncludedLibraries != null) + { + var includedLibraries = Plugin.Instance.Configuration.IncludedLibraries.Trim(); + if (!string.IsNullOrEmpty(includedLibraries)) + { + TargetLibraryNames = includedLibraries + .Split(',') + .Select(lib => lib.Trim()) + .Where(lib => !string.IsNullOrEmpty(lib)) + .ToList(); + } + else + { + TargetLibraryNames = new List(); + } + } + else + { + TargetLibraryNames = new List(); + } + } + catch (System.Exception ex) + { + this.Logger.LogError($"Error loading target library names from configuration: {ex.Message}"); + TargetLibraryNames = new List(); + } + } + /// /// Gets intro content for the specified item and user. - /// Only provides intros for movies in the target library (if specified). + /// Only provides intros for movies in the target libraries (if specified). /// /// The item to get intros for /// The user requesting intros @@ -57,16 +96,16 @@ public Task> GetIntros(BaseItem item, User user) this.Logger.LogDebug($"Item '{item.Name}' is a movie, proceeding with library check"); // Apply library filtering if enabled - if (!string.IsNullOrEmpty(TargetLibraryName)) + if (TargetLibraryNames != null && TargetLibraryNames.Any()) { - if (!IsItemInTargetLibrary(item)) + if (!IsItemInTargetLibraries(item)) { return Task.FromResult(Enumerable.Empty()); } } else { - this.Logger.LogDebug("Library filtering disabled (TargetLibraryName is null or empty), proceeding with intros for all movies"); + this.Logger.LogDebug("Library filtering disabled (TargetLibraryNames is null or empty), proceeding with intros for all movies"); } // Get intros from IntroManager @@ -75,7 +114,7 @@ public Task> GetIntros(BaseItem item, User user) var intros = introManager.Get(item, user); var introList = intros.ToList(); - this.Logger.LogInformation($"Found {introList.Count} intros for item '{item.Name}' in library '{TargetLibraryName}'"); + this.Logger.LogInformation($"Found {introList.Count} intros for item '{item.Name}' in target libraries [{string.Join(", ", TargetLibraryNames)}]"); foreach (var intro in introList) { this.Logger.LogDebug($"Intro: ItemId={intro.ItemId}, Path={intro.Path}"); @@ -85,13 +124,13 @@ public Task> GetIntros(BaseItem item, User user) } /// - /// Checks if the item belongs to the target library. + /// Checks if the item belongs to any of the target libraries. /// /// The item to check - /// True if the item is in the target library, false otherwise - private bool IsItemInTargetLibrary(BaseItem item) + /// True if the item is in any of the target libraries, false otherwise + private bool IsItemInTargetLibraries(BaseItem item) { - this.Logger.LogDebug($"Library filtering enabled. Target library: '{TargetLibraryName}'"); + this.Logger.LogDebug($"Library filtering enabled. Target libraries: [{string.Join(", ", TargetLibraryNames)}]"); var library = GetLibraryFromItem(item); if (library == null) @@ -102,13 +141,13 @@ private bool IsItemInTargetLibrary(BaseItem item) this.Logger.LogDebug($"Item '{item.Name}' found in library: '{library.Name}'"); - if (!library.Name.Equals(TargetLibraryName, System.StringComparison.OrdinalIgnoreCase)) + if (!TargetLibraryNames.Any(targetName => targetName.Equals(library.Name, System.StringComparison.OrdinalIgnoreCase))) { - this.Logger.LogInformation($"Skipping intros for item '{item.Name}' - in library '{library.Name}' but target is '{TargetLibraryName}'"); + this.Logger.LogInformation($"Skipping intros for item '{item.Name}' - in library '{library.Name}' but target libraries are [{string.Join(", ", TargetLibraryNames)}]"); return false; } - this.Logger.LogInformation($"Item '{item.Name}' matches target library '{TargetLibraryName}', proceeding with intros"); + this.Logger.LogInformation($"Item '{item.Name}' matches target library '{library.Name}', proceeding with intros"); return true; } From edf440ed13b5eb8bf51b9a4d350c40553b185bd8 Mon Sep 17 00:00:00 2001 From: troberts Date: Sun, 22 Jun 2025 14:56:35 -0700 Subject: [PATCH 4/6] Caches list of included libraries so it's quicker --- Jellyfin.Plugin.CinemaMode/IntroProvider.cs | 94 ++++++++++++++++----- Jellyfin.Plugin.CinemaMode/Plugin.cs | 16 ++++ 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/Jellyfin.Plugin.CinemaMode/IntroProvider.cs b/Jellyfin.Plugin.CinemaMode/IntroProvider.cs index 8e5c137..61582a3 100644 --- a/Jellyfin.Plugin.CinemaMode/IntroProvider.cs +++ b/Jellyfin.Plugin.CinemaMode/IntroProvider.cs @@ -24,26 +24,79 @@ public class IntroProvider : IIntroProvider /// public List TargetLibraryNames { get; set; } = new List(); + /// + /// Cached target library names to avoid repeated configuration parsing. + /// + private List _cachedTargetLibraryNames; + + /// + /// Cached configuration value to detect changes. + /// + private string _cachedIncludedLibraries; + + /// + /// Flag to indicate if the cache needs to be refreshed. + /// + private bool _cacheNeedsRefresh = true; + public IntroProvider(ILogger logger) { this.Logger = logger; - // Load target library names from configuration - LoadTargetLibraryNames(); + // Register this instance with the Plugin for cache management + if (Plugin.Instance != null) + { + Plugin.IntroProviderInstance = this; + this.Logger.LogDebug("IntroProvider registered with Plugin instance for cache management"); + } // Log initialization with debug information - this.Logger.LogInformation($"CinemaMode IntroProvider initialized with target libraries: [{string.Join(", ", TargetLibraryNames)}]"); + this.Logger.LogInformation($"CinemaMode IntroProvider initialized"); this.Logger.LogDebug("Debug logging enabled for CinemaMode IntroProvider"); this.Logger.LogDebug($"Logger type: {logger.GetType().Name}"); - this.Logger.LogDebug($"Target library names: [{string.Join(", ", TargetLibraryNames)}]"); this.Logger.LogDebug($"Plugin instance available: {Plugin.Instance != null}"); } + /// + /// Clears the cache to force a refresh of target library names on next access. + /// This should be called when the plugin configuration is updated. + /// + public void ClearCache() + { + this.Logger.LogDebug("Clearing target library names cache"); + _cacheNeedsRefresh = true; + _cachedTargetLibraryNames = null; + _cachedIncludedLibraries = null; + } + + /// + /// Gets the cached target library names, refreshing the cache if needed. + /// Automatically detects configuration changes and refreshes the cache. + /// + /// List of target library names + private List GetTargetLibraryNames() + { + var currentIncludedLibraries = Plugin.Instance?.Configuration?.IncludedLibraries ?? ""; + + // Check if configuration has changed + if (_cacheNeedsRefresh || _cachedTargetLibraryNames == null || + _cachedIncludedLibraries != currentIncludedLibraries) + { + this.Logger.LogDebug("Refreshing target library names cache due to configuration change"); + _cachedTargetLibraryNames = LoadTargetLibraryNames(); + _cachedIncludedLibraries = currentIncludedLibraries; + _cacheNeedsRefresh = false; + this.Logger.LogDebug($"Cached target libraries: [{string.Join(", ", _cachedTargetLibraryNames)}]"); + } + + return _cachedTargetLibraryNames; + } + /// /// Loads target library names from the plugin configuration. /// Parses the comma-separated IncludedLibraries string into a list. /// - private void LoadTargetLibraryNames() + private List LoadTargetLibraryNames() { try { @@ -52,26 +105,19 @@ private void LoadTargetLibraryNames() var includedLibraries = Plugin.Instance.Configuration.IncludedLibraries.Trim(); if (!string.IsNullOrEmpty(includedLibraries)) { - TargetLibraryNames = includedLibraries + return includedLibraries .Split(',') .Select(lib => lib.Trim()) .Where(lib => !string.IsNullOrEmpty(lib)) .ToList(); } - else - { - TargetLibraryNames = new List(); - } - } - else - { - TargetLibraryNames = new List(); } + return new List(); } catch (System.Exception ex) { this.Logger.LogError($"Error loading target library names from configuration: {ex.Message}"); - TargetLibraryNames = new List(); + return new List(); } } @@ -95,10 +141,13 @@ public Task> GetIntros(BaseItem item, User user) this.Logger.LogDebug($"Item '{item.Name}' is a movie, proceeding with library check"); + // Get cached target library names + var targetLibraryNames = GetTargetLibraryNames(); + // Apply library filtering if enabled - if (TargetLibraryNames != null && TargetLibraryNames.Any()) + if (targetLibraryNames != null && targetLibraryNames.Any()) { - if (!IsItemInTargetLibraries(item)) + if (!IsItemInTargetLibraries(item, targetLibraryNames)) { return Task.FromResult(Enumerable.Empty()); } @@ -114,7 +163,7 @@ public Task> GetIntros(BaseItem item, User user) var intros = introManager.Get(item, user); var introList = intros.ToList(); - this.Logger.LogInformation($"Found {introList.Count} intros for item '{item.Name}' in target libraries [{string.Join(", ", TargetLibraryNames)}]"); + this.Logger.LogInformation($"Found {introList.Count} intros for item '{item.Name}' in target libraries [{string.Join(", ", targetLibraryNames)}]"); foreach (var intro in introList) { this.Logger.LogDebug($"Intro: ItemId={intro.ItemId}, Path={intro.Path}"); @@ -127,10 +176,11 @@ public Task> GetIntros(BaseItem item, User user) /// Checks if the item belongs to any of the target libraries. /// /// The item to check + /// The list of target library names /// True if the item is in any of the target libraries, false otherwise - private bool IsItemInTargetLibraries(BaseItem item) + private bool IsItemInTargetLibraries(BaseItem item, List targetLibraryNames) { - this.Logger.LogDebug($"Library filtering enabled. Target libraries: [{string.Join(", ", TargetLibraryNames)}]"); + this.Logger.LogDebug($"Library filtering enabled. Target libraries: [{string.Join(", ", targetLibraryNames)}]"); var library = GetLibraryFromItem(item); if (library == null) @@ -141,9 +191,9 @@ private bool IsItemInTargetLibraries(BaseItem item) this.Logger.LogDebug($"Item '{item.Name}' found in library: '{library.Name}'"); - if (!TargetLibraryNames.Any(targetName => targetName.Equals(library.Name, System.StringComparison.OrdinalIgnoreCase))) + if (!targetLibraryNames.Any(targetName => targetName.Equals(library.Name, System.StringComparison.OrdinalIgnoreCase))) { - this.Logger.LogInformation($"Skipping intros for item '{item.Name}' - in library '{library.Name}' but target libraries are [{string.Join(", ", TargetLibraryNames)}]"); + this.Logger.LogInformation($"Skipping intros for item '{item.Name}' - in library '{library.Name}' but target libraries are [{string.Join(", ", targetLibraryNames)}]"); return false; } diff --git a/Jellyfin.Plugin.CinemaMode/Plugin.cs b/Jellyfin.Plugin.CinemaMode/Plugin.cs index 28419c0..2ccf1a4 100644 --- a/Jellyfin.Plugin.CinemaMode/Plugin.cs +++ b/Jellyfin.Plugin.CinemaMode/Plugin.cs @@ -24,6 +24,11 @@ public class Plugin : BasePlugin, IHasWebPages public static ILibraryManager LibraryManager { get; private set; } + /// + /// Reference to the IntroProvider instance for cache management. + /// + public static IntroProvider IntroProviderInstance { get; set; } + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths) : base(applicationPaths, xmlSerializer) { @@ -32,6 +37,17 @@ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer, ServerApplicationPaths = serverApplicationPaths; } + /// + /// Clears the IntroProvider cache when configuration is updated. + /// + public static void ClearIntroProviderCache() + { + if (IntroProviderInstance != null) + { + IntroProviderInstance.ClearCache(); + } + } + public IEnumerable GetPages() { yield return new PluginPageInfo From 308ab6333cdb6e06b8a0e19b52d7c64acafd68fc Mon Sep 17 00:00:00 2001 From: troberts Date: Sun, 22 Jun 2025 15:13:06 -0700 Subject: [PATCH 5/6] use a multiselect --- .../Configuration/config.html | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Plugin.CinemaMode/Configuration/config.html b/Jellyfin.Plugin.CinemaMode/Configuration/config.html index 5095711..6d6d7ca 100644 --- a/Jellyfin.Plugin.CinemaMode/Configuration/config.html +++ b/Jellyfin.Plugin.CinemaMode/Configuration/config.html @@ -203,11 +203,16 @@

Seasonal Tag Definitions

Included Libraries

-
- - +
+ + +
+
0
+ +
- Comma-separated list of library names where Cinema Mode should be active. Leave empty for all libraries. + Select the movie libraries where Cinema Mode should be active. Hold Ctrl/Cmd to select multiple libraries. Leave empty for all libraries.
@@ -483,6 +488,45 @@

Included Libraries

}); }; + function initMultiselectLibrarySelector(selectorID, selectedLibraries) { + var selector = document.getElementById(selectorID); + mediaFolders.then((folderArray) => { + // Clear existing options + selector.innerHTML = ""; + + // Add movie libraries + for (const folder of folderArray) { + if (folder.CollectionType == "movies") { + var opt = document.createElement('option'); + opt.value = folder.Name; // Use library name as value + opt.innerHTML = folder.Name; + selector.appendChild(opt); + }; + }; + + // Set selected values + if (selectedLibraries && selectedLibraries.trim() !== "") { + var selectedArray = selectedLibraries.split(',').map(lib => lib.trim()); + for (let i = 0; i < selector.options.length; i++) { + if (selectedArray.includes(selector.options[i].value)) { + selector.options[i].selected = true; + } + } + } + }); + }; + + function getSelectedLibraries(selectorID) { + var selector = document.getElementById(selectorID); + var selected = []; + for (let i = 0; i < selector.options.length; i++) { + if (selector.options[i].selected) { + selected.push(selector.options[i].value); + } + } + return selected.join(','); + } + $('.cinemaModeConfigurationPage').on('pageshow', function () { Dashboard.showLoadingMsg(); clearList('trailerPreRollLibrarySelect'); @@ -508,7 +552,7 @@

Included Libraries

initSeasonalTagDefs(config.SeasonalTagDefinitions); initTrailerRules(config.TrailerSelectionRules); - $('#included-libraries').val(config.IncludedLibraries); + initMultiselectLibrarySelector('includedLibrariesSelect', config.IncludedLibraries); Dashboard.hideLoadingMsg(); }); }); @@ -534,7 +578,7 @@

Included Libraries

config.NumberOfTrailers = parseInt(NumberOfTrailers); config.EnforceRatingLimitTrailers = document.getElementById('enforce-rating-limit-trailers').checked; config.TrailerConsumeMode = document.getElementById('trailer-rules-consume-mode').checked; - config.IncludedLibraries = $('#included-libraries').val(); + config.IncludedLibraries = getSelectedLibraries('includedLibrariesSelect'); ApiClient.updatePluginConfiguration(pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); From 2e5df9f7e4fbccd7c7ead05889cdfb3fb3b7b238 Mon Sep 17 00:00:00 2001 From: troberts Date: Sun, 22 Jun 2025 15:19:35 -0700 Subject: [PATCH 6/6] Switched to checkboxes --- .../Configuration/config.html | 97 ++++++++++++++++--- 1 file changed, 85 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Plugin.CinemaMode/Configuration/config.html b/Jellyfin.Plugin.CinemaMode/Configuration/config.html index 6d6d7ca..4e1e64d 100644 --- a/Jellyfin.Plugin.CinemaMode/Configuration/config.html +++ b/Jellyfin.Plugin.CinemaMode/Configuration/config.html @@ -203,18 +203,23 @@

Seasonal Tag Definitions

Included Libraries

-
- - -
-
0
- -
-
- Select the movie libraries where Cinema Mode should be active. Hold Ctrl/Cmd to select multiple libraries. Leave empty for all libraries. +
+ +
+ When checked, Cinema Mode will be active for all movie libraries. When unchecked, only selected libraries below will be included.
+
+

Specific Libraries

+
+
+
+
+ Select specific movie libraries where Cinema Mode should be active. If no libraries are selected and "Include All Libraries" is unchecked, Cinema Mode will be disabled. +

@@ -527,6 +532,74 @@

Included Libraries

return selected.join(','); } + function initLibraryCheckboxes(selectedLibraries) { + var container = document.getElementById('included-libraries-list'); + container.innerHTML = ""; + + mediaFolders.then((folderArray) => { + var selectedArray = []; + if (selectedLibraries && selectedLibraries.trim() !== "") { + selectedArray = selectedLibraries.split(',').map(lib => lib.trim()); + } + + // Check if "all libraries" should be selected + var includeAllLibraries = selectedArray.length === 0; + document.getElementById('include-all-libraries').checked = includeAllLibraries; + + // Add movie libraries as checkboxes + for (const folder of folderArray) { + if (folder.CollectionType == "movies") { + var listItem = document.createElement('div'); + listItem.setAttribute("class", "listItem listItem-border"); + + var isSelected = selectedArray.includes(folder.Name); + + var innerHTML = '
'; + innerHTML += ''; + innerHTML += '
'; + + listItem.innerHTML = innerHTML; + container.appendChild(listItem); + } + } + + // Add event listener for "Include All Libraries" checkbox + document.getElementById('include-all-libraries').addEventListener('change', function() { + var checkboxes = container.querySelectorAll('input[type="checkbox"]'); + for (let checkbox of checkboxes) { + checkbox.checked = this.checked; + checkbox.disabled = this.checked; + } + }); + }); + } + + function getSelectedLibrariesFromCheckboxes() { + var includeAllLibraries = document.getElementById('include-all-libraries').checked; + if (includeAllLibraries) { + return ""; // Empty string means all libraries + } + + var container = document.getElementById('included-libraries-list'); + var checkboxes = container.querySelectorAll('input[type="checkbox"]:checked'); + var selected = []; + + for (let checkbox of checkboxes) { + // Extract library name from checkbox id (remove "library-" prefix and replace dashes with spaces) + var libraryName = checkbox.id.replace('library-', '').replace(/-/g, ' '); + selected.push(libraryName); + } + + return selected.join(','); + } + $('.cinemaModeConfigurationPage').on('pageshow', function () { Dashboard.showLoadingMsg(); clearList('trailerPreRollLibrarySelect'); @@ -552,7 +625,7 @@

Included Libraries

initSeasonalTagDefs(config.SeasonalTagDefinitions); initTrailerRules(config.TrailerSelectionRules); - initMultiselectLibrarySelector('includedLibrariesSelect', config.IncludedLibraries); + initLibraryCheckboxes(config.IncludedLibraries); Dashboard.hideLoadingMsg(); }); }); @@ -578,7 +651,7 @@

Included Libraries

config.NumberOfTrailers = parseInt(NumberOfTrailers); config.EnforceRatingLimitTrailers = document.getElementById('enforce-rating-limit-trailers').checked; config.TrailerConsumeMode = document.getElementById('trailer-rules-consume-mode').checked; - config.IncludedLibraries = getSelectedLibraries('includedLibrariesSelect'); + config.IncludedLibraries = getSelectedLibrariesFromCheckboxes(); ApiClient.updatePluginConfiguration(pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); });