Browse Source

Naming refactoring and WIP porting of new interface repositories

JPVenson 11 tháng trước cách đây
mục cha
commit
be48cdd9e9
32 tập tin đã thay đổi với 601 bổ sung367 xóa
  1. 7 6
      Emby.Server.Implementations/ApplicationHost.cs
  2. 139 0
      Emby.Server.Implementations/Data/ItemTypeLookup.cs
  3. 2 2
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  4. 1 1
      Jellyfin.Data/Entities/AncestorId.cs
  5. 1 1
      Jellyfin.Data/Entities/AttachmentStreamInfo.cs
  6. 2 1
      Jellyfin.Data/Entities/BaseItemEntity.cs
  7. 20 3
      Jellyfin.Data/Entities/BaseItemProvider.cs
  8. 1 1
      Jellyfin.Data/Entities/Chapter.cs
  9. 22 1
      Jellyfin.Data/Entities/ItemValue.cs
  10. 1 1
      Jellyfin.Data/Entities/MediaStreamInfo.cs
  11. 32 2
      Jellyfin.Data/Entities/People.cs
  12. 147 247
      Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
  13. 40 16
      Jellyfin.Server.Implementations/Item/ChapterRepository.cs
  14. 1 1
      Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
  15. 5 5
      Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
  16. 4 3
      Jellyfin.Server.Implementations/Item/PeopleRepository.cs
  17. 1 1
      Jellyfin.Server.Implementations/JellyfinDbContext.cs
  18. 2 2
      Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
  19. 1 1
      Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs
  20. 0 24
      MediaBrowser.Controller/Chapters/ChapterManager.cs
  21. 0 35
      MediaBrowser.Controller/Chapters/IChapterManager.cs
  22. 49 0
      MediaBrowser.Controller/Chapters/IChapterRepository.cs
  23. 25 0
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  24. 5 2
      MediaBrowser.Controller/Entities/BaseItem.cs
  25. 0 1
      MediaBrowser.Controller/Persistence/IItemRepository.cs
  26. 57 0
      MediaBrowser.Controller/Persistence/IItemTypeLookup.cs
  27. 1 2
      MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs
  28. 5 2
      MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs
  29. 1 2
      MediaBrowser.Controller/Persistence/IPeopleRepository.cs
  30. 2 2
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  31. 2 2
      MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
  32. 25 0
      src/Jellyfin.Drawing/ImageProcessor.cs

+ 7 - 6
Emby.Server.Implementations/ApplicationHost.cs

@@ -40,6 +40,7 @@ using Jellyfin.MediaEncoding.Hls.Playlist;
 using Jellyfin.Networking.Manager;
 using Jellyfin.Networking.Udp;
 using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Item;
 using Jellyfin.Server.Implementations.MediaSegments;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
@@ -83,7 +84,6 @@ using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Tasks;
-using MediaBrowser.Providers.Chapters;
 using MediaBrowser.Providers.Lyric;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.Tmdb;
@@ -494,7 +494,12 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
-            serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+            serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
+            serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
+            serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
+            serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
+            serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
+            serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
 
             serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
             serviceCollection.AddSingleton<EncodingHelper>();
@@ -539,8 +544,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
-
             serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
 
             serviceCollection.AddSingleton<IAuthService, AuthService>();
@@ -578,8 +581,6 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
-
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
 

+ 139 - 0
Emby.Server.Implementations/Data/ItemTypeLookup.cs

@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Channels;
+using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Model.Querying;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/// <summary>
+/// Provides static topic based lookups for the BaseItemKind.
+/// </summary>
+public class ItemTypeLookup : IItemTypeLookup
+{
+    /// <summary>
+    /// Gets all values of the ItemFields type.
+    /// </summary>
+    public IReadOnlyList<ItemFields> AllItemFields { get; } = Enum.GetValues<ItemFields>();
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are considered Programs.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ProgramTypes { get; } =
+    [
+            BaseItemKind.Program,
+            BaseItemKind.TvChannel,
+            BaseItemKind.LiveTvProgram,
+            BaseItemKind.LiveTvChannel
+    ];
+
+    /// <summary>
+    /// Gets all BaseItemKinds that should be excluded from parent lookup.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ProgramExcludeParentTypes { get; } =
+    [
+            BaseItemKind.Series,
+            BaseItemKind.Season,
+            BaseItemKind.MusicAlbum,
+            BaseItemKind.MusicArtist,
+            BaseItemKind.PhotoAlbum
+    ];
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are considered to be provided by services.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ServiceTypes { get; } =
+    [
+            BaseItemKind.TvChannel,
+            BaseItemKind.LiveTvChannel
+    ];
+
+    /// <summary>
+    /// Gets all BaseItemKinds that have a StartDate.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> StartDateTypes { get; } =
+    [
+            BaseItemKind.Program,
+            BaseItemKind.LiveTvProgram
+    ];
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are considered Series.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> SeriesTypes { get; } =
+    [
+            BaseItemKind.Book,
+            BaseItemKind.AudioBook,
+            BaseItemKind.Episode,
+            BaseItemKind.Season
+    ];
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are not to be evaluated for Artists.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ArtistExcludeParentTypes { get; } =
+    [
+            BaseItemKind.Series,
+            BaseItemKind.Season,
+            BaseItemKind.PhotoAlbum
+    ];
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are considered Artists.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ArtistsTypes { get; } =
+    [
+            BaseItemKind.Audio,
+            BaseItemKind.MusicAlbum,
+            BaseItemKind.MusicVideo,
+            BaseItemKind.AudioBook
+    ];
+
+    /// <summary>
+    /// Gets mapping for all BaseItemKinds and their expected serialisaition target.
+    /// </summary>
+    public IDictionary<BaseItemKind, string?> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string?>()
+    {
+        { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName },
+        { BaseItemKind.Audio, typeof(Audio).FullName },
+        { BaseItemKind.AudioBook, typeof(AudioBook).FullName },
+        { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName },
+        { BaseItemKind.Book, typeof(Book).FullName },
+        { BaseItemKind.BoxSet, typeof(BoxSet).FullName },
+        { BaseItemKind.Channel, typeof(Channel).FullName },
+        { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName },
+        { BaseItemKind.Episode, typeof(Episode).FullName },
+        { BaseItemKind.Folder, typeof(Folder).FullName },
+        { BaseItemKind.Genre, typeof(Genre).FullName },
+        { BaseItemKind.Movie, typeof(Movie).FullName },
+        { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName },
+        { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName },
+        { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName },
+        { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName },
+        { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName },
+        { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName },
+        { BaseItemKind.Person, typeof(Person).FullName },
+        { BaseItemKind.Photo, typeof(Photo).FullName },
+        { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName },
+        { BaseItemKind.Playlist, typeof(Playlist).FullName },
+        { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName },
+        { BaseItemKind.Season, typeof(Season).FullName },
+        { BaseItemKind.Series, typeof(Series).FullName },
+        { BaseItemKind.Studio, typeof(Studio).FullName },
+        { BaseItemKind.Trailer, typeof(Trailer).FullName },
+        { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName },
+        { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName },
+        { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName },
+        { BaseItemKind.UserView, typeof(UserView).FullName },
+        { BaseItemKind.Video, typeof(Video).FullName },
+        { BaseItemKind.Year, typeof(Year).FullName }
+    }.AsReadOnly();
+}

+ 2 - 2
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.MediaEncoder
         private readonly IFileSystem _fileSystem;
         private readonly ILogger<EncodingManager> _logger;
         private readonly IMediaEncoder _encoder;
-        private readonly IChapterManager _chapterManager;
+        private readonly IChapterRepository _chapterManager;
         private readonly ILibraryManager _libraryManager;
 
         /// <summary>
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.MediaEncoder
             ILogger<EncodingManager> logger,
             IFileSystem fileSystem,
             IMediaEncoder encoder,
-            IChapterManager chapterManager,
+            IChapterRepository chapterManager,
             ILibraryManager libraryManager)
         {
             _logger = logger;

+ 1 - 1
Jellyfin.Data/Entities/AncestorId.cs

@@ -13,7 +13,7 @@ public class AncestorId
 
     public Guid ItemId { get; set; }
 
-    public required BaseItem Item { get; set; }
+    public required BaseItemEntity Item { get; set; }
 
     public string? AncestorIdText { get; set; }
 }

+ 1 - 1
Jellyfin.Data/Entities/AttachmentStreamInfo.cs

@@ -7,7 +7,7 @@ public class AttachmentStreamInfo
 {
     public required Guid ItemId { get; set; }
 
-    public required BaseItem Item { get; set; }
+    public required BaseItemEntity Item { get; set; }
 
     public required int Index { get; set; }
 

+ 2 - 1
Jellyfin.Data/Entities/BaseItem.cs → Jellyfin.Data/Entities/BaseItemEntity.cs

@@ -6,7 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema;
 namespace Jellyfin.Data.Entities;
 
 #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-public class BaseItem
+public class BaseItemEntity
 {
     [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
 
@@ -160,6 +160,7 @@ public class BaseItem
 
     public long? Size { get; set; }
 
+#pragma warning disable CA2227 // Collection properties should be read only
     public ICollection<People>? Peoples { get; set; }
 
     public ICollection<UserData>? UserData { get; set; }

+ 20 - 3
Jellyfin.Data/Entities/BaseItemProvider.cs

@@ -5,11 +5,28 @@ using System.ComponentModel.DataAnnotations.Schema;
 
 namespace Jellyfin.Data.Entities;
 
+/// <summary>
+/// Represents an Key-Value relaten of an BaseItem's provider.
+/// </summary>
 public class BaseItemProvider
 {
+    /// <summary>
+    /// Gets or Sets the reference ItemId.
+    /// </summary>
     public Guid ItemId { get; set; }
-    public required BaseItem Item { get; set; }
 
-    public string ProviderId { get; set; }
-    public string ProviderValue { get; set; }
+    /// <summary>
+    /// Gets or Sets the reference BaseItem.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the ProvidersId.
+    /// </summary>
+    public required string ProviderId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Providers Value.
+    /// </summary>
+    public required string ProviderValue { get; set; }
 }

+ 1 - 1
Jellyfin.Data/Entities/Chapter.cs

@@ -10,7 +10,7 @@ public class Chapter
 {
     public Guid ItemId { get; set; }
 
-    public required BaseItem Item { get; set; }
+    public required BaseItemEntity Item { get; set; }
 
     public required int ChapterIndex { get; set; }
 

+ 22 - 1
Jellyfin.Data/Entities/ItemValue.cs

@@ -5,12 +5,33 @@ using System.ComponentModel.DataAnnotations.Schema;
 
 namespace Jellyfin.Data.Entities;
 
+/// <summary>
+/// Represents an ItemValue for a BaseItem.
+/// </summary>
 public class ItemValue
 {
+    /// <summary>
+    /// Gets or Sets the reference ItemId.
+    /// </summary>
     public Guid ItemId { get; set; }
-    public required BaseItem Item { get; set; }
 
+    /// <summary>
+    /// Gets or Sets the referenced BaseItem.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Type.
+    /// </summary>
     public required int Type { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Value.
+    /// </summary>
     public required string Value { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the sanatised Value.
+    /// </summary>
     public required string CleanValue { get; set; }
 }

+ 1 - 1
Jellyfin.Data/Entities/MediaStreamInfo.cs

@@ -7,7 +7,7 @@ public class MediaStreamInfo
 {
     public Guid ItemId { get; set; }
 
-    public required BaseItem Item { get; set; }
+    public required BaseItemEntity Item { get; set; }
 
     public int StreamIndex { get; set; }
 

+ 32 - 2
Jellyfin.Data/Entities/People.cs

@@ -4,14 +4,44 @@ using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 
 namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// People entity.
+/// </summary>
 public class People
 {
-    public Guid ItemId { get; set; }
-    public BaseItem Item { get; set; }
+    /// <summary>
+    /// Gets or Sets The ItemId.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets Reference Item.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
 
+    /// <summary>
+    /// Gets or Sets the Persons Name.
+    /// </summary>
     public required string Name { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Role.
+    /// </summary>
     public string? Role { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Type.
+    /// </summary>
     public string? PersonType { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the SortOrder.
+    /// </summary>
     public int? SortOrder { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the ListOrder.
+    /// </summary>
     public int? ListOrder { get; set; }
 }

+ 147 - 247
Jellyfin.Server.Implementations/Item/BaseItemManager.cs → Jellyfin.Server.Implementations/Item/BaseItemRepository.cs

@@ -24,112 +24,23 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
 using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Internal;
-using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
 using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
-using BaseItemEntity = Jellyfin.Data.Entities.BaseItem;
+using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity;
 
 namespace Jellyfin.Server.Implementations.Item;
 
 /// <summary>
 /// Handles all storage logic for BaseItems.
 /// </summary>
-public sealed class BaseItemManager : IItemRepository, IDisposable
+/// <remarks>
+/// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
+/// </remarks>
+/// <param name="dbProvider">The db factory.</param>
+/// <param name="appHost">The Application host.</param>
+/// <param name="itemTypeLookup">The static type lookup.</param>
+public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost appHost, IItemTypeLookup itemTypeLookup)
+    : IItemRepository, IDisposable
 {
-    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
-    private readonly IServerApplicationHost _appHost;
-
-    private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
-
-    private static readonly BaseItemKind[] _programTypes = new[]
-    {
-            BaseItemKind.Program,
-            BaseItemKind.TvChannel,
-            BaseItemKind.LiveTvProgram,
-            BaseItemKind.LiveTvChannel
-    };
-
-    private static readonly BaseItemKind[] _programExcludeParentTypes = new[]
-    {
-            BaseItemKind.Series,
-            BaseItemKind.Season,
-            BaseItemKind.MusicAlbum,
-            BaseItemKind.MusicArtist,
-            BaseItemKind.PhotoAlbum
-    };
-
-    private static readonly BaseItemKind[] _serviceTypes = new[]
-    {
-            BaseItemKind.TvChannel,
-            BaseItemKind.LiveTvChannel
-    };
-
-    private static readonly BaseItemKind[] _startDateTypes = new[]
-    {
-            BaseItemKind.Program,
-            BaseItemKind.LiveTvProgram
-    };
-
-    private static readonly BaseItemKind[] _seriesTypes = new[]
-    {
-            BaseItemKind.Book,
-            BaseItemKind.AudioBook,
-            BaseItemKind.Episode,
-            BaseItemKind.Season
-    };
-
-    private static readonly BaseItemKind[] _artistExcludeParentTypes = new[]
-    {
-            BaseItemKind.Series,
-            BaseItemKind.Season,
-            BaseItemKind.PhotoAlbum
-    };
-
-    private static readonly BaseItemKind[] _artistsTypes = new[]
-    {
-            BaseItemKind.Audio,
-            BaseItemKind.MusicAlbum,
-            BaseItemKind.MusicVideo,
-            BaseItemKind.AudioBook
-    };
-
-    private static readonly Dictionary<BaseItemKind, string?> _baseItemKindNames = new()
-        {
-            { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName },
-            { BaseItemKind.Audio, typeof(Audio).FullName },
-            { BaseItemKind.AudioBook, typeof(AudioBook).FullName },
-            { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName },
-            { BaseItemKind.Book, typeof(Book).FullName },
-            { BaseItemKind.BoxSet, typeof(BoxSet).FullName },
-            { BaseItemKind.Channel, typeof(Channel).FullName },
-            { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName },
-            { BaseItemKind.Episode, typeof(Episode).FullName },
-            { BaseItemKind.Folder, typeof(Folder).FullName },
-            { BaseItemKind.Genre, typeof(Genre).FullName },
-            { BaseItemKind.Movie, typeof(Movie).FullName },
-            { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName },
-            { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName },
-            { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName },
-            { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName },
-            { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName },
-            { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName },
-            { BaseItemKind.Person, typeof(Person).FullName },
-            { BaseItemKind.Photo, typeof(Photo).FullName },
-            { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName },
-            { BaseItemKind.Playlist, typeof(Playlist).FullName },
-            { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName },
-            { BaseItemKind.Season, typeof(Season).FullName },
-            { BaseItemKind.Series, typeof(Series).FullName },
-            { BaseItemKind.Studio, typeof(Studio).FullName },
-            { BaseItemKind.Trailer, typeof(Trailer).FullName },
-            { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName },
-            { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName },
-            { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName },
-            { BaseItemKind.UserView, typeof(UserView).FullName },
-            { BaseItemKind.Video, typeof(Video).FullName },
-            { BaseItemKind.Year, typeof(Year).FullName }
-        };
-
     /// <summary>
     /// This holds all the types in the running assemblies
     /// so that we can de-serialize properly when we don't have strong types.
@@ -137,17 +48,6 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
     private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
     private bool _disposed;
 
-    /// <summary>
-    /// Initializes a new instance of the <see cref="BaseItemManager"/> class.
-    /// </summary>
-    /// <param name="dbProvider">The db factory.</param>
-    /// <param name="appHost">The Application host.</param>
-    public BaseItemManager(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost appHost)
-    {
-        _dbProvider = dbProvider;
-        _appHost = appHost;
-    }
-
     /// <inheritdoc/>
     public void Dispose()
     {
@@ -159,124 +59,12 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
         _disposed = true;
     }
 
-    private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, int[] itemValueTypes, string returnType)
-    {
-        ArgumentNullException.ThrowIfNull(filter);
-
-        if (!filter.Limit.HasValue)
-        {
-            filter.EnableTotalRecordCount = false;
-        }
-
-        using var context = _dbProvider.CreateDbContext();
-
-        var innerQuery = new InternalItemsQuery(filter.User)
-        {
-            ExcludeItemTypes = filter.ExcludeItemTypes,
-            IncludeItemTypes = filter.IncludeItemTypes,
-            MediaTypes = filter.MediaTypes,
-            AncestorIds = filter.AncestorIds,
-            ItemIds = filter.ItemIds,
-            TopParentIds = filter.TopParentIds,
-            ParentId = filter.ParentId,
-            IsAiring = filter.IsAiring,
-            IsMovie = filter.IsMovie,
-            IsSports = filter.IsSports,
-            IsKids = filter.IsKids,
-            IsNews = filter.IsNews,
-            IsSeries = filter.IsSeries
-        };
-        var query = TranslateQuery(context.BaseItems, context, innerQuery);
-
-        query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Contains(f.Type)));
-
-        var outerQuery = new InternalItemsQuery(filter.User)
-        {
-            IsPlayed = filter.IsPlayed,
-            IsFavorite = filter.IsFavorite,
-            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
-            IsLiked = filter.IsLiked,
-            IsLocked = filter.IsLocked,
-            NameLessThan = filter.NameLessThan,
-            NameStartsWith = filter.NameStartsWith,
-            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
-            Tags = filter.Tags,
-            OfficialRatings = filter.OfficialRatings,
-            StudioIds = filter.StudioIds,
-            GenreIds = filter.GenreIds,
-            Genres = filter.Genres,
-            Years = filter.Years,
-            NameContains = filter.NameContains,
-            SearchTerm = filter.SearchTerm,
-            SimilarTo = filter.SimilarTo,
-            ExcludeItemIds = filter.ExcludeItemIds
-        };
-        query = TranslateQuery(query, context, outerQuery)
-            .OrderBy(e => e.PresentationUniqueKey);
-
-        if (filter.OrderBy.Count != 0
-            || filter.SimilarTo is not null
-            || !string.IsNullOrEmpty(filter.SearchTerm))
-        {
-            query = ApplyOrder(query, filter);
-        }
-        else
-        {
-            query = query.OrderBy(e => e.SortName);
-        }
-
-        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
-        {
-            var offset = filter.StartIndex ?? 0;
-
-            if (offset > 0)
-            {
-                query = query.Skip(offset);
-            }
-
-            if (filter.Limit.HasValue)
-            {
-                query.Take(filter.Limit.Value);
-            }
-        }
-
-        var result = new QueryResult<(BaseItem, ItemCounts)>();
-        string countText = string.Empty;
-        if (filter.EnableTotalRecordCount)
-        {
-            result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count();
-        }
-
-        var resultQuery = query.Select(e => new
-        {
-            item = e,
-            itemCount = new ItemCounts()
-            {
-                SeriesCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Series),
-                EpisodeCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Episode),
-                MovieCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Movie),
-                AlbumCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum),
-                ArtistCount = e.ItemValues!.Count(e => e.Type == 0 || e.Type == 1),
-                SongCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum),
-                TrailerCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Trailer),
-            }
-        });
-
-        result.StartIndex = filter.StartIndex ?? 0;
-        result.Items = resultQuery.ToImmutableArray().Select(e =>
-        {
-            return (DeserialiseBaseItem(e.item), e.itemCount);
-        }).ToImmutableArray();
-
-        return result;
-    }
-
     /// <inheritdoc />
     public void DeleteItem(Guid id)
     {
         ArgumentNullException.ThrowIfNull(id.IsEmpty() ? null : id);
 
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
         using var transaction = context.Database.BeginTransaction();
         context.Peoples.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
         context.Chapters.Where(e => e.ItemId.Equals(id)).ExecuteDelete();
@@ -291,7 +79,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
     /// <inheritdoc />
     public void UpdateInheritedValues()
     {
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
         using var transaction = context.Database.BeginTransaction();
 
         context.ItemValues.Where(e => e.Type == 6).ExecuteDelete();
@@ -324,7 +112,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
         ArgumentNullException.ThrowIfNull(filter);
         PrepareFilterQuery(filter);
 
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
         var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter)
             .DistinctBy(e => e.Id);
 
@@ -352,56 +140,56 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
     /// <inheritdoc />
     public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter)
     {
-        return GetItemValues(filter, new[] { 0, 1 }, typeof(MusicArtist).FullName!);
+        return GetItemValues(filter, [0, 1], typeof(MusicArtist).FullName!);
     }
 
     /// <inheritdoc />
     public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter)
     {
-        return GetItemValues(filter, new[] { 0 }, typeof(MusicArtist).FullName!);
+        return GetItemValues(filter, [0], typeof(MusicArtist).FullName!);
     }
 
     /// <inheritdoc />
     public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
     {
-        return GetItemValues(filter, new[] { 1 }, typeof(MusicArtist).FullName!);
+        return GetItemValues(filter, [1], typeof(MusicArtist).FullName!);
     }
 
     /// <inheritdoc />
     public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter)
     {
-        return GetItemValues(filter, new[] { 3 }, typeof(Studio).FullName!);
+        return GetItemValues(filter, [3], typeof(Studio).FullName!);
     }
 
     /// <inheritdoc />
     public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter)
     {
-        return GetItemValues(filter, new[] { 2 }, typeof(Genre).FullName!);
+        return GetItemValues(filter, [2], typeof(Genre).FullName!);
     }
 
     /// <inheritdoc />
     public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
     {
-        return GetItemValues(filter, new[] { 2 }, typeof(MusicGenre).FullName!);
+        return GetItemValues(filter, [2], typeof(MusicGenre).FullName!);
     }
 
     /// <inheritdoc />
     public IReadOnlyList<string> GetStudioNames()
     {
-        return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>());
+        return GetItemValueNames([3], Array.Empty<string>(), Array.Empty<string>());
     }
 
     /// <inheritdoc />
     public IReadOnlyList<string> GetAllArtistNames()
     {
-        return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>());
+        return GetItemValueNames([0, 1], Array.Empty<string>(), Array.Empty<string>());
     }
 
     /// <inheritdoc />
     public IReadOnlyList<string> GetMusicGenreNames()
     {
         return GetItemValueNames(
-            new[] { 2 },
+            [2],
             new string[]
             {
                     typeof(Audio).FullName!,
@@ -416,7 +204,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
     public IReadOnlyList<string> GetGenreNames()
     {
         return GetItemValueNames(
-            new[] { 2 },
+            [2],
             Array.Empty<string>(),
             new string[]
             {
@@ -443,7 +231,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
         PrepareFilterQuery(filter);
         var result = new QueryResult<BaseItemDto>();
 
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
         var dbQuery = TranslateQuery(context.BaseItems, context, filter)
             .DistinctBy(e => e.Id);
         if (filter.EnableTotalRecordCount)
@@ -477,7 +265,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
         ArgumentNullException.ThrowIfNull(filter);
         PrepareFilterQuery(filter);
 
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
         var dbQuery = TranslateQuery(context.BaseItems, context, filter)
             .DistinctBy(e => e.Id);
         if (filter.Limit.HasValue || filter.StartIndex.HasValue)
@@ -505,7 +293,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
         // Hack for right now since we currently don't support filtering out these duplicates within a query
         PrepareFilterQuery(filter);
 
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
         var dbQuery = TranslateQuery(context.BaseItems, context, filter);
 
         return dbQuery.Count();
@@ -646,7 +434,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
             var excludeTypes = filter.ExcludeItemTypes;
             if (excludeTypes.Length == 1)
             {
-                if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
+                if (itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
                 {
                     baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
                 }
@@ -656,7 +444,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
                 var excludeTypeName = new List<string>();
                 foreach (var excludeType in excludeTypes)
                 {
-                    if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
+                    if (itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
                     {
                         excludeTypeName.Add(baseItemKindName!);
                     }
@@ -667,7 +455,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
         }
         else if (includeTypes.Length == 1)
         {
-            if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
+            if (itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
             {
                 baseQuery = baseQuery.Where(e => e.Type == includeTypeName);
             }
@@ -677,7 +465,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
             var includeTypeName = new List<string>();
             foreach (var includeType in includeTypes)
             {
-                if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
+                if (itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
                 {
                     includeTypeName.Add(baseItemKindName!);
                 }
@@ -1421,7 +1209,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
         ArgumentNullException.ThrowIfNull(item);
 
         var images = SerializeImages(item.ImageInfos);
-        using var db = _dbProvider.CreateDbContext();
+        using var db = dbProvider.CreateDbContext();
 
         db.BaseItems
             .Where(e => e.Id.Equals(item.Id))
@@ -1457,7 +1245,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
             tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
         }
 
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
         foreach (var item in tuples)
         {
             var entity = Map(item.Item);
@@ -1501,7 +1289,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
             throw new ArgumentException("Guid can't be empty", nameof(id));
         }
 
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
         var item = context.BaseItems.FirstOrDefault(e => e.Id.Equals(id));
         if (item is null)
         {
@@ -1832,7 +1620,7 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
 
     private IReadOnlyList<string> GetItemValueNames(int[] itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
     {
-        using var context = _dbProvider.CreateDbContext();
+        using var context = dbProvider.CreateDbContext();
 
         var query = context.ItemValues
             .Where(e => itemValueTypes.Contains(e.Type));
@@ -1857,6 +1645,118 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
         return Map(baseItemEntity, dto);
     }
 
+    private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, int[] itemValueTypes, string returnType)
+    {
+        ArgumentNullException.ThrowIfNull(filter);
+
+        if (!filter.Limit.HasValue)
+        {
+            filter.EnableTotalRecordCount = false;
+        }
+
+        using var context = dbProvider.CreateDbContext();
+
+        var innerQuery = new InternalItemsQuery(filter.User)
+        {
+            ExcludeItemTypes = filter.ExcludeItemTypes,
+            IncludeItemTypes = filter.IncludeItemTypes,
+            MediaTypes = filter.MediaTypes,
+            AncestorIds = filter.AncestorIds,
+            ItemIds = filter.ItemIds,
+            TopParentIds = filter.TopParentIds,
+            ParentId = filter.ParentId,
+            IsAiring = filter.IsAiring,
+            IsMovie = filter.IsMovie,
+            IsSports = filter.IsSports,
+            IsKids = filter.IsKids,
+            IsNews = filter.IsNews,
+            IsSeries = filter.IsSeries
+        };
+        var query = TranslateQuery(context.BaseItems, context, innerQuery);
+
+        query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Contains(f.Type)));
+
+        var outerQuery = new InternalItemsQuery(filter.User)
+        {
+            IsPlayed = filter.IsPlayed,
+            IsFavorite = filter.IsFavorite,
+            IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
+            IsLiked = filter.IsLiked,
+            IsLocked = filter.IsLocked,
+            NameLessThan = filter.NameLessThan,
+            NameStartsWith = filter.NameStartsWith,
+            NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
+            Tags = filter.Tags,
+            OfficialRatings = filter.OfficialRatings,
+            StudioIds = filter.StudioIds,
+            GenreIds = filter.GenreIds,
+            Genres = filter.Genres,
+            Years = filter.Years,
+            NameContains = filter.NameContains,
+            SearchTerm = filter.SearchTerm,
+            SimilarTo = filter.SimilarTo,
+            ExcludeItemIds = filter.ExcludeItemIds
+        };
+        query = TranslateQuery(query, context, outerQuery)
+            .OrderBy(e => e.PresentationUniqueKey);
+
+        if (filter.OrderBy.Count != 0
+            || filter.SimilarTo is not null
+            || !string.IsNullOrEmpty(filter.SearchTerm))
+        {
+            query = ApplyOrder(query, filter);
+        }
+        else
+        {
+            query = query.OrderBy(e => e.SortName);
+        }
+
+        if (filter.Limit.HasValue || filter.StartIndex.HasValue)
+        {
+            var offset = filter.StartIndex ?? 0;
+
+            if (offset > 0)
+            {
+                query = query.Skip(offset);
+            }
+
+            if (filter.Limit.HasValue)
+            {
+                query.Take(filter.Limit.Value);
+            }
+        }
+
+        var result = new QueryResult<(BaseItem, ItemCounts)>();
+        string countText = string.Empty;
+        if (filter.EnableTotalRecordCount)
+        {
+            result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count();
+        }
+
+        var resultQuery = query.Select(e => new
+        {
+            item = e,
+            itemCount = new ItemCounts()
+            {
+                SeriesCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Series),
+                EpisodeCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Episode),
+                MovieCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Movie),
+                AlbumCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum),
+                ArtistCount = e.ItemValues!.Count(e => e.Type == 0 || e.Type == 1),
+                SongCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum),
+                TrailerCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Trailer),
+            }
+        });
+
+        result.StartIndex = filter.StartIndex ?? 0;
+        result.Items = resultQuery.ToImmutableArray().Select(e =>
+        {
+            return (DeserialiseBaseItem(e.item), e.itemCount);
+        }).ToImmutableArray();
+
+        return result;
+    }
+
     private static void PrepareFilterQuery(InternalItemsQuery query)
     {
         if (query.Limit.HasValue && query.EnableGroupByMetadataKey)
@@ -2046,12 +1946,12 @@ public sealed class BaseItemManager : IItemRepository, IDisposable
             return null;
         }
 
-        return _appHost.ReverseVirtualPath(path);
+        return appHost.ReverseVirtualPath(path);
     }
 
     private string RestorePath(string path)
     {
-        return _appHost.ExpandVirtualPath(path);
+        return appHost.ExpandVirtualPath(path);
     }
 
     internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)

+ 40 - 16
Jellyfin.Server.Implementations/Item/ChapterManager.cs → Jellyfin.Server.Implementations/Item/ChapterRepository.cs

@@ -14,46 +14,69 @@ namespace Jellyfin.Server.Implementations.Item;
 /// <summary>
 /// The Chapter manager.
 /// </summary>
-public class ChapterManager : IChapterManager
+public class ChapterRepository : IChapterRepository
 {
     private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
     private readonly IImageProcessor _imageProcessor;
 
     /// <summary>
-    /// Initializes a new instance of the <see cref="ChapterManager"/> class.
+    /// Initializes a new instance of the <see cref="ChapterRepository"/> class.
     /// </summary>
     /// <param name="dbProvider">The EFCore provider.</param>
     /// <param name="imageProcessor">The Image Processor.</param>
-    public ChapterManager(IDbContextFactory<JellyfinDbContext> dbProvider, IImageProcessor imageProcessor)
+    public ChapterRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IImageProcessor imageProcessor)
     {
         _dbProvider = dbProvider;
         _imageProcessor = imageProcessor;
     }
 
-    /// <inheritdoc cref="IChapterManager"/>
+    /// <inheritdoc cref="IChapterRepository"/>
     public ChapterInfo? GetChapter(BaseItemDto baseItem, int index)
+    {
+        return GetChapter(baseItem.Id, index);
+    }
+
+    /// <inheritdoc cref="IChapterRepository"/>
+    public IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem)
+    {
+        return GetChapters(baseItem.Id);
+    }
+
+    /// <inheritdoc cref="IChapterRepository"/>
+    public ChapterInfo? GetChapter(Guid baseItemId, int index)
     {
         using var context = _dbProvider.CreateDbContext();
-        var chapter = context.Chapters.FirstOrDefault(e => e.ItemId.Equals(baseItem.Id) && e.ChapterIndex == index);
+        var chapter = context.Chapters
+            .Select(e => new
+            {
+                chapter = e,
+                baseItemPath = e.Item.Path
+            })
+            .FirstOrDefault(e => e.chapter.ItemId.Equals(baseItemId) && e.chapter.ChapterIndex == index);
         if (chapter is not null)
         {
-            return Map(chapter, baseItem);
+            return Map(chapter.chapter, chapter.baseItemPath!);
         }
 
         return null;
     }
 
-    /// <inheritdoc cref="IChapterManager"/>
-    public IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem)
+    /// <inheritdoc cref="IChapterRepository"/>
+    public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
     {
         using var context = _dbProvider.CreateDbContext();
-        return context.Chapters.Where(e => e.ItemId.Equals(baseItem.Id))
+        return context.Chapters.Where(e => e.ItemId.Equals(baseItemId))
+            .Select(e => new
+            {
+                chapter = e,
+                baseItemPath = e.Item.Path
+            })
             .ToList()
-            .Select(e => Map(e, baseItem))
+            .Select(e => Map(e.chapter, e.baseItemPath!))
             .ToImmutableArray();
     }
 
-    /// <inheritdoc cref="IChapterManager"/>
+    /// <inheritdoc cref="IChapterRepository"/>
     public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
     {
         using var context = _dbProvider.CreateDbContext();
@@ -80,20 +103,21 @@ public class ChapterManager : IChapterManager
             ImageDateModified = chapterInfo.ImageDateModified,
             ImagePath = chapterInfo.ImagePath,
             ItemId = itemId,
-            Name = chapterInfo.Name
+            Name = chapterInfo.Name,
+            Item = null!
         };
     }
 
-    private ChapterInfo Map(Chapter chapterInfo, BaseItemDto baseItem)
+    private ChapterInfo Map(Chapter chapterInfo, string baseItemPath)
     {
-        var info = new ChapterInfo()
+        var chapterEntity = new ChapterInfo()
         {
             StartPositionTicks = chapterInfo.StartPositionTicks,
             ImageDateModified = chapterInfo.ImageDateModified.GetValueOrDefault(),
             ImagePath = chapterInfo.ImagePath,
             Name = chapterInfo.Name,
         };
-        info.ImageTag = _imageProcessor.GetImageCacheTag(baseItem, info);
-        return info;
+        chapterEntity.ImageTag = _imageProcessor.GetImageCacheTag(baseItemPath, chapterEntity.ImageDateModified);
+        return chapterEntity;
     }
 }

+ 1 - 1
Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs → Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs

@@ -14,7 +14,7 @@ namespace Jellyfin.Server.Implementations.Item;
 /// Manager for handling Media Attachments.
 /// </summary>
 /// <param name="dbProvider">Efcore Factory.</param>
-public class MediaAttachmentManager(IDbContextFactory<JellyfinDbContext> dbProvider) : IMediaAttachmentManager
+public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbProvider) : IMediaAttachmentRepository
 {
     /// <inheritdoc />
     public void SaveMediaAttachments(

+ 5 - 5
Jellyfin.Server.Implementations/Item/MediaStreamManager.cs → Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs

@@ -13,12 +13,12 @@ using Microsoft.EntityFrameworkCore;
 namespace Jellyfin.Server.Implementations.Item;
 
 /// <summary>
-/// Initializes a new instance of the <see cref="MediaStreamManager"/> class.
+/// Initializes a new instance of the <see cref="MediaStreamRepository"/> class.
 /// </summary>
-/// <param name="dbProvider"></param>
-/// <param name="serverApplicationHost"></param>
-/// <param name="localization"></param>
-public class MediaStreamManager(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) : IMediaStreamManager
+/// <param name="dbProvider">The EFCore db factory.</param>
+/// <param name="serverApplicationHost">The Application host.</param>
+/// <param name="localization">The Localisation Provider.</param>
+public class MediaStreamRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) : IMediaStreamRepository
 {
     /// <inheritdoc />
     public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken)

+ 4 - 3
Jellyfin.Server.Implementations/Item/PeopleManager.cs → Jellyfin.Server.Implementations/Item/PeopleRepository.cs

@@ -16,13 +16,13 @@ namespace Jellyfin.Server.Implementations.Item;
 /// </summary>
 /// <param name="dbProvider">Efcore Factory.</param>
 /// <remarks>
-/// Initializes a new instance of the <see cref="PeopleManager"/> class.
+/// Initializes a new instance of the <see cref="PeopleRepository"/> class.
 /// </remarks>
-/// <param name="dbProvider">The EFCore Context factory.</param>
-public class PeopleManager(IDbContextFactory<JellyfinDbContext> dbProvider) : IPeopleManager
+public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider) : IPeopleRepository
 {
     private readonly IDbContextFactory<JellyfinDbContext> _dbProvider = dbProvider;
 
+    /// <inheritdoc/>
     public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery filter)
     {
         using var context = _dbProvider.CreateDbContext();
@@ -37,6 +37,7 @@ public class PeopleManager(IDbContextFactory<JellyfinDbContext> dbProvider) : IP
         return dbQuery.ToList().Select(Map).ToImmutableArray();
     }
 
+    /// <inheritdoc/>
     public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter)
     {
         using var context = _dbProvider.CreateDbContext();

+ 1 - 1
Jellyfin.Server.Implementations/JellyfinDbContext.cs

@@ -106,7 +106,7 @@ public class JellyfinDbContext : DbContext
     /// <summary>
     /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
     /// </summary>
-    public DbSet<BaseItem> BaseItems => Set<BaseItem>();
+    public DbSet<BaseItemEntity> BaseItems => Set<BaseItemEntity>();
 
     /// <summary>
     /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.

+ 2 - 2
Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs

@@ -8,10 +8,10 @@ namespace Jellyfin.Server.Implementations.ModelConfiguration;
 /// <summary>
 /// Configuration for BaseItem.
 /// </summary>
-public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItem>
+public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
 {
     /// <inheritdoc/>
-    public void Configure(EntityTypeBuilder<BaseItem> builder)
+    public void Configure(EntityTypeBuilder<BaseItemEntity> builder)
     {
         builder.HasNoKey();
         builder.HasIndex(e => e.Path);

+ 1 - 1
Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs

@@ -13,7 +13,7 @@ public class BaseItemProviderConfiguration : IEntityTypeConfiguration<BaseItemPr
     /// <inheritdoc/>
     public void Configure(EntityTypeBuilder<BaseItemProvider> builder)
     {
-        builder.HasNoKey();
+        builder.HasKey(e => new { e.ItemId, e.ProviderId });
         builder.HasOne(e => e.Item);
         builder.HasIndex(e => new { e.ProviderId, e.ProviderValue, e.ItemId });
     }

+ 0 - 24
MediaBrowser.Controller/Chapters/ChapterManager.cs

@@ -1,24 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Chapters;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Providers.Chapters
-{
-    public class ChapterManager : IChapterManager
-    {
-        public ChapterManager(IDbContextFactory<JellyfinDbContext> dbProvider)
-        {
-            _itemRepo = itemRepo;
-        }
-
-        /// <inheritdoc />
-        public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
-        {
-            _itemRepo.SaveChapters(itemId, chapters);
-        }
-    }
-}

+ 0 - 35
MediaBrowser.Controller/Chapters/IChapterManager.cs

@@ -1,35 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.Chapters
-{
-    /// <summary>
-    /// Interface IChapterManager.
-    /// </summary>
-    public interface IChapterManager
-    {
-        /// <summary>
-        /// Saves the chapters.
-        /// </summary>
-        /// <param name="itemId">The item.</param>
-        /// <param name="chapters">The set of chapters.</param>
-        void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
-
-        /// <summary>
-        /// Gets all chapters associated with the baseItem.
-        /// </summary>
-        /// <param name="baseItem">The baseitem.</param>
-        /// <returns>A readonly list of chapter instances.</returns>
-        IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem);
-
-        /// <summary>
-        /// Gets a single chapter of a BaseItem on a specific index.
-        /// </summary>
-        /// <param name="baseItem">The baseitem.</param>
-        /// <param name="index">The index of that chapter.</param>
-        /// <returns>A chapter instance.</returns>
-        ChapterInfo? GetChapter(BaseItemDto baseItem, int index);
-    }
-}

+ 49 - 0
MediaBrowser.Controller/Chapters/IChapterRepository.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.Chapters;
+
+/// <summary>
+/// Interface IChapterManager.
+/// </summary>
+public interface IChapterRepository
+{
+    /// <summary>
+    /// Saves the chapters.
+    /// </summary>
+    /// <param name="itemId">The item.</param>
+    /// <param name="chapters">The set of chapters.</param>
+    void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
+
+    /// <summary>
+    /// Gets all chapters associated with the baseItem.
+    /// </summary>
+    /// <param name="baseItem">The baseitem.</param>
+    /// <returns>A readonly list of chapter instances.</returns>
+    IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem);
+
+    /// <summary>
+    /// Gets a single chapter of a BaseItem on a specific index.
+    /// </summary>
+    /// <param name="baseItem">The baseitem.</param>
+    /// <param name="index">The index of that chapter.</param>
+    /// <returns>A chapter instance.</returns>
+    ChapterInfo? GetChapter(BaseItemDto baseItem, int index);
+
+    /// <summary>
+    /// Gets all chapters associated with the baseItem.
+    /// </summary>
+    /// <param name="baseItemId">The BaseItems id.</param>
+    /// <returns>A readonly list of chapter instances.</returns>
+    IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId);
+
+    /// <summary>
+    /// Gets a single chapter of a BaseItem on a specific index.
+    /// </summary>
+    /// <param name="baseItemId">The BaseItems id.</param>
+    /// <param name="index">The index of that chapter.</param>
+    /// <returns>A chapter instance.</returns>
+    ChapterInfo? GetChapter(Guid baseItemId, int index);
+}

+ 25 - 0
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Controller.Drawing
@@ -57,6 +58,22 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>BlurHash.</returns>
         string GetImageBlurHash(string path, ImageDimensions imageDimensions);
 
+        /// <summary>
+        /// Gets the image cache tag.
+        /// </summary>
+        /// <param name="baseItemPath">The items basePath.</param>
+        /// <param name="imageDateModified">The image last modification date.</param>
+        /// <returns>Guid.</returns>
+        string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified);
+
+        /// <summary>
+        /// Gets the image cache tag.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="image">The image.</param>
+        /// <returns>Guid.</returns>
+        string? GetImageCacheTag(BaseItemDto item, ChapterInfo image);
+
         /// <summary>
         /// Gets the image cache tag.
         /// </summary>
@@ -65,6 +82,14 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>Guid.</returns>
         string GetImageCacheTag(BaseItem item, ItemImageInfo image);
 
+        /// <summary>
+        /// Gets the image cache tag.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="image">The image.</param>
+        /// <returns>Guid.</returns>
+        string GetImageCacheTag(BaseItemDto item, ItemImageInfo image);
+
         string? GetImageCacheTag(BaseItem item, ChapterInfo chapter);
 
         string? GetImageCacheTag(User user);

+ 5 - 2
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -16,6 +16,7 @@ using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities.Audio;
@@ -479,6 +480,8 @@ namespace MediaBrowser.Controller.Entities
 
         public static IItemRepository ItemRepository { get; set; }
 
+        public static IChapterRepository ChapterRepository { get; set; }
+
         public static IFileSystem FileSystem { get; set; }
 
         public static IUserDataManager UserDataManager { get; set; }
@@ -2031,7 +2034,7 @@ namespace MediaBrowser.Controller.Entities
         {
             if (imageType == ImageType.Chapter)
             {
-                var chapter = ItemRepository.GetChapter(this, imageIndex);
+                var chapter = ChapterRepository.GetChapter(this.Id, imageIndex);
 
                 if (chapter is null)
                 {
@@ -2081,7 +2084,7 @@ namespace MediaBrowser.Controller.Entities
 
             if (image.Type == ImageType.Chapter)
             {
-                var chapters = ItemRepository.GetChapters(this);
+                var chapters = ChapterRepository.GetChapters(this.Id);
                 for (var i = 0; i < chapters.Count; i++)
                 {
                     if (chapters[i].ImagePath == image.Path)

+ 0 - 1
MediaBrowser.Controller/Persistence/IItemRepository.cs

@@ -52,7 +52,6 @@ public interface IItemRepository : IDisposable
     /// <returns>List&lt;Guid&gt;.</returns>
     IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter);
 
-
     /// <summary>
     /// Gets the item list.
     /// </summary>

+ 57 - 0
MediaBrowser.Controller/Persistence/IItemTypeLookup.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Model.Querying;
+
+namespace MediaBrowser.Controller.Persistence;
+
+/// <summary>
+/// Provides static lookup data for <see cref="ItemFields"/> and <see cref="BaseItemKind"/> for the domain.
+/// </summary>
+public interface IItemTypeLookup
+{
+    /// <summary>
+    /// Gets all values of the ItemFields type.
+    /// </summary>
+    public IReadOnlyList<ItemFields> AllItemFields { get; }
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are considered Programs.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ProgramTypes { get; }
+
+    /// <summary>
+    /// Gets all BaseItemKinds that should be excluded from parent lookup.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ProgramExcludeParentTypes { get; }
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are considered to be provided by services.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ServiceTypes { get; }
+
+    /// <summary>
+    /// Gets all BaseItemKinds that have a StartDate.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> StartDateTypes { get; }
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are considered Series.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> SeriesTypes { get; }
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are not to be evaluated for Artists.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ArtistExcludeParentTypes { get; }
+
+    /// <summary>
+    /// Gets all BaseItemKinds that are considered Artists.
+    /// </summary>
+    public IReadOnlyList<BaseItemKind> ArtistsTypes { get; }
+
+    /// <summary>
+    /// Gets mapping for all BaseItemKinds and their expected serialisaition target.
+    /// </summary>
+    public IDictionary<BaseItemKind, string?> BaseItemKindNames { get; }
+}

+ 1 - 2
MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs → MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs

@@ -9,9 +9,8 @@ using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Controller.Persistence;
 
-public interface IMediaAttachmentManager
+public interface IMediaAttachmentRepository
 {
-
     /// <summary>
     /// Gets the media attachments.
     /// </summary>

+ 5 - 2
MediaBrowser.Controller/Persistence/IMediaStreamManager.cs → MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs

@@ -9,14 +9,17 @@ using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Controller.Persistence;
 
-public interface IMediaStreamManager
+/// <summary>
+/// Provides methods for accessing MediaStreams.
+/// </summary>
+public interface IMediaStreamRepository
 {
     /// <summary>
     /// Gets the media streams.
     /// </summary>
     /// <param name="filter">The query.</param>
     /// <returns>IEnumerable{MediaStream}.</returns>
-    List<MediaStream> GetMediaStreams(MediaStreamQuery filter);
+    IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter);
 
     /// <summary>
     /// Saves the media streams.

+ 1 - 2
MediaBrowser.Controller/Persistence/IPeopleManager.cs → MediaBrowser.Controller/Persistence/IPeopleRepository.cs

@@ -8,7 +8,7 @@ using MediaBrowser.Controller.Entities;
 
 namespace MediaBrowser.Controller.Persistence;
 
-public interface IPeopleManager
+public interface IPeopleRepository
 {
     /// <summary>
     /// Gets the people.
@@ -30,5 +30,4 @@ public interface IPeopleManager
     /// <param name="filter">The query.</param>
     /// <returns>List&lt;System.String&gt;.</returns>
     IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter);
-
 }

+ 2 - 2
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -38,7 +38,7 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly IEncodingManager _encodingManager;
         private readonly IServerConfigurationManager _config;
         private readonly ISubtitleManager _subtitleManager;
-        private readonly IChapterManager _chapterManager;
+        private readonly IChapterRepository _chapterManager;
         private readonly ILibraryManager _libraryManager;
         private readonly AudioResolver _audioResolver;
         private readonly SubtitleResolver _subtitleResolver;
@@ -54,7 +54,7 @@ namespace MediaBrowser.Providers.MediaInfo
             IEncodingManager encodingManager,
             IServerConfigurationManager config,
             ISubtitleManager subtitleManager,
-            IChapterManager chapterManager,
+            IChapterRepository chapterManager,
             ILibraryManager libraryManager,
             AudioResolver audioResolver,
             SubtitleResolver subtitleResolver)

+ 2 - 2
MediaBrowser.Providers/MediaInfo/ProbeProvider.cs

@@ -61,7 +61,7 @@ namespace MediaBrowser.Providers.MediaInfo
         /// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
         /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
         /// <param name="subtitleManager">Instance of the <see cref="ISubtitleManager"/> interface.</param>
-        /// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param>
+        /// <param name="chapterManager">Instance of the <see cref="IChapterRepository"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/>.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
@@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.MediaInfo
             IEncodingManager encodingManager,
             IServerConfigurationManager config,
             ISubtitleManager subtitleManager,
-            IChapterManager chapterManager,
+            IChapterRepository chapterManager,
             ILibraryManager libraryManager,
             IFileSystem fileSystem,
             ILoggerFactory loggerFactory,

+ 25 - 0
src/Jellyfin.Drawing/ImageProcessor.cs

@@ -15,6 +15,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
@@ -403,10 +404,34 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
         return _imageEncoder.GetImageBlurHash(xComp, yComp, path);
     }
 
+    /// <inheritdoc />
+    public string GetImageCacheTag(string baseItemPath, DateTime imageDateModified)
+        => (baseItemPath + imageDateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+
     /// <inheritdoc />
     public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
         => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
 
+    /// <inheritdoc />
+    public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image)
+        => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+
+    /// <inheritdoc />
+    public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter)
+    {
+        if (chapter.ImagePath is null)
+        {
+            return null;
+        }
+
+        return GetImageCacheTag(item, new ItemImageInfo
+        {
+            Path = chapter.ImagePath,
+            Type = ImageType.Chapter,
+            DateModified = chapter.ImageDateModified
+        });
+    }
+
     /// <inheritdoc />
     public string? GetImageCacheTag(BaseItem item, ChapterInfo chapter)
     {