diff options
Diffstat (limited to 'MediaBrowser.Controller/Persistence')
7 files changed, 418 insertions, 47 deletions
diff --git a/MediaBrowser.Controller/Persistence/IItemCountService.cs b/MediaBrowser.Controller/Persistence/IItemCountService.cs new file mode 100644 index 0000000000..d57f1fc893 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemCountService.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides item counting and played-status query operations. +/// </summary> +public interface IItemCountService +{ + /// <summary> + /// Gets the count of items matching the filter. + /// </summary> + /// <param name="filter">The query filter.</param> + /// <returns>The item count.</returns> + int GetCount(InternalItemsQuery filter); + + /// <summary> + /// Gets item counts grouped by type. + /// </summary> + /// <param name="filter">The query filter.</param> + /// <returns>The item counts by type.</returns> + ItemCounts GetItemCounts(InternalItemsQuery filter); + + /// <summary> + /// Gets item counts for a "by-name" item using an optimized query. + /// </summary> + /// <param name="kind">The kind of the name item.</param> + /// <param name="id">The ID of the name item.</param> + /// <param name="relatedItemKinds">The item kinds to count.</param> + /// <param name="accessFilter">A pre-configured query with user access filtering settings.</param> + /// <returns>The item counts grouped by type.</returns> + ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, InternalItemsQuery accessFilter); + + /// <summary> + /// Gets the count of played items that are descendants of the specified ancestor. + /// </summary> + /// <param name="filter">The query filter containing user access settings.</param> + /// <param name="ancestorId">The ancestor item id.</param> + /// <returns>The count of played descendant items.</returns> + int GetPlayedCount(InternalItemsQuery filter, Guid ancestorId); + + /// <summary> + /// Gets the total count of items that are descendants of the specified ancestor. + /// </summary> + /// <param name="filter">The query filter containing user access settings.</param> + /// <param name="ancestorId">The ancestor item id.</param> + /// <returns>The total count of descendant items.</returns> + int GetTotalCount(InternalItemsQuery filter, Guid ancestorId); + + /// <summary> + /// Gets both the played count and total count of descendant items. + /// </summary> + /// <param name="filter">The query filter containing user access settings.</param> + /// <param name="ancestorId">The ancestor item id.</param> + /// <returns>A tuple containing (Played count, Total count).</returns> + (int Played, int Total) GetPlayedAndTotalCount(InternalItemsQuery filter, Guid ancestorId); + + /// <summary> + /// Gets both the played count and total count from linked children. + /// </summary> + /// <param name="filter">The query filter containing user access settings.</param> + /// <param name="parentId">The parent item id.</param> + /// <returns>A tuple containing (Played count, Total count).</returns> + (int Played, int Total) GetPlayedAndTotalCountFromLinkedChildren(InternalItemsQuery filter, Guid parentId); + + /// <summary> + /// Batch-fetches played and total counts for multiple folder items. + /// </summary> + /// <param name="folderIds">The list of folder item IDs to get counts for.</param> + /// <param name="user">The user for access filtering and played status.</param> + /// <returns>Dictionary mapping folder ID to (Played count, Total count).</returns> + Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user); + + /// <summary> + /// Batch-fetches child counts for multiple parent folders. + /// </summary> + /// <param name="parentIds">The list of parent folder IDs.</param> + /// <param name="userId">The user ID for access filtering.</param> + /// <returns>Dictionary mapping parent ID to child count.</returns> + Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId); +} diff --git a/MediaBrowser.Controller/Persistence/IItemPersistenceService.cs b/MediaBrowser.Controller/Persistence/IItemPersistenceService.cs new file mode 100644 index 0000000000..37f7194e7a --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemPersistenceService.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides item persistence operations (save, delete, update). +/// </summary> +public interface IItemPersistenceService +{ + /// <summary> + /// Deletes items by their IDs. + /// </summary> + /// <param name="ids">The IDs to delete.</param> + void DeleteItem(params IReadOnlyList<Guid> ids); + + /// <summary> + /// Saves items to the database. + /// </summary> + /// <param name="items">The items to save.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); + + /// <summary> + /// Saves image info for an item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>A task representing the asynchronous operation.</returns> + Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); + + /// <summary> + /// Reattaches user data entries to the correct item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>A task representing the asynchronous operation.</returns> + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + + /// <summary> + /// Updates inherited values. + /// </summary> + void UpdateInheritedValues(); +} diff --git a/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs b/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs new file mode 100644 index 0000000000..2e29cbdbba --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemQueryHelpers.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides shared query-building methods used by extracted item services. +/// Implemented by <c>BaseItemRepository</c>. +/// </summary> +public interface IItemQueryHelpers +{ + /// <summary> + /// Translates an <see cref="InternalItemsQuery"/> into EF Core filter expressions. + /// </summary> + /// <param name="baseQuery">The base queryable to filter.</param> + /// <param name="context">The database context.</param> + /// <param name="filter">The query filter.</param> + /// <returns>The filtered queryable.</returns> + IQueryable<BaseItemEntity> TranslateQuery( + IQueryable<BaseItemEntity> baseQuery, + JellyfinDbContext context, + InternalItemsQuery filter); + + /// <summary> + /// Prepares a base query for items from the context. + /// </summary> + /// <param name="context">The database context.</param> + /// <param name="filter">The query filter.</param> + /// <returns>The prepared queryable.</returns> + IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter); + + /// <summary> + /// Applies user access filtering (library access, parental controls, tags) to a query. + /// </summary> + /// <param name="context">The database context.</param> + /// <param name="baseQuery">The base queryable to filter.</param> + /// <param name="filter">The query filter containing access settings.</param> + /// <returns>The access-filtered queryable.</returns> + IQueryable<BaseItemEntity> ApplyAccessFiltering( + JellyfinDbContext context, + IQueryable<BaseItemEntity> baseQuery, + InternalItemsQuery filter); + + /// <summary> + /// Applies navigation property includes to a query based on filter options. + /// </summary> + /// <param name="dbQuery">The queryable to apply navigations to.</param> + /// <param name="filter">The query filter.</param> + /// <returns>The queryable with navigation includes.</returns> + IQueryable<BaseItemEntity> ApplyNavigations( + IQueryable<BaseItemEntity> dbQuery, + InternalItemsQuery filter); + + /// <summary> + /// Applies ordering to a query based on filter options. + /// </summary> + /// <param name="query">The queryable to order.</param> + /// <param name="filter">The query filter.</param> + /// <param name="context">The database context.</param> + /// <returns>The ordered queryable.</returns> + IQueryable<BaseItemEntity> ApplyOrder( + IQueryable<BaseItemEntity> query, + InternalItemsQuery filter, + JellyfinDbContext context); + + /// <summary> + /// Builds a query for descendants of an ancestor with user access filtering applied. + /// </summary> + /// <param name="context">The database context.</param> + /// <param name="filter">The query filter.</param> + /// <param name="ancestorId">The ancestor item ID.</param> + /// <returns>The filtered descendant queryable.</returns> + IQueryable<BaseItemEntity> BuildAccessFilteredDescendantsQuery( + JellyfinDbContext context, + InternalItemsQuery filter, + Guid ancestorId); + + /// <summary> + /// Builds an <see cref="IQueryable{Guid}"/> of folder IDs whose descendants are all played + /// for the given user. Composable into outer queries to avoid an extra DB roundtrip. + /// </summary> + /// <param name="context">The database context the resulting query is bound to.</param> + /// <param name="folderIds">A query yielding candidate folder IDs.</param> + /// <param name="user">The user for access filtering and played status.</param> + /// <returns>An <see cref="IQueryable{Guid}"/> of fully-played folder IDs.</returns> + IQueryable<Guid> GetFullyPlayedFolderIdsQuery( + JellyfinDbContext context, + IQueryable<Guid> folderIds, + User user); + + /// <summary> + /// Deserializes a <see cref="BaseItemEntity"/> into a <see cref="BaseItem"/>. + /// </summary> + /// <param name="entity">The database entity.</param> + /// <param name="skipDeserialization">Whether to skip JSON deserialization.</param> + /// <returns>The deserialized item, or null.</returns> + BaseItem? DeserializeBaseItem(BaseItemEntity entity, bool skipDeserialization = false); + + /// <summary> + /// Prepares a filter query by adjusting limits and virtual item settings. + /// </summary> + /// <param name="query">The query to prepare.</param> + void PrepareFilterQuery(InternalItemsQuery query); +} diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 00c492742a..291916ab25 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -1,15 +1,11 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -21,21 +17,6 @@ namespace MediaBrowser.Controller.Persistence; public interface IItemRepository { /// <summary> - /// Deletes the item. - /// </summary> - /// <param name="ids">The identifier to delete.</param> - void DeleteItem(params IReadOnlyList<Guid> ids); - - /// <summary> - /// Saves the items. - /// </summary> - /// <param name="items">The items.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); - - Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); - - /// <summary> /// Retrieves the item. /// </summary> /// <param name="id">The id.</param> @@ -72,62 +53,91 @@ public interface IItemRepository IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType); /// <summary> - /// Gets the list of series presentation keys for next up. + /// Checks if an item has been persisted to the database. /// </summary> - /// <param name="filter">The query.</param> - /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param> - /// <returns>The list of keys.</returns> - IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff); + /// <param name="id">The id to check.</param> + /// <returns>True if the item exists, otherwise false.</returns> + Task<bool> ItemExistsAsync(Guid id); /// <summary> - /// Updates the inherited values. + /// Gets genres with item counts. /// </summary> - void UpdateInheritedValues(); - - int GetCount(InternalItemsQuery filter); - - ItemCounts GetItemCounts(InternalItemsQuery filter); - + /// <param name="filter">The query filter.</param> + /// <returns>The genres and their item counts.</returns> QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter); + /// <summary> + /// Gets music genres with item counts. + /// </summary> + /// <param name="filter">The query filter.</param> + /// <returns>The music genres and their item counts.</returns> QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter); + /// <summary> + /// Gets studios with item counts. + /// </summary> + /// <param name="filter">The query filter.</param> + /// <returns>The studios and their item counts.</returns> QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter); + /// <summary> + /// Gets artists with item counts. + /// </summary> + /// <param name="filter">The query filter.</param> + /// <returns>The artists and their item counts.</returns> QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter); + /// <summary> + /// Gets album artists with item counts. + /// </summary> + /// <param name="filter">The query filter.</param> + /// <returns>The album artists and their item counts.</returns> QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter); + /// <summary> + /// Gets all artists with item counts. + /// </summary> + /// <param name="filter">The query filter.</param> + /// <returns>All artists and their item counts.</returns> QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter); + /// <summary> + /// Gets all music genre names. + /// </summary> + /// <returns>The list of music genre names.</returns> IReadOnlyList<string> GetMusicGenreNames(); + /// <summary> + /// Gets all studio names. + /// </summary> + /// <returns>The list of studio names.</returns> IReadOnlyList<string> GetStudioNames(); + /// <summary> + /// Gets all genre names. + /// </summary> + /// <returns>The list of genre names.</returns> IReadOnlyList<string> GetGenreNames(); - IReadOnlyList<string> GetAllArtistNames(); - /// <summary> - /// Checks if an item has been persisted to the database. + /// Gets all artist names. /// </summary> - /// <param name="id">The id to check.</param> - /// <returns>True if the item exists, otherwise false.</returns> - Task<bool> ItemExistsAsync(Guid id); + /// <returns>The list of artist names.</returns> + IReadOnlyList<string> GetAllArtistNames(); /// <summary> - /// Gets a value indicating wherever all children of the requested Id has been played. + /// Gets legacy query filters aggregated from the database. /// </summary> - /// <param name="user">The userdata to check against.</param> - /// <param name="id">The Top id to check.</param> - /// <param name="recursive">Whever the check should be done recursive. Warning expensive operation.</param> - /// <returns>A value indicating whever all children has been played.</returns> - bool GetIsPlayed(User user, Guid id, bool recursive); + /// <param name="filter">The query filter.</param> + /// <returns>Aggregated filter values.</returns> + QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery filter); /// <summary> - /// Gets all artist matches from the db. + /// Gets whether all children of the requested item have been played. /// </summary> - /// <param name="artistNames">The names of the artists.</param> - /// <returns>A map of the artist name and the potential matches.</returns> - IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames); + /// <param name="user">The user to check against.</param> + /// <param name="id">The top item id to check.</param> + /// <param name="recursive">Whether the check should be done recursively.</param> + /// <returns>A value indicating whether all children have been played.</returns> + bool GetIsPlayed(User user, Guid id, bool recursive); } diff --git a/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs b/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs new file mode 100644 index 0000000000..d0cddf54a6 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/ILinkedChildrenService.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities.Audio; +using LinkedChildType = MediaBrowser.Controller.Entities.LinkedChildType; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides linked children query and manipulation operations. +/// </summary> +public interface ILinkedChildrenService +{ + /// <summary> + /// Gets the IDs of linked children for the specified parent. + /// </summary> + /// <param name="parentId">The parent item ID.</param> + /// <param name="childType">Optional child type filter.</param> + /// <returns>List of child item IDs.</returns> + IReadOnlyList<Guid> GetLinkedChildrenIds(Guid parentId, int? childType = null); + + /// <summary> + /// Gets all artist matches from the database. + /// </summary> + /// <param name="artistNames">The names of the artists.</param> + /// <returns>A map of the artist name and the potential matches.</returns> + IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames); + + /// <summary> + /// Gets parent IDs that reference the specified child with LinkedChildType.Manual. + /// </summary> + /// <param name="childId">The child item ID.</param> + /// <returns>List of parent IDs that reference the child.</returns> + IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId); + + /// <summary> + /// Updates LinkedChildren references from one child to another. + /// </summary> + /// <param name="fromChildId">The child ID to re-route from.</param> + /// <param name="toChildId">The child ID to re-route to.</param> + /// <returns>List of parent item IDs whose LinkedChildren were modified.</returns> + IReadOnlyList<Guid> RerouteLinkedChildren(Guid fromChildId, Guid toChildId); + + /// <summary> + /// Creates or updates a LinkedChild entry. + /// </summary> + /// <param name="parentId">The parent item ID.</param> + /// <param name="childId">The child item ID.</param> + /// <param name="childType">The type of linked child relationship.</param> + void UpsertLinkedChild(Guid parentId, Guid childId, LinkedChildType childType); +} diff --git a/MediaBrowser.Controller/Persistence/INextUpService.cs b/MediaBrowser.Controller/Persistence/INextUpService.cs new file mode 100644 index 0000000000..ade026d0da --- /dev/null +++ b/MediaBrowser.Controller/Persistence/INextUpService.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides next-up episode query operations. +/// </summary> +public interface INextUpService +{ + /// <summary> + /// Gets the list of series presentation keys for next up. + /// </summary> + /// <param name="filter">The query.</param> + /// <param name="dateCutoff">The minimum date for a series to have been most recently watched.</param> + /// <returns>The list of keys.</returns> + IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff); + + /// <summary> + /// Gets next up episodes for multiple series in a single batched query. + /// </summary> + /// <param name="filter">The query filter.</param> + /// <param name="seriesKeys">The series presentation unique keys to query.</param> + /// <param name="includeSpecials">Whether to include specials.</param> + /// <param name="includeWatchedForRewatching">Whether to include watched episodes for rewatching mode.</param> + /// <returns>A dictionary mapping series key to batch result.</returns> + IReadOnlyDictionary<string, NextUpEpisodeBatchResult> GetNextUpEpisodesBatch( + InternalItemsQuery filter, + IReadOnlyList<string> seriesKeys, + bool includeSpecials, + bool includeWatchedForRewatching); +} diff --git a/MediaBrowser.Controller/Persistence/NextUpEpisodeBatchResult.cs b/MediaBrowser.Controller/Persistence/NextUpEpisodeBatchResult.cs new file mode 100644 index 0000000000..f5b09498b9 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/NextUpEpisodeBatchResult.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Result of a batched NextUp query for a single series. +/// </summary> +public sealed class NextUpEpisodeBatchResult +{ + /// <summary> + /// Gets or sets the last watched episode (highest season/episode that is played). + /// </summary> + public BaseItem? LastWatched { get; set; } + + /// <summary> + /// Gets or sets the next unwatched episode after the last watched position. + /// </summary> + public BaseItem? NextUp { get; set; } + + /// <summary> + /// Gets or sets specials that may air between episodes. + /// Only populated when includeSpecials is true. + /// </summary> + public IReadOnlyList<BaseItem>? Specials { get; set; } + + /// <summary> + /// Gets or sets the last watched episode for rewatching mode (most recently played). + /// Only populated when includeWatchedForRewatching is true. + /// </summary> + public BaseItem? LastWatchedForRewatching { get; set; } + + /// <summary> + /// Gets or sets the next played episode for rewatching mode. + /// Only populated when includeWatchedForRewatching is true. + /// </summary> + public BaseItem? NextPlayedForRewatching { get; set; } +} |
