aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-01-17 17:10:07 +0100
committerShadowghost <Ghost_of_Stone@web.de>2026-01-18 19:48:46 +0100
commit5996c4afce11249804d24f1caa3a99b390543c4d (patch)
treed84b98428d95c801492b1354571e2ab3fc0cc99b /MediaBrowser.Controller
parentdfa78590c2899c7e74b142ebbced4140a354aed0 (diff)
Complete LinkedChildren integration and batch DTO optimizations
This commit integrates remaining performance changes: - Add batch user data fetching in DtoService to reduce N+1 queries - Add GetNextUpEpisodesBatch in TVSeriesManager for efficient batch retrieval - Update Video/Movie/BoxSet to use LibraryManager for alternate versions - Transition LinkedChild to use ItemId instead of Path (obsolete Path/LibraryItemId) - Update providers and controllers for LinkedChildren-based references - Add NextUpEpisodeBatchResult for batched episode queries - Integrate IDescendantQueryProvider in SqliteDatabaseProvider
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Dto/IDtoService.cs3
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs19
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs7
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs201
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs4
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChild.cs20
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChildComparer.cs23
-rw-r--r--MediaBrowser.Controller/Entities/LinkedChildType.cs12
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs36
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs4
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs4
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs243
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs69
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs47
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs72
-rw-r--r--MediaBrowser.Controller/Persistence/NextUpEpisodeBatchResult.cs38
17 files changed, 397 insertions, 409 deletions
diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs
index f1d507fcbd..8dc6f0aa68 100644
--- a/MediaBrowser.Controller/Dto/IDtoService.cs
+++ b/MediaBrowser.Controller/Dto/IDtoService.cs
@@ -36,8 +36,9 @@ namespace MediaBrowser.Controller.Dto
/// <param name="options">The options.</param>
/// <param name="user">The user.</param>
/// <param name="owner">The owner.</param>
+ /// <param name="skipVisibilityCheck">Skip redundant visibility check if items are already filtered.</param>
/// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns>
- IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null);
+ IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null, bool skipVisibilityCheck = false);
/// <summary>
/// Gets the item by name dto.
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index eb7daeb532..13af7a6178 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1806,10 +1806,23 @@ namespace MediaBrowser.Controller.Entities
return item;
}
+#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy LinkedChild data
private BaseItem FindLinkedChild(LinkedChild info)
{
- var path = info.Path;
+ // First try to find by ItemId (new preferred method)
+ if (info.ItemId.HasValue && !info.ItemId.Value.Equals(Guid.Empty))
+ {
+ var item = LibraryManager.GetItemById(info.ItemId.Value);
+ if (item is not null)
+ {
+ return item;
+ }
+ Logger.LogWarning("Unable to find linked item by ItemId {0}", info.ItemId);
+ }
+
+ // Fall back to Path (legacy method)
+ var path = info.Path;
if (!string.IsNullOrEmpty(path))
{
path = FileSystem.MakeAbsolutePath(ContainingFolderPath, path);
@@ -1824,13 +1837,14 @@ namespace MediaBrowser.Controller.Entities
return itemByPath;
}
+ // Fall back to LibraryItemId (legacy method)
if (!string.IsNullOrEmpty(info.LibraryItemId))
{
var item = LibraryManager.GetItemById(info.LibraryItemId);
if (item is null)
{
- Logger.LogWarning("Unable to find linked item at path {0}", info.Path);
+ Logger.LogWarning("Unable to find linked item by LibraryItemId {0}", info.LibraryItemId);
}
return item;
@@ -1838,6 +1852,7 @@ namespace MediaBrowser.Controller.Entities
return null;
}
+#pragma warning restore CS0618
/// <summary>
/// Adds a studio to the item.
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index ca79e62454..cf615788ee 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -45,6 +45,11 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
+ /// Event raised when library options are updated for any collection folder.
+ /// </summary>
+ public static event EventHandler<LibraryOptionsUpdatedEventArgs> LibraryOptionsUpdated;
+
+ /// <summary>
/// Gets the display preferences id.
/// </summary>
/// <remarks>
@@ -168,6 +173,8 @@ namespace MediaBrowser.Controller.Entities
}
XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
+
+ LibraryOptionsUpdated?.Invoke(null, new LibraryOptionsUpdatedEventArgs(path, options));
}
public static void OnCollectionFolderChange()
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index d2a3290c47..6338e54292 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -59,6 +59,10 @@ namespace MediaBrowser.Controller.Entities
/// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
public bool IsRoot { get; set; }
+ /// <summary>
+ /// Gets or sets the linked children.
+ /// </summary>
+ [JsonIgnore]
public LinkedChild[] LinkedChildren { get; set; }
[JsonIgnore]
@@ -455,6 +459,14 @@ namespace MediaBrowser.Controller.Entities
// If it's an AggregateFolder, don't remove
if (shouldRemove && itemsRemoved.Count > 0)
{
+ // Build a set of paths that are alternate versions of valid children
+ // These items should not be deleted - they're managed by their primary video
+ var alternateVersionPaths = validChildren
+ .OfType<Video>()
+ .SelectMany(v => v.LocalAlternateVersions ?? [])
+ .Where(p => !string.IsNullOrEmpty(p))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
foreach (var item in itemsRemoved)
{
if (!item.CanDelete())
@@ -463,6 +475,24 @@ namespace MediaBrowser.Controller.Entities
continue;
}
+ // Skip items that are alternate versions of another video
+ if (item is Video video)
+ {
+ // Check via PrimaryVersionId
+ if (!string.IsNullOrEmpty(video.PrimaryVersionId))
+ {
+ Logger.LogDebug("Item is an alternate version (via PrimaryVersionId), skipping deletion: {Path}", item.Path ?? item.Name);
+ continue;
+ }
+
+ // Check if path is in LocalAlternateVersions of any valid child
+ if (!string.IsNullOrEmpty(item.Path) && alternateVersionPaths.Contains(item.Path))
+ {
+ Logger.LogDebug("Item path matches an alternate version, skipping deletion: {Path}", item.Path);
+ continue;
+ }
+ }
+
if (item.IsFileProtocol)
{
Logger.LogDebug("Removed item: {Path}", item.Path);
@@ -806,104 +836,12 @@ namespace MediaBrowser.Controller.Entities
private bool RequiresPostFiltering(InternalItemsQuery query)
{
- if (LinkedChildren.Length > 0)
- {
- if (this is not ICollectionFolder)
- {
- Logger.LogDebug("{Type}: Query requires post-filtering due to LinkedChildren.", GetType().Name);
- return true;
- }
- }
-
- // Filter by Video3DFormat
- if (query.Is3D.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to Is3D");
- return true;
- }
-
- if (query.HasOfficialRating.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to HasOfficialRating");
- return true;
- }
-
- if (query.IsPlaceHolder.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to IsPlaceHolder");
- return true;
- }
-
- if (query.HasSpecialFeature.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to HasSpecialFeature");
- return true;
- }
-
- if (query.HasSubtitles.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to HasSubtitles");
- return true;
- }
-
- if (query.HasTrailer.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to HasTrailer");
- return true;
- }
-
- if (query.HasThemeSong.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to HasThemeSong");
- return true;
- }
-
- if (query.HasThemeVideo.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to HasThemeVideo");
- return true;
- }
-
- // Filter by VideoType
- if (query.VideoTypes.Length > 0)
- {
- Logger.LogDebug("Query requires post-filtering due to VideoTypes");
- return true;
- }
-
if (CollapseBoxSetItems(query, this, query.User, ConfigurationManager))
{
Logger.LogDebug("Query requires post-filtering due to CollapseBoxSetItems");
return true;
}
- if (!query.AdjacentTo.IsNullOrEmpty())
- {
- Logger.LogDebug("Query requires post-filtering due to AdjacentTo");
- return true;
- }
-
- if (query.SeriesStatuses.Length > 0)
- {
- Logger.LogDebug("Query requires post-filtering due to SeriesStatuses");
- return true;
- }
-
- if (query.AiredDuringSeason.HasValue)
- {
- Logger.LogDebug("Query requires post-filtering due to AiredDuringSeason");
- return true;
- }
-
- if (query.IsPlayed.HasValue)
- {
- if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(BaseItemKind.Series))
- {
- Logger.LogDebug("Query requires post-filtering due to IsPlayed");
- return true;
- }
- }
-
return false;
}
@@ -1012,29 +950,6 @@ namespace MediaBrowser.Controller.Entities
items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
}
-#pragma warning disable CA1309
- if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
- {
- items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1);
- }
-
- if (!string.IsNullOrEmpty(query.NameStartsWith))
- {
- items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase));
- }
-
- if (!string.IsNullOrEmpty(query.NameLessThan))
- {
- items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1);
- }
-#pragma warning restore CA1309
-
- // This must be the last filter
- if (!query.AdjacentTo.IsNullOrEmpty())
- {
- items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
- }
-
var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
@@ -1664,11 +1579,13 @@ namespace MediaBrowser.Controller.Entities
if (!string.IsNullOrEmpty(resolvedPath))
{
+#pragma warning disable CS0618 // Type or member is obsolete - shortcuts require Path for lazy ItemId resolution
return new LinkedChild
{
Path = resolvedPath,
Type = LinkedChildType.Shortcut
};
+#pragma warning restore CS0618
}
Logger.LogError("Error resolving shortcut {0}", i.FullName);
@@ -1786,38 +1703,42 @@ namespace MediaBrowser.Controller.Entities
return;
}
- if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
- {
- itemDto.RecursiveItemCount = GetRecursiveChildCount(user);
- }
-
- if (SupportsPlayedStatus)
+ if (SupportsPlayedStatus || (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount)))
{
- var unplayedQueryResult = GetItems(new InternalItemsQuery(user)
- {
- Recursive = true,
- IsFolder = false,
- IsVirtualItem = false,
- EnableTotalRecordCount = true,
- Limit = 0,
- IsPlayed = false,
- DtoOptions = new DtoOptions(false)
- {
- EnableImages = false
- }
- }).TotalRecordCount;
+ var query = new InternalItemsQuery(user);
+ LibraryManager.ConfigureUserAccess(query, user);
- dto.UnplayedItemCount = unplayedQueryResult;
+ int playedCount;
+ int totalCount;
- if (itemDto?.RecursiveItemCount > 0)
+ if (LinkedChildren.Length > 0)
{
- var unplayedPercentage = ((double)unplayedQueryResult / itemDto.RecursiveItemCount.Value) * 100;
- dto.PlayedPercentage = 100 - unplayedPercentage;
- dto.Played = dto.PlayedPercentage.Value >= 100;
+ (playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCountFromLinkedChildren(query, Id);
}
else
{
- dto.Played = (dto.UnplayedItemCount ?? 0) == 0;
+ (playedCount, totalCount) = ItemRepository.GetPlayedAndTotalCount(query, Id);
+ }
+
+ if (itemDto is not null && fields.ContainsField(ItemFields.RecursiveItemCount))
+ {
+ itemDto.RecursiveItemCount = totalCount;
+ }
+
+ if (SupportsPlayedStatus)
+ {
+ var unplayedCount = totalCount - playedCount;
+ dto.UnplayedItemCount = unplayedCount;
+
+ if (totalCount > 0)
+ {
+ dto.PlayedPercentage = playedCount / (double)totalCount * 100;
+ dto.Played = playedCount >= totalCount;
+ }
+ else
+ {
+ dto.Played = true;
+ }
}
}
}
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 24fe3bb32d..b36ea627d8 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -33,6 +33,7 @@ namespace MediaBrowser.Controller.Entities
ExcludeItemIds = Array.Empty<Guid>();
ExcludeItemTypes = Array.Empty<BaseItemKind>();
ExcludeTags = Array.Empty<string>();
+ ExtraTypes = Array.Empty<ExtraType>();
GenreIds = Array.Empty<Guid>();
Genres = Array.Empty<string>();
GroupByPresentationUniqueKey = true;
@@ -44,6 +45,7 @@ namespace MediaBrowser.Controller.Entities
MediaTypes = Array.Empty<MediaType>();
OfficialRatings = Array.Empty<string>();
OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
+ OwnerIds = Array.Empty<Guid>();
PersonIds = Array.Empty<Guid>();
PersonTypes = Array.Empty<string>();
PresetViews = Array.Empty<CollectionType?>();
@@ -369,6 +371,8 @@ namespace MediaBrowser.Controller.Entities
public bool SkipDeserialization { get; set; }
+ public bool IncludeExtras { get; set; }
+
public void SetUser(User user)
{
var maxRating = user.MaxParentalRatingScore;
diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs
index 98e4f525f5..a3aa9dd0c9 100644
--- a/MediaBrowser.Controller/Entities/LinkedChild.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChild.cs
@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
-using System.Globalization;
namespace MediaBrowser.Controller.Entities
{
@@ -13,10 +12,18 @@ namespace MediaBrowser.Controller.Entities
{
}
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ [Obsolete("Use ItemId instead")]
public string Path { get; set; }
public LinkedChildType Type { get; set; }
+ /// <summary>
+ /// Gets or sets the library item id.
+ /// </summary>
+ [Obsolete("Use ItemId instead")]
public string LibraryItemId { get; set; }
/// <summary>
@@ -28,18 +35,11 @@ namespace MediaBrowser.Controller.Entities
{
ArgumentNullException.ThrowIfNull(item);
- var child = new LinkedChild
+ return new LinkedChild
{
- Path = item.Path,
+ ItemId = item.Id,
Type = LinkedChildType.Manual
};
-
- if (string.IsNullOrEmpty(child.Path))
- {
- child.LibraryItemId = item.Id.ToString("N", CultureInfo.InvariantCulture);
- }
-
- return child;
}
}
}
diff --git a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
index 4f13ac61fe..8b611345f4 100644
--- a/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChildComparer.cs
@@ -19,17 +19,34 @@ namespace MediaBrowser.Controller.Entities
public bool Equals(LinkedChild x, LinkedChild y)
{
- if (x.Type == y.Type)
+ if (x.Type != y.Type)
{
- return _fileSystem.AreEqual(x.Path, y.Path);
+ return false;
}
- return false;
+ // Compare by ItemId first (preferred)
+ if (x.ItemId.HasValue && y.ItemId.HasValue)
+ {
+ return x.ItemId.Value.Equals(y.ItemId.Value);
+ }
+
+#pragma warning disable CS0618 // Type or member is obsolete - fallback for shortcut/legacy comparison
+ // Fall back to Path comparison for shortcuts and legacy data
+ return _fileSystem.AreEqual(x.Path, y.Path);
+#pragma warning restore CS0618
}
public int GetHashCode(LinkedChild obj)
{
+ // Use ItemId for hash if available, otherwise fall back to legacy fields
+ if (obj.ItemId.HasValue && !obj.ItemId.Value.Equals(Guid.Empty))
+ {
+ return HashCode.Combine(obj.ItemId.Value, obj.Type);
+ }
+
+#pragma warning disable CS0618 // Type or member is obsolete - fallback for shortcut/legacy hashing
return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal);
+#pragma warning restore CS0618
}
}
}
diff --git a/MediaBrowser.Controller/Entities/LinkedChildType.cs b/MediaBrowser.Controller/Entities/LinkedChildType.cs
index 3bd260a102..5ce66a561f 100644
--- a/MediaBrowser.Controller/Entities/LinkedChildType.cs
+++ b/MediaBrowser.Controller/Entities/LinkedChildType.cs
@@ -13,6 +13,16 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Shortcut linked child.
/// </summary>
- Shortcut = 1
+ Shortcut = 1,
+
+ /// <summary>
+ /// Local alternate version (same item, different file path).
+ /// </summary>
+ LocalAlternateVersion = 2,
+
+ /// <summary>
+ /// Linked alternate version (different item ID).
+ /// </summary>
+ LinkedAlternateVersion = 3
}
}
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 3999c3e076..c55a70a67b 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -37,9 +37,7 @@ namespace MediaBrowser.Controller.Entities.Movies
/// <inheritdoc />
[JsonIgnore]
- public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
- .ToArray();
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras([Model.Entities.ExtraType.Trailer]).ToArray();
/// <summary>
/// Gets or sets the display order.
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index 710b05e7f9..797f44e2d5 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -4,13 +4,13 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
namespace MediaBrowser.Controller.Entities.Movies
{
@@ -28,9 +28,7 @@ namespace MediaBrowser.Controller.Entities.Movies
/// <inheritdoc />
[JsonIgnore]
- public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
- .ToArray();
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras([Model.Entities.ExtraType.Trailer]).ToArray();
/// <summary>
/// Gets or sets the name of the TMDb collection.
@@ -85,6 +83,34 @@ namespace MediaBrowser.Controller.Entities.Movies
return info;
}
+ protected override async Task RefreshMetadataForVersions(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
+ {
+ var newOptions = new MetadataRefreshOptions(options)
+ {
+ SearchResult = null
+ };
+
+ var id = LibraryManager.GetNewItemId(path, typeof(Movie));
+ if (LibraryManager.GetItemById(id) is not Movie movie)
+ {
+ movie = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Movie;
+
+ newOptions.ForceSave = true;
+ }
+
+ if (movie is null)
+ {
+ return;
+ }
+
+ if (movie.OwnerId.Equals(Guid.Empty))
+ {
+ movie.OwnerId = Id;
+ }
+
+ await RefreshMetadataForOwnedItem(movie, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false);
+ }
+
/// <inheritdoc />
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 6bdba36f9c..dbe6f94dfd 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -28,9 +28,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <inheritdoc />
[JsonIgnore]
- public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
- .ToArray();
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras([Model.Entities.ExtraType.Trailer]).ToArray();
/// <summary>
/// Gets or sets the season in which it aired.
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 6396631f99..ca9ac3f047 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -52,9 +52,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <inheritdoc />
[JsonIgnore]
- public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
- .Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
- .ToArray();
+ public IReadOnlyList<BaseItem> LocalTrailers => GetExtras([Model.Entities.ExtraType.Trailer]).ToArray();
/// <summary>
/// Gets or sets the display order.
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index bed7554b19..47d732c745 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -16,9 +16,7 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace MediaBrowser.Controller.Entities
{
@@ -140,7 +138,7 @@ namespace MediaBrowser.Controller.Entities
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[] { BaseItemKind.Movie };
+ query.IncludeItemTypes = [BaseItemKind.Movie];
}
return parent.QueryRecursive(query);
@@ -165,7 +163,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { BaseItemKind.Movie };
+ query.IncludeItemTypes = [BaseItemKind.Movie];
return _libraryManager.GetItemsResult(query);
}
@@ -176,7 +174,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { BaseItemKind.Series };
+ query.IncludeItemTypes = [BaseItemKind.Series];
return _libraryManager.GetItemsResult(query);
}
@@ -187,7 +185,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
query.IsFavorite = true;
- query.IncludeItemTypes = new[] { BaseItemKind.Episode };
+ query.IncludeItemTypes = [BaseItemKind.Episode];
return _libraryManager.GetItemsResult(query);
}
@@ -198,7 +196,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { BaseItemKind.Movie };
+ query.IncludeItemTypes = [BaseItemKind.Movie];
return _libraryManager.GetItemsResult(query);
}
@@ -206,7 +204,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetMovieCollections(User user, InternalItemsQuery query)
{
query.Parent = null;
- query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
+ query.IncludeItemTypes = [BaseItemKind.BoxSet];
query.SetUser(user);
query.Recursive = true;
@@ -215,25 +213,25 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetMovieLatest(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
+ query.OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending)];
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { BaseItemKind.Movie };
+ query.IncludeItemTypes = [BaseItemKind.Movie];
return ConvertToResult(_libraryManager.GetItemList(query));
}
private QueryResult<BaseItem> GetMovieResume(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
+ query.OrderBy = [(ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending)];
query.IsResumable = true;
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { BaseItemKind.Movie };
+ query.IncludeItemTypes = [BaseItemKind.Movie];
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -247,7 +245,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { BaseItemKind.Movie },
+ IncludeItemTypes = [BaseItemKind.Movie],
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -275,10 +273,10 @@ namespace MediaBrowser.Controller.Entities
{
query.Recursive = true;
query.Parent = queryParent;
- query.GenreIds = new[] { displayParent.Id };
+ query.GenreIds = [displayParent.Id];
query.SetUser(user);
- query.IncludeItemTypes = new[] { BaseItemKind.Movie };
+ query.IncludeItemTypes = [BaseItemKind.Movie];
return _libraryManager.GetItemsResult(query);
}
@@ -292,12 +290,12 @@ namespace MediaBrowser.Controller.Entities
if (query.IncludeItemTypes.Length == 0)
{
- query.IncludeItemTypes = new[]
- {
+ query.IncludeItemTypes =
+ [
BaseItemKind.Series,
BaseItemKind.Season,
BaseItemKind.Episode
- };
+ ];
}
return parent.QueryRecursive(query);
@@ -319,12 +317,12 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
+ query.OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending)];
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { BaseItemKind.Episode };
+ query.IncludeItemTypes = [BaseItemKind.Episode];
query.IsVirtualItem = false;
return ConvertToResult(_libraryManager.GetItemList(query));
@@ -332,7 +330,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query)
{
- var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.tvshows });
+ var parentFolders = GetMediaFolders(parent, query.User, [CollectionType.tvshows]);
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@@ -349,13 +347,13 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetTvResume(Folder parent, User user, InternalItemsQuery query)
{
- query.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending) };
+ query.OrderBy = [(ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.SortName, SortOrder.Descending)];
query.IsResumable = true;
query.Recursive = true;
query.Parent = parent;
query.SetUser(user);
query.Limit = GetSpecialItemsLimit();
- query.IncludeItemTypes = new[] { BaseItemKind.Episode };
+ query.IncludeItemTypes = [BaseItemKind.Episode];
return ConvertToResult(_libraryManager.GetItemList(query));
}
@@ -366,7 +364,7 @@ namespace MediaBrowser.Controller.Entities
query.Parent = parent;
query.SetUser(user);
- query.IncludeItemTypes = new[] { BaseItemKind.Series };
+ query.IncludeItemTypes = [BaseItemKind.Series];
return _libraryManager.GetItemsResult(query);
}
@@ -375,7 +373,7 @@ namespace MediaBrowser.Controller.Entities
{
var genres = parent.QueryRecursive(new InternalItemsQuery(user)
{
- IncludeItemTypes = new[] { BaseItemKind.Series },
+ IncludeItemTypes = [BaseItemKind.Series],
Recursive = true,
EnableTotalRecordCount = false
}).Items
@@ -403,10 +401,10 @@ namespace MediaBrowser.Controller.Entities
{
query.Recursive = true;
query.Parent = queryParent;
- query.GenreIds = new[] { displayParent.Id };
+ query.GenreIds = [displayParent.Id];
query.SetUser(user);
- query.IncludeItemTypes = new[] { BaseItemKind.Series };
+ query.IncludeItemTypes = [BaseItemKind.Series];
return _libraryManager.GetItemsResult(query);
}
@@ -418,7 +416,7 @@ namespace MediaBrowser.Controller.Entities
{
items = items.Where(i => Filter(i, query.User, query, _userDataManager, _libraryManager));
- return PostFilterAndSort(items, null, query, _libraryManager);
+ return SortAndPage(items, null, query, _libraryManager);
}
public static bool FilterItem(BaseItem item, InternalItemsQuery query)
@@ -426,21 +424,6 @@ namespace MediaBrowser.Controller.Entities
return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager);
}
- public static QueryResult<BaseItem> PostFilterAndSort(
- IEnumerable<BaseItem> items,
- int? totalRecordLimit,
- InternalItemsQuery query,
- ILibraryManager libraryManager)
- {
- // This must be the last filter
- if (!query.AdjacentTo.IsNullOrEmpty())
- {
- items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
- }
-
- return SortAndPage(items, totalRecordLimit, query, libraryManager);
- }
-
public static QueryResult<BaseItem> SortAndPage(
IEnumerable<BaseItem> items,
int? totalRecordLimit,
@@ -556,38 +539,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- if (query.IsPlayed.HasValue)
- {
- userData ??= userDataManager.GetUserData(user, item);
- if (item.IsPlayed(user, userData) != query.IsPlayed.Value)
- {
- return false;
- }
- }
-
- // Filter by Video3DFormat
- if (query.Is3D.HasValue)
- {
- var val = query.Is3D.Value;
- var video = item as Video;
-
- if (video is null || val != video.Video3DFormat.HasValue)
- {
- return false;
- }
- }
-
- /*
- * fuck - fix this
- if (query.IsHD.HasValue)
- {
- if (item.IsHD != query.IsHD.Value)
- {
- return false;
- }
- }
- */
-
if (query.IsLocked.HasValue)
{
var val = query.IsLocked.Value;
@@ -645,68 +596,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- if (query.HasOfficialRating.HasValue)
- {
- var filterValue = query.HasOfficialRating.Value;
-
- var hasValue = !string.IsNullOrEmpty(item.OfficialRating);
-
- if (hasValue != filterValue)
- {
- return false;
- }
- }
-
- if (query.IsPlaceHolder.HasValue)
- {
- var filterValue = query.IsPlaceHolder.Value;
-
- var isPlaceHolder = false;
-
- if (item is ISupportsPlaceHolders hasPlaceHolder)
- {
- isPlaceHolder = hasPlaceHolder.IsPlaceHolder;
- }
-
- if (isPlaceHolder != filterValue)
- {
- return false;
- }
- }
-
- if (query.HasSpecialFeature.HasValue)
- {
- var filterValue = query.HasSpecialFeature.Value;
-
- if (item is IHasSpecialFeatures movie)
- {
- var ok = filterValue
- ? movie.SpecialFeatureIds.Count > 0
- : movie.SpecialFeatureIds.Count == 0;
-
- if (!ok)
- {
- return false;
- }
- }
- else
- {
- return false;
- }
- }
-
- if (query.HasSubtitles.HasValue)
- {
- var val = query.HasSubtitles.Value;
-
- var video = item as Video;
-
- if (video is null || val != video.HasSubtitles)
- {
- return false;
- }
- }
-
if (query.HasParentalRating.HasValue)
{
var val = query.HasParentalRating.Value;
@@ -734,66 +623,12 @@ namespace MediaBrowser.Controller.Entities
}
}
- if (query.HasTrailer.HasValue)
- {
- var val = query.HasTrailer.Value;
- var trailerCount = 0;
-
- if (item is IHasTrailers hasTrailers)
- {
- trailerCount = hasTrailers.GetTrailerCount();
- }
-
- var ok = val ? trailerCount > 0 : trailerCount == 0;
-
- if (!ok)
- {
- return false;
- }
- }
-
- if (query.HasThemeSong.HasValue)
- {
- var filterValue = query.HasThemeSong.Value;
-
- var themeCount = item.GetThemeSongs(user).Count;
- var ok = filterValue ? themeCount > 0 : themeCount == 0;
-
- if (!ok)
- {
- return false;
- }
- }
-
- if (query.HasThemeVideo.HasValue)
- {
- var filterValue = query.HasThemeVideo.Value;
-
- var themeCount = item.GetThemeVideos(user).Count;
- var ok = filterValue ? themeCount > 0 : themeCount == 0;
-
- if (!ok)
- {
- return false;
- }
- }
-
// Apply genre filter
if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
- // Filter by VideoType
- if (query.VideoTypes.Length > 0)
- {
- var video = item as Video;
- if (video is null || !query.VideoTypes.Contains(video.VideoType))
- {
- return false;
- }
- }
-
if (query.ImageTypes.Length > 0 && !query.ImageTypes.Any(item.HasImage))
{
return false;
@@ -912,30 +747,6 @@ namespace MediaBrowser.Controller.Entities
}
}
- if (query.SeriesStatuses.Length > 0)
- {
- var ok = new[] { item }.OfType<Series>().Any(p => p.Status.HasValue && query.SeriesStatuses.Contains(p.Status.Value));
- if (!ok)
- {
- return false;
- }
- }
-
- if (query.AiredDuringSeason.HasValue)
- {
- var episode = item as Episode;
-
- if (episode is null)
- {
- return false;
- }
-
- if (!Series.FilterEpisodesBySeason(new[] { episode }, query.AiredDuringSeason.Value, true).Any())
- {
- return false;
- }
- }
-
if (query.ExcludeItemIds.Contains(item.Id))
{
return false;
@@ -989,7 +800,7 @@ namespace MediaBrowser.Controller.Entities
return GetMediaFolders(user, viewTypes);
}
- return new BaseItem[] { parent };
+ return [parent];
}
private UserView GetUserViewWithName(CollectionType? type, string sortName, BaseItem parent)
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 1043029c6e..1ddc193359 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -160,7 +160,7 @@ namespace MediaBrowser.Controller.Entities
public bool IsStacked => AdditionalParts.Length > 0;
[JsonIgnore]
- public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0;
+ public override bool HasLocalAlternateVersions => LibraryManager.GetLocalAlternateVersionIds(this).Any();
public static IRecordingsManager RecordingsManager { get; set; }
@@ -260,7 +260,10 @@ namespace MediaBrowser.Controller.Entities
{
if (callstack.Contains(video.Id))
{
- return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1;
+ // Count alternate versions using LibraryManager
+ var linkedCount = LibraryManager.GetLinkedAlternateVersions(video).Count();
+ var localCount = LibraryManager.GetLocalAlternateVersionIds(video).Count();
+ return linkedCount + localCount + 1;
}
callstack.Add(video.Id);
@@ -268,7 +271,10 @@ namespace MediaBrowser.Controller.Entities
}
}
- return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
+ // Count alternate versions using LibraryManager
+ var linkedVersionCount = LibraryManager.GetLinkedAlternateVersions(this).Count();
+ var localVersionCount = LibraryManager.GetLocalAlternateVersionIds(this).Count();
+ return linkedVersionCount + localVersionCount + 1;
}
public override List<string> GetUserDataKeys()
@@ -364,11 +370,6 @@ namespace MediaBrowser.Controller.Entities
return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
}
- public IEnumerable<Guid> GetLocalAlternateVersionIds()
- {
- return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
- }
-
private string GetUserDataKey(string providerId)
{
var key = providerId + "-" + ExtraType.ToString().ToLowerInvariant();
@@ -382,15 +383,6 @@ namespace MediaBrowser.Controller.Entities
return key;
}
- public IEnumerable<Video> GetLinkedAlternateVersions()
- {
- return LinkedAlternateVersions
- .Select(GetLinkedChild)
- .Where(i => i is not null)
- .OfType<Video>()
- .OrderBy(i => i.SortName);
- }
-
/// <summary>
/// Gets the additional parts.
/// </summary>
@@ -454,7 +446,7 @@ namespace MediaBrowser.Controller.Entities
RefreshLinkedAlternateVersions();
var tasks = LocalAlternateVersions
- .Select(i => RefreshMetadataForOwnedVideo(options, false, i, cancellationToken));
+ .Select(i => RefreshMetadataForVersions(options, false, i, cancellationToken));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
@@ -463,6 +455,39 @@ namespace MediaBrowser.Controller.Entities
return hasChanges;
}
+ protected virtual async Task RefreshMetadataForVersions(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
+ {
+ await RefreshMetadataForOwnedVideo(options, copyTitleMetadata, path, cancellationToken).ConfigureAwait(false);
+ }
+
+ private new async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
+ {
+ var newOptions = new MetadataRefreshOptions(options)
+ {
+ SearchResult = null
+ };
+
+ var id = LibraryManager.GetNewItemId(path, typeof(Video));
+ if (LibraryManager.GetItemById(id) is not Video video)
+ {
+ video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
+
+ newOptions.ForceSave = true;
+ }
+
+ if (video is null)
+ {
+ return;
+ }
+
+ if (video.OwnerId.IsEmpty())
+ {
+ video.OwnerId = Id;
+ }
+
+ await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false);
+ }
+
private void RefreshLinkedAlternateVersions()
{
foreach (var child in LinkedAlternateVersions)
@@ -480,7 +505,7 @@ namespace MediaBrowser.Controller.Entities
{
await base.UpdateToRepositoryAsync(updateReason, cancellationToken).ConfigureAwait(false);
- var localAlternates = GetLocalAlternateVersionIds()
+ var localAlternates = LibraryManager.GetLocalAlternateVersionIds(this)
.Select(i => LibraryManager.GetItemById(i))
.Where(i => i is not null);
@@ -537,7 +562,7 @@ namespace MediaBrowser.Controller.Entities
(this, MediaSourceType.Default)
};
- list.AddRange(GetLinkedAlternateVersions().Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
+ list.AddRange(LibraryManager.GetLinkedAlternateVersions(this).Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
if (!string.IsNullOrEmpty(PrimaryVersionId))
{
@@ -545,14 +570,14 @@ namespace MediaBrowser.Controller.Entities
{
var existingIds = list.Select(i => i.Item1.Id).ToList();
list.Add((primary, MediaSourceType.Grouping));
- list.AddRange(primary.GetLinkedAlternateVersions().Where(i => !existingIds.Contains(i.Id)).Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
+ list.AddRange(LibraryManager.GetLinkedAlternateVersions(primary).Where(i => !existingIds.Contains(i.Id)).Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
}
}
var localAlternates = list
.SelectMany(i =>
{
- return i.Item1 is Video video ? video.GetLocalAlternateVersionIds() : Enumerable.Empty<Guid>();
+ return i.Item1 is Video video ? LibraryManager.GetLocalAlternateVersionIds(video) : Enumerable.Empty<Guid>();
})
.Select(LibraryManager.GetItemById)
.Where(i => i is not null)
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index df1c98f3f7..c19d15d85f 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -214,6 +214,22 @@ namespace MediaBrowser.Controller.Library
Task<IEnumerable<Video>> GetIntros(BaseItem item, User user);
/// <summary>
+ /// Gets the IDs of local alternate versions for a video.
+ /// Local alternate versions are alternate quality versions at different file paths.
+ /// </summary>
+ /// <param name="video">The video item.</param>
+ /// <returns>Enumerable of alternate version item IDs.</returns>
+ IEnumerable<Guid> GetLocalAlternateVersionIds(Video video);
+
+ /// <summary>
+ /// Gets the linked alternate versions for a video.
+ /// Linked alternate versions are different items representing the same content (e.g., Director's Cut).
+ /// </summary>
+ /// <param name="video">The video item.</param>
+ /// <returns>Enumerable of linked Video items.</returns>
+ IEnumerable<Video> GetLinkedAlternateVersions(Video video);
+
+ /// <summary>
/// Adds the parts.
/// </summary>
/// <param name="rules">The rules.</param>
@@ -601,6 +617,20 @@ namespace MediaBrowser.Controller.Library
IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents, DateTime dateCutoff);
/// <summary>
+ /// Gets next up episodes for multiple series in a single batched query.
+ /// </summary>
+ /// <param name="query">The query filter.</param>
+ /// <param name="seriesKeys">The series presentation unique keys to query.</param>
+ /// <param name="includeSpecials">Whether to include specials for aired episode order sorting.</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, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisodesBatch(
+ InternalItemsQuery query,
+ IReadOnlyList<string> seriesKeys,
+ bool includeSpecials,
+ bool includeWatchedForRewatching);
+
+ /// <summary>
/// Gets the items result.
/// </summary>
/// <param name="query">The query.</param>
@@ -649,6 +679,23 @@ namespace MediaBrowser.Controller.Library
ItemCounts GetItemCounts(InternalItemsQuery query);
+ /// <summary>
+ /// Batch-fetches child counts for multiple parent folders.
+ /// Returns the count of immediate children (non-recursive) for each parent.
+ /// </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);
+
+ /// <summary>
+ /// Configures the query with user access settings including TopParentIds for library access.
+ /// Call this before passing a query to methods that need user access filtering.
+ /// </summary>
+ /// <param name="query">The query to configure.</param>
+ /// <param name="user">The user to configure access for.</param>
+ void ConfigureUserAccess(InternalItemsQuery query, User user);
+
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
BaseItem GetParentItem(Guid? parentId, Guid? userId);
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index bf80b7d0a8..f7ed39730e 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -88,6 +88,21 @@ public interface IItemRepository
IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff);
/// <summary>
+ /// Gets next up episodes for multiple series in a single batched query.
+ /// Returns the last watched episode, next unwatched episode, specials, and next played episode for each series.
+ /// </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 (ParentIndexNumber = 0) in the results.</param>
+ /// <param name="includeWatchedForRewatching">Whether to include watched episodes for rewatching mode.</param>
+ /// <returns>A dictionary mapping series key to batch result containing episodes needed for NextUp calculation.</returns>
+ IReadOnlyDictionary<string, NextUpEpisodeBatchResult> GetNextUpEpisodesBatch(
+ InternalItemsQuery filter,
+ IReadOnlyList<string> seriesKeys,
+ bool includeSpecials,
+ bool includeWatchedForRewatching);
+
+ /// <summary>
/// Updates the inherited values.
/// </summary>
void UpdateInheritedValues();
@@ -133,9 +148,66 @@ public interface IItemRepository
bool GetIsPlayed(User user, Guid id, bool recursive);
/// <summary>
+ /// Gets the count of played items that are descendants of the specified ancestor.
+ /// Uses the AncestorIds table for efficient recursive lookup.
+ /// Applies user access filtering (library access, parental controls, tags).
+ /// </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.
+ /// Uses the AncestorIds table for efficient recursive lookup.
+ /// Applies user access filtering (library access, parental controls, tags).
+ /// </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 items that are descendants of the specified ancestor.
+ /// Uses the AncestorIds table for efficient recursive lookup.
+ /// Applies user access filtering (library access, parental controls, tags).
+ /// </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 of items that are linked children of the specified parent.
+ /// Uses the LinkedChildren table for BoxSets, Playlists, etc.
+ /// Applies user access filtering (library access, parental controls, tags).
+ /// </summary>
+ /// <param name="filter">The query filter containing user access settings.</param>
+ /// <param name="parentId">The parent item id (BoxSet, Playlist, etc.).</param>
+ /// <returns>A tuple containing (Played count, Total count).</returns>
+ (int Played, int Total) GetPlayedAndTotalCountFromLinkedChildren(InternalItemsQuery filter, Guid parentId);
+
+ /// <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 (e.g., LocalAlternateVersion, LinkedAlternateVersion).</param>
+ /// <returns>List of child item IDs.</returns>
+ IReadOnlyList<Guid> GetLinkedChildrenIds(Guid parentId, int? childType = null);
+
+ /// <summary>
/// Gets all artist matches from the db.
/// </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>
+ /// Batch-fetches child counts for multiple parent folders.
+ /// Returns the count of immediate children (non-recursive) for each parent.
+ /// </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/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; }
+}