aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/Entities
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller/Entities')
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs18
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs12
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs35
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs112
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs22
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs37
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs13
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs51
8 files changed, 247 insertions, 53 deletions
diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index a02802f41e..ef24f632ed 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -54,6 +54,24 @@ namespace MediaBrowser.Controller.Entities
public string[] PhysicalLocationsList { get; set; }
+ // Children caches the resolved items, _childrenIds the ids they were loaded from. Clearing
+ // only the former sends the next read back through LoadChildren, which replays the stale
+ // id list, so a caller invalidating this folder has to drop both.
+ [JsonIgnore]
+ public override IEnumerable<BaseItem> Children
+ {
+ get => base.Children;
+ set
+ {
+ if (value is null)
+ {
+ ClearCache();
+ }
+
+ base.Children = value;
+ }
+ }
+
public override bool CanDelete()
{
return false;
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index d016d8f62b..281a98dad5 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -165,6 +165,18 @@ namespace MediaBrowser.Controller.Entities.Audio
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
+ try
+ {
+ await RefreshAllMetadataInternal(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ ReleaseCachedChildren();
+ }
+ }
+
+ private async Task RefreshAllMetadataInternal(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
+ {
var items = GetRecursiveChildren();
var totalItems = items.Count;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index d030c8f420..70e7da8932 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1411,7 +1411,8 @@ namespace MediaBrowser.Controller.Entities
/// token shared by the descriptors but separated only by spaces (e.g. a common "2160p ") is
/// kept in the label, falling back to a space only when no structural delimiter is shared. The
/// separators mirror the version delimiters recognised by the naming layer (Emby.Naming
- /// VideoFlagDelimiters).
+ /// VideoFlagDelimiters), except that a dot between digits is a decimal point rather than a
+ /// delimiter, so numeric version labels stay whole.
/// </summary>
/// <param name="fileNames">The version file names without extension; must contain at least one entry.</param>
/// <returns>The shared prefix retreated to a separator boundary, or an empty string when none is shared.</returns>
@@ -1445,9 +1446,12 @@ namespace MediaBrowser.Controller.Entities
if (!prefixIsWholeName)
{
- // Retreat to the last structural delimiter ('-', '_', '.').
+ // Retreat to the last structural delimiter ('-', '_', '.'), skipping dots that are
+ // decimal points within a number rather than delimiters (see IsDecimalPoint).
var cut = prefix.Length;
- while (cut > 0 && Array.IndexOf(VersionDelimiters, prefix[cut - 1]) < 0)
+ while (cut > 0
+ && (Array.IndexOf(VersionDelimiters, prefix[cut - 1]) < 0
+ || IsDecimalPoint(prefix, cut - 1, fileNames)))
{
cut--;
}
@@ -1467,6 +1471,31 @@ namespace MediaBrowser.Controller.Entities
return prefix;
}
+ private static bool IsDecimalPoint(string prefix, int index, IReadOnlyList<string> fileNames)
+ {
+ if (index == 0 || prefix[index] != '.' || !char.IsDigit(prefix[index - 1]))
+ {
+ return false;
+ }
+
+ if (index + 1 < prefix.Length)
+ {
+ return char.IsDigit(prefix[index + 1]);
+ }
+
+ // The dot ends the prefix, so the character after it is the first one that differs between
+ // the versions: only a decimal point when every version continues the number.
+ for (var i = 0; i < fileNames.Count; i++)
+ {
+ if (fileNames[i].Length <= index + 1 || !char.IsDigit(fileNames[i][index + 1]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
public Task RefreshMetadata(CancellationToken cancellationToken)
{
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index d8203ea6f2..626bc0d5a1 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -286,6 +286,27 @@ namespace MediaBrowser.Controller.Entities
return GetCachedChildren();
}
+ /// <summary>
+ /// Drops the children this folder has materialised, and the ones held by every folder below
+ /// it, without loading anything that is not already in memory.
+ /// </summary>
+ public void ReleaseCachedChildren()
+ {
+ // Cleared before descending, so a folder already on the way down is not walked twice.
+ var children = _children;
+ _children = null;
+
+ if (children is null)
+ {
+ return;
+ }
+
+ foreach (var child in children)
+ {
+ (child as Folder)?.ReleaseCachedChildren();
+ }
+ }
+
public override double? GetRefreshProgress()
{
return ProviderManager.GetRefreshProgress(Id);
@@ -316,7 +337,7 @@ namespace MediaBrowser.Controller.Entities
var dictionary = new Dictionary<Guid, BaseItem>();
Children = null; // invalidate cached children.
- var childrenList = Children.ToList();
+ var childrenList = GetChildrenForValidation();
foreach (var child in childrenList)
{
@@ -370,6 +391,9 @@ namespace MediaBrowser.Controller.Entities
{
ProviderManager.OnRefreshComplete(this);
}
+
+ // The subtree is done with, so stop holding it.
+ ReleaseCachedChildren();
}
}
@@ -405,19 +429,23 @@ namespace MediaBrowser.Controller.Entities
if (IsFileProtocol)
{
- IEnumerable<BaseItem> nonCachedChildren = [];
+ IEnumerable<BaseItem> nonCachedChildren;
try
{
- nonCachedChildren = GetNonCachedChildren(directoryService);
+ // Finish enumeration before mutating the library. An I/O failure, including
+ // one partway through a lazy enumeration, must not look like removed files.
+ nonCachedChildren = GetNonCachedChildren(directoryService).ToArray();
}
catch (IOException ex)
{
Logger.LogError(ex, "Error retrieving children from file system");
+ return;
}
catch (SecurityException ex)
{
Logger.LogError(ex, "Error retrieving children from file system");
+ return;
}
catch (Exception ex)
{
@@ -551,7 +579,7 @@ namespace MediaBrowser.Controller.Entities
&& primaryVideo.OwnerId.IsEmpty()
&& (primaryVideo.LocalAlternateVersions ?? []).Any(p => alternateVersionPaths.Contains(p)))
{
- var newPrimary = newItems
+ var newPrimary = validChildren
.OfType<Video>()
.FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
.Any(p => (primaryVideo.LocalAlternateVersions ?? [])
@@ -593,6 +621,8 @@ namespace MediaBrowser.Controller.Entities
newPrimary.Name,
newPrimary.Id);
+ await PromoteToPrimaryVersionAsync(newPrimary, cancellationToken).ConfigureAwait(false);
+
// Reroute collection/playlist references from old primary to new primary
await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(false);
@@ -621,9 +651,12 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.DeleteItem(oldPrimary, new DeleteOptions { DeleteFileLocation = false }, this, false);
}
- // Demote old primaries that are now alternate versions of newly created primaries.
+ // Demote old primaries that are now alternate versions of another primary.
// This handles the case where a new file is added that becomes the new primary
- // (e.g. movie-2 added, movie-3 was primary → movie-3 needs demotion).
+ // (e.g. movie-2 added, movie-3 was primary → movie-3 needs demotion), and the case
+ // where the file that takes over was already in the library and merely traded
+ // places with this one — so the new primary is looked up among all valid children
+ // rather than only the newly created ones.
// Items in replacedPrimaries are excluded (already in actuallyRemoved).
var oldPrimariesToDemote = new List<(Video OldPrimary, Video NewPrimary)>();
foreach (var item in itemsRemoved.Except(actuallyRemoved))
@@ -633,7 +666,7 @@ namespace MediaBrowser.Controller.Entities
&& !string.IsNullOrEmpty(item.Path)
&& alternateVersionPaths.Contains(item.Path))
{
- var newPrimary = newItems
+ var newPrimary = validChildren
.OfType<Video>()
.FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
.Any(p => string.Equals(p, item.Path, StringComparison.OrdinalIgnoreCase)));
@@ -653,10 +686,13 @@ namespace MediaBrowser.Controller.Entities
newPrimary.Name,
newPrimary.Id);
+ await PromoteToPrimaryVersionAsync(newPrimary, cancellationToken).ConfigureAwait(false);
+
// First: update old primary's alternate items to point to new primary.
// Order matters — update alternates FIRST so they don't get orphan-deleted
// when old primary's arrays are cleared.
- var oldAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary)
+ var oldLocalAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary).ToHashSet();
+ var oldAlternateIds = oldLocalAlternateIds
.Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
.Distinct()
.ToList();
@@ -666,7 +702,10 @@ namespace MediaBrowser.Controller.Entities
if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
{
altVideo.SetPrimaryVersionId(newPrimary.Id);
- altVideo.OwnerId = newPrimary.Id;
+
+ // Only a version stored next to the new primary is owned by it; one that
+ // was merged in by hand keeps its own row and must stay unowned.
+ altVideo.OwnerId = oldLocalAlternateIds.Contains(altVideo.Id) ? newPrimary.Id : Guid.Empty;
await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
}
@@ -772,6 +811,23 @@ namespace MediaBrowser.Controller.Entities
}
}
+ private async Task PromoteToPrimaryVersionAsync(Video newPrimary, CancellationToken cancellationToken)
+ {
+ if (!newPrimary.PrimaryVersionId.HasValue && newPrimary.OwnerId.IsEmpty())
+ {
+ return;
+ }
+
+ Logger.LogInformation(
+ "Promoting {Name} ({Id}) to the primary version of its group",
+ newPrimary.Name,
+ newPrimary.Id);
+
+ newPrimary.SetPrimaryVersionId(null);
+ newPrimary.OwnerId = Guid.Empty;
+ await newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+
private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
{
await RunTasks(
@@ -807,7 +863,14 @@ namespace MediaBrowser.Controller.Entities
if (recursive && child is Folder folder)
{
folder.Children = null; // invalidate cached children.
- await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ folder.ReleaseCachedChildren();
+ }
}
}
}
@@ -876,6 +939,17 @@ namespace MediaBrowser.Controller.Entities
});
}
+ private IReadOnlyList<BaseItem> GetChildrenForValidation()
+ {
+ return ItemRepository.GetItemList(new InternalItemsQuery
+ {
+ Parent = this,
+ GroupByPresentationUniqueKey = false,
+ IncludeAlternateVersions = true,
+ DtoOptions = new DtoOptions(true)
+ });
+ }
+
public virtual int GetChildCount(User user)
{
if (LinkedChildren.Length > 0)
@@ -1613,7 +1687,17 @@ namespace MediaBrowser.Controller.Entities
/// <returns>IEnumerable{BaseItem}.</returns>
public List<BaseItem> GetLinkedChildren()
{
- var resolved = ResolveLinkedChildren(LinkedChildren);
+ return GetLinkedChildren(new DtoOptions());
+ }
+
+ /// <summary>
+ /// Gets the linked children, populating only what <paramref name="options"/> asks for.
+ /// </summary>
+ /// <param name="options">Fields to populate on the resolved children.</param>
+ /// <returns>The resolved children.</returns>
+ public List<BaseItem> GetLinkedChildren(DtoOptions options)
+ {
+ var resolved = ResolveLinkedChildren(LinkedChildren, options);
var list = new List<BaseItem>(resolved.Count);
foreach (var (_, item) in resolved)
{
@@ -1730,8 +1814,9 @@ namespace MediaBrowser.Controller.Entities
/// path (legacy path-based resolution).
/// </summary>
/// <param name="linkedChildren">Linked children to resolve.</param>
+ /// <param name="options">Fields to populate on the resolved items; all fields when null.</param>
/// <returns>Each input entry paired with its resolved item; entries that fail to resolve are dropped.</returns>
- private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList<LinkedChild> linkedChildren)
+ private List<(LinkedChild Info, BaseItem Item)> ResolveLinkedChildren(IReadOnlyList<LinkedChild> linkedChildren, DtoOptions options = null)
{
var resolved = new List<(LinkedChild Info, BaseItem Item)>(linkedChildren.Count);
if (linkedChildren.Count == 0)
@@ -1753,7 +1838,8 @@ namespace MediaBrowser.Controller.Entities
{
var batched = LibraryManager.GetItemList(new InternalItemsQuery
{
- ItemIds = [.. idsToBatch]
+ ItemIds = [.. idsToBatch],
+ DtoOptions = options ?? new DtoOptions()
});
byId = new Dictionary<Guid, BaseItem>(batched.Count);
foreach (var item in batched)
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index e85f86b72f..7c88d5dd05 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -103,6 +103,7 @@ namespace MediaBrowser.Controller.Entities
|| SubtitleLanguages.Count > 0
|| LinkedChildAncestorIds.Length > 0
|| AncestorIds.Length > 0
+ || DescendantOfId.HasValue
|| IsFavorite.HasValue
|| IsFavoriteOrLiked.HasValue
|| IsLiked.HasValue
@@ -368,6 +369,13 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public Guid[] LinkedChildAncestorIds { get; set; }
+ /// <summary>
+ /// Gets or sets the id of a folder whose descendants the items must be part of.
+ /// Unlike <see cref="AncestorIds"/> this also follows the linked children of BoxSets and
+ /// Playlists, so it reaches the items below a linked folder (a Series' episodes, for example).
+ /// </summary>
+ public Guid? DescendantOfId { get; set; }
+
public Guid[] TopParentIds { get; set; }
public CollectionType?[] PresetViews { get; set; }
@@ -424,12 +432,18 @@ namespace MediaBrowser.Controller.Entities
public string? HasNoSubtitleTrackWithLanguage { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to return only items nothing names any more.
+ /// </summary>
public bool? IsDeadArtist { get; set; }
public bool? IsDeadStudio { get; set; }
public bool? IsDeadGenre { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to return only items nothing names any more.
+ /// </summary>
public bool? IsDeadPerson { get; set; }
/// <summary>
@@ -474,6 +488,14 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public bool IncludeOwnedItems { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether to include alternate versions, which carry a
+ /// <see cref="Video.PrimaryVersionId"/> and are normally hidden behind the version they
+ /// belong to. Unlike <see cref="IncludeOwnedItems"/> this keeps the versions a user merged
+ /// by hand without also returning the parts and extras owned by another item.
+ /// </summary>
+ public bool IncludeAlternateVersions { get; set; }
+
public bool? Is4K { get; set; }
public int? MaxHeight { get; set; }
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 8216937cad..16d8bfe391 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -11,6 +11,7 @@ using Jellyfin.Data;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Querying;
@@ -89,7 +90,7 @@ namespace MediaBrowser.Controller.Entities.Movies
return base.GetNonCachedChildren(directoryService);
}
- return Enumerable.Empty<BaseItem>();
+ return [];
}
protected override IReadOnlyList<BaseItem> LoadChildren()
@@ -168,14 +169,21 @@ namespace MediaBrowser.Controller.Entities.Movies
return true;
}
- var userLibraryFolderIds = GetLibraryFolderIds(user);
- var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds();
+ List<BaseItem> linkedItems = null;
+ var libraryFolderIds = LibraryFolderIds;
+ if (libraryFolderIds is null)
+ {
+ linkedItems = GetLinkedChildren(DtoOptions.StoredColumnsOnly);
+ libraryFolderIds = GetLibraryFolderIds(linkedItems);
+ }
if (libraryFolderIds.Length == 0)
{
return true;
}
+ var userLibraryFolderIds = GetLibraryFolderIds(user);
+
if (!userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i)))
{
return false;
@@ -184,7 +192,7 @@ namespace MediaBrowser.Controller.Entities.Movies
// If user has parental controls, hide the BoxSet when all children are restricted
if (user.MaxParentalRatingScore.HasValue)
{
- var linkedItems = GetLinkedChildren();
+ linkedItems ??= GetLinkedChildren(DtoOptions.StoredColumnsOnly);
if (linkedItems.Count > 0 && linkedItems.All(child => !child.IsParentalAllowed(user, true)))
{
return false;
@@ -241,10 +249,19 @@ namespace MediaBrowser.Controller.Entities.Movies
public Guid[] GetLibraryFolderIds()
{
- var expandedFolders = new List<Guid>();
+ return GetLibraryFolderIds(GetLinkedChildren(DtoOptions.StoredColumnsOnly));
+ }
+
+ private Guid[] GetLibraryFolderIds(IEnumerable<BaseItem> linkedChildren)
+ {
+ // Seeded with this box set so a cycle through a nested collection terminates.
+ var expandedFolders = new List<Guid> { Id };
+
+ // The user root children are the same for every item.
+ var rootChildren = LibraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
- return FlattenItems(this, expandedFolders)
- .SelectMany(LibraryManager.GetCollectionFolders)
+ return FlattenItems(linkedChildren, expandedFolders)
+ .SelectMany(i => LibraryManager.GetCollectionFolders(i, rootChildren))
.Select(i => i.Id)
.Distinct()
.ToArray();
@@ -264,13 +281,13 @@ namespace MediaBrowser.Controller.Entities.Movies
{
expandedFolders.Add(item.Id);
- return FlattenItems(boxset.GetLinkedChildren(), expandedFolders);
+ return FlattenItems(boxset.GetLinkedChildren(DtoOptions.StoredColumnsOnly), expandedFolders);
}
- return Array.Empty<BaseItem>();
+ return [];
}
- return new[] { item };
+ return [item];
}
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 1a1da84b7a..126f4361ba 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -333,6 +333,19 @@ namespace MediaBrowser.Controller.Entities.TV
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
Children = null; // invalidate cached children.
+
+ try
+ {
+ await RefreshAllMetadataInternal(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ ReleaseCachedChildren();
+ }
+ }
+
+ private async Task RefreshAllMetadataInternal(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
+ {
// Refresh bottom up, seasons and episodes first, then the series
var items = GetRecursiveChildren();
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index f9ad2d86e6..82256cd964 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -455,25 +455,34 @@ namespace MediaBrowser.Controller.Entities
{
var itemList = filtered.ToList();
var folderIds = itemList.OfType<Folder>().Select(f => f.Id).ToList();
+ var leaves = itemList.Where(i => i is not Folder).ToList();
+ var isPlayedValue = query.IsPlayed.Value;
- if (folderIds.Count > 0)
- {
- var counts = libraryManager.GetPlayedAndTotalCountBatch(folderIds, user);
- var isPlayedValue = query.IsPlayed.Value;
+ var counts = folderIds.Count > 0
+ ? libraryManager.GetPlayedAndTotalCountBatch(folderIds, user)
+ : null;
+
+ // A movie held as several files is watched once any of its versions is watched.
+ var resumeData = leaves.Count > 0
+ ? userDataManager.GetResumeUserDataBatch(leaves, user)
+ : null;
- return itemList.Where(item =>
+ return itemList.Where(item =>
+ {
+ if (item is Folder)
{
- if (item is Folder)
- {
- var itemCount = counts.GetValueOrDefault(item.Id);
- return (itemCount.Played >= itemCount.Total) == isPlayedValue;
- }
+ var itemCount = counts?.GetValueOrDefault(item.Id) ?? default;
+ return (itemCount.Played >= itemCount.Total) == isPlayedValue;
+ }
- return true;
- });
- }
+ var played = userDataManager.GetUserData(user, item)?.Played ?? false;
+ if (!played && resumeData is not null && resumeData.TryGetValue(item.Id, out var versionData))
+ {
+ played = versionData.UserData.Played;
+ }
- return itemList;
+ return played == isPlayedValue;
+ });
}
return filtered;
@@ -606,19 +615,7 @@ namespace MediaBrowser.Controller.Entities
}
}
- if (query.IsPlayed.HasValue)
- {
- // Folder.IsPlayed() hits the DB per-item (N+1 queries).
- // Folders are batch-filtered by the collection Filter() overload.
- if (!item.IsFolder)
- {
- userData ??= userDataManager.GetUserData(user, item);
- if (item.IsPlayed(user, userData) != query.IsPlayed.Value)
- {
- return false;
- }
- }
- }
+ // IsPlayed is answered by the collection Filter() overload for folders and leaves alike.
if (query.IsLocked.HasValue)
{