aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/Entities/Folder.cs
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller/Entities/Folder.cs')
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs314
1 files changed, 186 insertions, 128 deletions
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 4da22854b..151b957fe 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -11,7 +11,6 @@ using System.Security;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
-using System.Threading.Tasks.Dataflow;
using J2N.Collections.Generic.Extensions;
using Jellyfin.Data;
using Jellyfin.Data.Enums;
@@ -25,6 +24,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LibraryTaskScheduler;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
@@ -42,6 +42,8 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
public class Folder : BaseItem
{
+ private IEnumerable<BaseItem> _children;
+
public Folder()
{
LinkedChildren = Array.Empty<LinkedChild>();
@@ -49,6 +51,8 @@ namespace MediaBrowser.Controller.Entities
public static IUserViewManager UserViewManager { get; set; }
+ public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; }
+
/// <summary>
/// Gets or sets a value indicating whether this instance is root.
/// </summary>
@@ -106,11 +110,15 @@ namespace MediaBrowser.Controller.Entities
}
/// <summary>
- /// Gets the actual children.
+ /// Gets or Sets the actual children.
/// </summary>
/// <value>The actual children.</value>
[JsonIgnore]
- public virtual IEnumerable<BaseItem> Children => LoadChildren();
+ public virtual IEnumerable<BaseItem> Children
+ {
+ get => _children ??= LoadChildren();
+ set => _children = value;
+ }
/// <summary>
/// Gets thread-safe access to all recursive children of this folder - without regard to user.
@@ -279,6 +287,7 @@ namespace MediaBrowser.Controller.Entities
/// <returns>Task.</returns>
public Task ValidateChildren(IProgress<double> progress, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true, bool allowRemoveRoot = false, CancellationToken cancellationToken = default)
{
+ Children = null; // invalidate cached children.
return ValidateChildrenInternal(progress, recursive, true, allowRemoveRoot, metadataRefreshOptions, metadataRefreshOptions.DirectoryService, cancellationToken);
}
@@ -286,6 +295,7 @@ namespace MediaBrowser.Controller.Entities
{
var dictionary = new Dictionary<Guid, BaseItem>();
+ Children = null; // invalidate cached children.
var childrenList = Children.ToList();
foreach (var child in childrenList)
@@ -327,6 +337,11 @@ namespace MediaBrowser.Controller.Entities
try
{
+ if (GetParents().Any(f => f.Id.Equals(Id)))
+ {
+ throw new InvalidOperationException("Recursive datastructure detected abort processing this item.");
+ }
+
await ValidateChildrenInternal2(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
}
finally
@@ -442,6 +457,12 @@ namespace MediaBrowser.Controller.Entities
{
foreach (var item in itemsRemoved)
{
+ if (!item.CanDelete())
+ {
+ Logger.LogDebug("Item marked as non-removable, skipping: {Path}", item.Path ?? item.Name);
+ continue;
+ }
+
if (item.IsFileProtocol)
{
Logger.LogDebug("Removed item: {Path}", item.Path);
@@ -524,6 +545,7 @@ namespace MediaBrowser.Controller.Entities
{
if (validChildrenNeedGeneration)
{
+ Children = null; // invalidate cached children.
validChildren = Children.ToList();
}
@@ -566,7 +588,8 @@ namespace MediaBrowser.Controller.Entities
if (recursive && child is Folder folder)
{
- await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
+ folder.Children = null; // invalidate cached children.
+ await folder.RefreshMetadataRecursive(folder.Children.Except([this, child]).ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -598,51 +621,13 @@ namespace MediaBrowser.Controller.Entities
/// <returns>Task.</returns>
private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progress, CancellationToken cancellationToken)
{
- var childrenCount = children.Count;
- var childrenProgress = new double[childrenCount];
-
- void UpdateProgress()
- {
- progress.Report(childrenProgress.Average());
- }
-
- var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
- var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount;
-
- var actionBlock = new ActionBlock<int>(
- async i =>
- {
- var innerProgress = new Progress<double>(innerPercent =>
- {
- // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls
- var innerPercentRounded = Math.Round(innerPercent);
- if (childrenProgress[i] != innerPercentRounded)
- {
- childrenProgress[i] = innerPercentRounded;
- UpdateProgress();
- }
- });
-
- await task(children[i], innerProgress).ConfigureAwait(false);
-
- childrenProgress[i] = 100;
-
- UpdateProgress();
- },
- new ExecutionDataflowBlockOptions
- {
- MaxDegreeOfParallelism = parallelism,
- CancellationToken = cancellationToken,
- });
-
- for (var i = 0; i < childrenCount; i++)
- {
- await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false);
- }
-
- actionBlock.Complete();
-
- await actionBlock.Completion.ConfigureAwait(false);
+ await LimitedConcurrencyLibraryScheduler
+ .Enqueue(
+ children.ToArray(),
+ task,
+ progress,
+ cancellationToken)
+ .ConfigureAwait(false);
}
/// <summary>
@@ -722,16 +707,29 @@ namespace MediaBrowser.Controller.Entities
IEnumerable<BaseItem> items;
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
+ var totalCount = 0;
if (query.User is null)
{
items = GetRecursiveChildren(filter);
+ totalCount = items.Count();
}
else
{
- items = GetRecursiveChildren(user, query);
+ // Save pagination params before clearing them to prevent pagination from happening
+ // before sorting. PostFilterAndSort will apply pagination after sorting.
+ var limit = query.Limit;
+ var startIndex = query.StartIndex;
+ query.Limit = null;
+ query.StartIndex = null;
+
+ items = GetRecursiveChildren(user, query, out totalCount);
+
+ // Restore pagination params so PostFilterAndSort can apply them after sorting
+ query.Limit = limit;
+ query.StartIndex = startIndex;
}
- return PostFilterAndSort(items, query, true);
+ return PostFilterAndSort(items, query);
}
if (this is not UserRootFolder
@@ -980,25 +978,31 @@ namespace MediaBrowser.Controller.Entities
IEnumerable<BaseItem> items;
+ int totalItemCount = 0;
if (query.User is null)
{
items = Children.Where(filter);
+ totalItemCount = items.Count();
}
else
{
// need to pass this param to the children.
+ // Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort
var childQuery = new InternalItemsQuery
{
- DisplayAlbumFolders = query.DisplayAlbumFolders
+ DisplayAlbumFolders = query.DisplayAlbumFolders,
+ NameStartsWith = query.NameStartsWith,
+ NameStartsWithOrGreater = query.NameStartsWithOrGreater,
+ NameLessThan = query.NameLessThan
};
- items = GetChildren(user, true, childQuery).Where(filter);
+ items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter);
}
- return PostFilterAndSort(items, query, true);
+ return PostFilterAndSort(items, query);
}
- protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
+ protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
{
var user = query.User;
@@ -1008,7 +1012,7 @@ namespace MediaBrowser.Controller.Entities
items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
}
- #pragma warning disable CA1309
+#pragma warning disable CA1309
if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
{
items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1);
@@ -1023,7 +1027,7 @@ namespace MediaBrowser.Controller.Entities
{
items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1);
}
- #pragma warning restore CA1309
+#pragma warning restore CA1309
// This must be the last filter
if (!query.AdjacentTo.IsNullOrEmpty())
@@ -1031,7 +1035,15 @@ namespace MediaBrowser.Controller.Entities
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
}
- return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
+ var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
+ var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
+
+ if (query.EnableTotalRecordCount)
+ {
+ result.TotalRecordCount = filteredItems.Count;
+ }
+
+ return result;
}
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
@@ -1044,12 +1056,49 @@ namespace MediaBrowser.Controller.Entities
{
ArgumentNullException.ThrowIfNull(items);
- if (CollapseBoxSetItems(query, queryParent, user, configurationManager))
+ if (!CollapseBoxSetItems(query, queryParent, user, configurationManager))
+ {
+ return items;
+ }
+
+ var config = configurationManager.Configuration;
+
+ bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
+ bool collapseSeries = config.EnableGroupingShowsIntoCollections;
+
+ if (user is null || (collapseMovies && collapseSeries))
+ {
+ return collectionManager.CollapseItemsWithinBoxSets(items, user);
+ }
+
+ if (!collapseMovies && !collapseSeries)
{
- items = collectionManager.CollapseItemsWithinBoxSets(items, user);
+ return items;
}
- return items;
+ var collapsibleItems = new List<BaseItem>();
+ var remainingItems = new List<BaseItem>();
+
+ foreach (var item in items)
+ {
+ if ((collapseMovies && item is Movie) || (collapseSeries && item is Series))
+ {
+ collapsibleItems.Add(item);
+ }
+ else
+ {
+ remainingItems.Add(item);
+ }
+ }
+
+ if (collapsibleItems.Count == 0)
+ {
+ return remainingItems;
+ }
+
+ var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user);
+
+ return collapsedItems.Concat(remainingItems);
}
private static bool CollapseBoxSetItems(
@@ -1080,24 +1129,26 @@ namespace MediaBrowser.Controller.Entities
}
var param = query.CollapseBoxSetItems;
-
- if (!param.HasValue)
+ if (param.HasValue)
{
- if (user is not null && query.IncludeItemTypes.Any(type =>
- (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) ||
- (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections)))
- {
- return false;
- }
+ return param.Value && AllowBoxSetCollapsing(query);
+ }
- if (query.IncludeItemTypes.Length == 0
- || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series))
- {
- param = true;
- }
+ var config = configurationManager.Configuration;
+
+ bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie);
+ bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Series);
+
+ bool collapseMovies = config.EnableGroupingMoviesIntoCollections;
+ bool collapseSeries = config.EnableGroupingShowsIntoCollections;
+
+ if (user is not null)
+ {
+ bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries);
+ return canCollapse && AllowBoxSetCollapsing(query);
}
- return param.HasValue && param.Value && AllowBoxSetCollapsing(query);
+ return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query);
}
private static bool AllowBoxSetCollapsing(InternalItemsQuery request)
@@ -1222,11 +1273,6 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.IsPlayed.HasValue)
- {
- return false;
- }
-
if (!string.IsNullOrWhiteSpace(request.Person))
{
return false;
@@ -1267,17 +1313,15 @@ namespace MediaBrowser.Controller.Entities
return false;
}
- if (request.MinCommunityRating.HasValue)
- {
- return false;
- }
-
- if (request.MinCriticRating.HasValue)
+ if (request.MinIndexNumber.HasValue)
{
return false;
}
- if (request.MinIndexNumber.HasValue)
+ if (request.OrderBy.Any(o =>
+ o.OrderBy == ItemSortBy.CommunityRating ||
+ o.OrderBy == ItemSortBy.CriticRating ||
+ o.OrderBy == ItemSortBy.Runtime))
{
return false;
}
@@ -1285,30 +1329,30 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- public IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren)
- {
- ArgumentNullException.ThrowIfNull(user);
-
- return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
- }
-
- public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
+ public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
{
ArgumentNullException.ThrowIfNull(user);
+ query ??= new InternalItemsQuery();
+ query.User = user;
// the true root should return our users root folder children
if (IsPhysicalRoot)
{
- return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren);
+ return LibraryManager.GetUserRootFolder().GetChildren(user, includeLinkedChildren, out totalItemCount);
}
var result = new Dictionary<Guid, BaseItem>();
- AddChildren(user, includeLinkedChildren, result, false, query);
+ totalItemCount = AddChildren(user, includeLinkedChildren, result, false, query);
return result.Values.ToArray();
}
+ public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query = null)
+ {
+ return GetChildren(user, includeLinkedChildren, out _, query);
+ }
+
protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{
return Children;
@@ -1317,13 +1361,13 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// Adds the children to list.
/// </summary>
- private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
+ private int AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders = null)
{
// Prevent infinite recursion of nested folders
visitedFolders ??= new HashSet<Folder>();
if (!visitedFolders.Add(this))
{
- return;
+ return 0;
}
// If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
@@ -1340,44 +1384,67 @@ namespace MediaBrowser.Controller.Entities
children = GetEligibleChildrenForRecursiveChildren(user);
}
- AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
-
if (includeLinkedChildren)
{
- AddChildrenFromCollection(GetLinkedChildren(user), user, includeLinkedChildren, result, recursive, query, visitedFolders);
+ children = children.Concat(GetLinkedChildren(user)).ToArray();
}
+
+ return AddChildrenFromCollection(children, user, includeLinkedChildren, result, recursive, query, visitedFolders);
}
- private void AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
+ private int AddChildrenFromCollection(IEnumerable<BaseItem> children, User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, InternalItemsQuery query, HashSet<Folder> visitedFolders)
{
- foreach (var child in children)
+ query ??= new InternalItemsQuery();
+ var limit = query.Limit > 0 ? query.Limit : int.MaxValue;
+ query.Limit = 0;
+
+ var visibleChildren = children
+ .Where(e => e.IsVisible(user))
+ .ToArray();
+
+ var realChildren = visibleChildren
+ .Where(e => query is null || UserViewBuilder.FilterItem(e, query))
+ .ToArray();
+
+ if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0))
{
- if (!child.IsVisible(user))
- {
- continue;
- }
+ realChildren = realChildren
+ .OrderBy(e => e.ProductionYear ?? int.MaxValue)
+ .ToArray();
+ }
- if (query is null || UserViewBuilder.FilterItem(child, query))
+ var childCount = realChildren.Length;
+ if (result.Count < limit)
+ {
+ var remainingCount = (int)(limit - result.Count);
+ foreach (var child in realChildren
+ .Skip(query.StartIndex ?? 0)
+ .Take(remainingCount))
{
result[child.Id] = child;
}
+ }
- if (recursive && child.IsFolder)
+ if (recursive)
+ {
+ foreach (var child in visibleChildren
+ .Where(e => e.IsFolder)
+ .OfType<Folder>())
{
- var folder = (Folder)child;
-
- folder.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
+ childCount += child.AddChildren(user, includeLinkedChildren, result, true, query, visitedFolders);
}
}
+
+ return childCount;
}
- public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query)
+ public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
{
ArgumentNullException.ThrowIfNull(user);
var result = new Dictionary<Guid, BaseItem>();
- AddChildren(user, true, result, true, query);
+ totalCount = AddChildren(user, true, result, true, query);
return result.Values.ToArray();
}
@@ -1709,23 +1776,14 @@ namespace MediaBrowser.Controller.Entities
}
}
- public override bool IsPlayed(User user)
+ public override bool IsPlayed(User user, UserItemData userItemData)
{
- var itemsResult = GetItemList(new InternalItemsQuery(user)
- {
- Recursive = true,
- IsFolder = false,
- IsVirtualItem = false,
- EnableTotalRecordCount = false
- });
-
- return itemsResult
- .All(i => i.IsPlayed(user));
+ return ItemRepository.GetIsPlayed(user, Id, true);
}
- public override bool IsUnplayed(User user)
+ public override bool IsUnplayed(User user, UserItemData userItemData)
{
- return !IsPlayed(user);
+ return !IsPlayed(user, userItemData);
}
public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, DtoOptions fields)