2021-05-07 00:39:20 +02:00
#nullable disable
2021-07-23 16:36:27 -07:00
#pragma warning disable CA1002 , CA1721 , CA1819 , CS1591
2020-02-23 10:53:51 +01:00
2019-01-13 21:01:16 +01:00
using System ;
2018-12-27 18:27:57 -05:00
using System.Collections.Generic ;
2024-10-09 10:36:08 +00:00
using System.Collections.Immutable ;
2018-12-27 18:27:57 -05:00
using System.IO ;
using System.Linq ;
2024-06-23 11:40:58 -04:00
using System.Security ;
2019-10-15 17:49:49 +02:00
using System.Text.Json.Serialization ;
2018-12-27 18:27:57 -05:00
using System.Threading ;
using System.Threading.Tasks ;
2024-10-09 10:36:08 +00:00
using J2N.Collections.Generic.Extensions ;
2025-01-26 20:45:28 +00:00
using Jellyfin.Data ;
2020-05-12 22:10:35 -04:00
using Jellyfin.Data.Enums ;
2025-03-25 16:45:00 +01:00
using Jellyfin.Database.Implementations.Entities ;
2025-03-25 15:30:22 +00:00
using Jellyfin.Database.Implementations.Enums ;
2024-01-17 08:51:39 -07:00
using Jellyfin.Extensions ;
2018-12-27 18:27:57 -05:00
using MediaBrowser.Controller.Channels ;
2019-01-13 20:25:32 +01:00
using MediaBrowser.Controller.Collections ;
using MediaBrowser.Controller.Configuration ;
2018-12-27 18:27:57 -05:00
using MediaBrowser.Controller.Dto ;
using MediaBrowser.Controller.Entities.Audio ;
using MediaBrowser.Controller.Entities.Movies ;
2019-01-13 20:25:32 +01:00
using MediaBrowser.Controller.Library ;
2025-06-16 00:21:11 +03:00
using MediaBrowser.Controller.LibraryTaskScheduler ;
2019-01-13 20:25:32 +01:00
using MediaBrowser.Controller.Providers ;
using MediaBrowser.Model.Dto ;
2018-12-27 18:27:57 -05:00
using MediaBrowser.Model.IO ;
2019-01-13 20:25:32 +01:00
using MediaBrowser.Model.Querying ;
2018-12-13 14:18:25 +01:00
using Microsoft.Extensions.Logging ;
2020-05-20 13:07:53 -04:00
using Episode = MediaBrowser . Controller . Entities . TV . Episode ;
using MusicAlbum = MediaBrowser . Controller . Entities . Audio . MusicAlbum ;
using Season = MediaBrowser . Controller . Entities . TV . Season ;
using Series = MediaBrowser . Controller . Entities . TV . Series ;
2018-12-27 18:27:57 -05:00
namespace MediaBrowser.Controller.Entities
{
/// <summary>
2020-06-16 10:37:52 +12:00
/// Class Folder.
2018-12-27 18:27:57 -05:00
/// </summary>
public class Folder : BaseItem
{
2025-09-16 21:08:04 +02:00
private IEnumerable < BaseItem > _children ;
2021-05-13 07:32:02 -06:00
public Folder ( )
{
LinkedChildren = Array . Empty < LinkedChild > ( ) ;
}
2018-12-27 18:27:57 -05:00
public static IUserViewManager UserViewManager { get ; set ; }
2025-06-16 00:21:11 +03:00
public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get ; set ; }
2018-12-27 18:27:57 -05:00
/// <summary>
/// Gets or sets a value indicating whether this instance is root.
/// </summary>
/// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
public bool IsRoot { get ; set ; }
public LinkedChild [ ] LinkedChildren { get ; set ; }
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2018-12-27 18:27:57 -05:00
public DateTime ? DateLastMediaAdded { get ; set ; }
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public override bool SupportsThemeMedia = > true ;
2018-12-27 18:27:57 -05:00
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public virtual bool IsPreSorted = > false ;
2018-12-27 18:27:57 -05:00
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public virtual bool IsPhysicalRoot = > false ;
2018-12-27 18:27:57 -05:00
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public override bool SupportsInheritedParentImages = > true ;
2018-12-27 18:27:57 -05:00
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public override bool SupportsPlayedStatus = > true ;
2018-12-27 18:27:57 -05:00
/// <summary>
/// Gets a value indicating whether this instance is folder.
/// </summary>
/// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public override bool IsFolder = > true ;
2018-12-27 18:27:57 -05:00
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public override bool IsDisplayedAsFolder = > true ;
2018-12-27 18:27:57 -05:00
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public virtual bool SupportsCumulativeRunTimeTicks = > false ;
2018-12-27 18:27:57 -05:00
2019-10-15 17:49:49 +02:00
[JsonIgnore]
2019-01-13 21:31:14 +01:00
public virtual bool SupportsDateLastMediaAdded = > false ;
2018-12-27 18:27:57 -05:00
2021-05-13 07:32:02 -06:00
[JsonIgnore]
public override string FileNameWithoutExtension
{
get
{
if ( IsFileProtocol )
{
return System . IO . Path . GetFileName ( Path ) ;
}
return null ;
}
}
/// <summary>
2025-09-16 21:08:04 +02:00
/// Gets or Sets the actual children.
2021-05-13 07:32:02 -06:00
/// </summary>
/// <value>The actual children.</value>
[JsonIgnore]
2025-09-16 21:08:04 +02:00
public virtual IEnumerable < BaseItem > Children
{
get = > _children ? ? = LoadChildren ( ) ;
set = > _children = value ;
}
2021-05-13 07:32:02 -06:00
/// <summary>
/// Gets thread-safe access to all recursive children of this folder - without regard to user.
/// </summary>
/// <value>The recursive children.</value>
[JsonIgnore]
public IEnumerable < BaseItem > RecursiveChildren = > GetRecursiveChildren ( ) ;
[JsonIgnore]
protected virtual bool SupportsShortcutChildren = > false ;
protected virtual bool FilterLinkedChildrenPerUser = > false ;
[JsonIgnore]
protected override bool SupportsOwnedItems = > base . SupportsOwnedItems | | SupportsShortcutChildren ;
[JsonIgnore]
public virtual bool SupportsUserDataFromChildren
{
get
{
// These are just far too slow.
if ( this is ICollectionFolder )
{
return false ;
}
if ( this is UserView )
{
return false ;
}
if ( this is UserRootFolder )
{
return false ;
}
if ( this is Channel )
{
return false ;
}
if ( SourceType ! = SourceType . Library )
{
return false ;
}
if ( this is IItemByName )
{
if ( this is not IHasDualAccess hasDualAccess | | hasDualAccess . IsAccessedByName )
{
return false ;
}
}
return true ;
}
}
2021-07-23 16:36:27 -07:00
public static ICollectionManager CollectionManager { get ; set ; }
2018-12-27 18:27:57 -05:00
public override bool CanDelete ( )
{
if ( IsRoot )
{
return false ;
}
return base . CanDelete ( ) ;
}
public override bool RequiresRefresh ( )
{
var baseResult = base . RequiresRefresh ( ) ;
if ( SupportsCumulativeRunTimeTicks & & ! RunTimeTicks . HasValue )
{
baseResult = true ;
}
return baseResult ;
}
/// <summary>
/// Adds the child.
/// </summary>
/// <param name="item">The item.</param>
2021-06-06 09:16:41 -06:00
/// <exception cref="InvalidOperationException">Unable to add + item.Name.</exception>
2021-07-10 10:09:02 -06:00
public void AddChild ( BaseItem item )
2018-12-27 18:27:57 -05:00
{
item . SetParent ( this ) ;
2024-01-17 08:51:39 -07:00
if ( item . Id . IsEmpty ( ) )
2018-12-27 18:27:57 -05:00
{
item . Id = LibraryManager . GetNewItemId ( item . Path , item . GetType ( ) ) ;
}
if ( item . DateCreated = = DateTime . MinValue )
{
item . DateCreated = DateTime . UtcNow ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( item . DateModified = = DateTime . MinValue )
{
item . DateModified = DateTime . UtcNow ;
}
LibraryManager . CreateItem ( item , this ) ;
}
2025-02-09 12:19:51 -05:00
public override bool IsVisible ( User user , bool skipAllowedTagsCheck = false )
2018-12-27 18:27:57 -05:00
{
2021-08-28 16:32:50 -06:00
if ( this is ICollectionFolder & & this is not BasePluginFolder )
2018-12-27 18:27:57 -05:00
{
2020-12-13 08:15:26 -07:00
var blockedMediaFolders = user . GetPreferenceValues < Guid > ( PreferenceKind . BlockedMediaFolders ) ;
2020-06-07 13:36:43 -04:00
if ( blockedMediaFolders . Length > 0 )
2018-12-27 18:27:57 -05:00
{
2020-12-11 15:04:14 -07:00
if ( blockedMediaFolders . Contains ( Id ) )
2018-12-27 18:27:57 -05:00
{
return false ;
}
}
else
{
2020-05-12 22:10:35 -04:00
if ( ! user . HasPermission ( PermissionKind . EnableAllFolders )
2020-12-13 08:15:26 -07:00
& & ! user . GetPreferenceValues < Guid > ( PreferenceKind . EnabledFolders ) . Contains ( Id ) )
2018-12-27 18:27:57 -05:00
{
return false ;
}
}
}
2025-02-09 12:19:51 -05:00
return base . IsVisible ( user , skipAllowedTagsCheck ) ;
2018-12-27 18:27:57 -05:00
}
/// <summary>
/// Loads our children. Validation will occur externally.
2020-11-18 13:46:14 +00:00
/// We want this synchronous.
2018-12-27 18:27:57 -05:00
/// </summary>
2021-08-11 13:38:23 -07:00
/// <returns>Returns children.</returns>
2024-10-09 10:36:08 +00:00
protected virtual IReadOnlyList < BaseItem > LoadChildren ( )
2018-12-27 18:27:57 -05:00
{
2020-06-14 21:11:11 +12:00
// logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path);
// just load our children from the repo - the library will be validated and maintained in other processes
2018-12-27 18:27:57 -05:00
return GetCachedChildren ( ) ;
}
public override double? GetRefreshProgress ( )
{
return ProviderManager . GetRefreshProgress ( Id ) ;
}
public Task ValidateChildren ( IProgress < double > progress , CancellationToken cancellationToken )
{
2021-06-06 09:16:41 -06:00
return ValidateChildren ( progress , new MetadataRefreshOptions ( new DirectoryService ( FileSystem ) ) , cancellationToken : cancellationToken ) ;
2018-12-27 18:27:57 -05:00
}
/// <summary>
2020-06-16 10:37:52 +12:00
/// Validates that the children of the folder still exist.
2018-12-27 18:27:57 -05:00
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="metadataRefreshOptions">The metadata refresh options.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
2024-04-17 14:41:19 +08:00
/// <param name="allowRemoveRoot">remove item even this folder is root.</param>
2021-06-06 09:16:41 -06:00
/// <param name="cancellationToken">The cancellation token.</param>
2018-12-27 18:27:57 -05:00
/// <returns>Task.</returns>
2024-04-17 14:41:19 +08:00
public Task ValidateChildren ( IProgress < double > progress , MetadataRefreshOptions metadataRefreshOptions , bool recursive = true , bool allowRemoveRoot = false , CancellationToken cancellationToken = default )
2018-12-27 18:27:57 -05:00
{
2025-09-16 21:08:04 +02:00
Children = null ; // invalidate cached children.
2024-04-17 21:32:21 +08:00
return ValidateChildrenInternal ( progress , recursive , true , allowRemoveRoot , metadataRefreshOptions , metadataRefreshOptions . DirectoryService , cancellationToken ) ;
2018-12-27 18:27:57 -05:00
}
private Dictionary < Guid , BaseItem > GetActualChildrenDictionary ( )
{
var dictionary = new Dictionary < Guid , BaseItem > ( ) ;
2025-09-16 21:08:04 +02:00
Children = null ; // invalidate cached children.
2018-12-27 18:27:57 -05:00
var childrenList = Children . ToList ( ) ;
foreach ( var child in childrenList )
{
var id = child . Id ;
if ( dictionary . ContainsKey ( id ) )
{
2020-10-12 20:05:11 +02:00
Logger . LogError (
2021-11-09 13:14:31 +01:00
"Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}" ,
2018-12-27 18:27:57 -05:00
Path ? ? Name ,
child . Path ? ? child . Name ) ;
}
else
{
dictionary [ id ] = child ;
}
}
return dictionary ;
}
/// <summary>
/// Validates the children internal.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
2024-04-17 14:41:19 +08:00
/// <param name="allowRemoveRoot">remove item even this folder is root.</param>
2018-12-27 18:27:57 -05:00
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="directoryService">The directory service.</param>
2021-06-06 09:16:41 -06:00
/// <param name="cancellationToken">The cancellation token.</param>
2018-12-27 18:27:57 -05:00
/// <returns>Task.</returns>
2024-04-17 14:41:19 +08:00
protected virtual async Task ValidateChildrenInternal ( IProgress < double > progress , bool recursive , bool refreshChildMetadata , bool allowRemoveRoot , MetadataRefreshOptions refreshOptions , IDirectoryService directoryService , CancellationToken cancellationToken )
2018-12-27 18:27:57 -05:00
{
if ( recursive )
{
ProviderManager . OnRefreshStart ( this ) ;
}
try
{
2024-04-17 14:41:19 +08:00
await ValidateChildrenInternal2 ( progress , recursive , refreshChildMetadata , allowRemoveRoot , refreshOptions , directoryService , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-27 18:27:57 -05:00
}
finally
{
if ( recursive )
{
ProviderManager . OnRefreshComplete ( this ) ;
}
}
}
2024-05-05 22:21:40 +08:00
private static bool IsLibraryFolderAccessible ( IDirectoryService directoryService , BaseItem item , bool checkCollection )
2024-03-18 16:09:00 +01:00
{
2024-05-05 22:21:40 +08:00
if ( ! checkCollection & & ( item is BoxSet | | string . Equals ( item . FileNameWithoutExtension , "collections" , StringComparison . OrdinalIgnoreCase ) ) )
{
return true ;
}
2024-03-18 16:09:00 +01:00
// For top parents i.e. Library folders, skip the validation if it's empty or inaccessible
if ( item . IsTopParent & & ! directoryService . IsAccessible ( item . ContainingFolderPath ) )
{
Logger . LogWarning ( "Library folder {LibraryFolderPath} is inaccessible or empty, skipping" , item . ContainingFolderPath ) ;
return false ;
}
return true ;
}
2024-04-17 14:41:19 +08:00
private async Task ValidateChildrenInternal2 ( IProgress < double > progress , bool recursive , bool refreshChildMetadata , bool allowRemoveRoot , MetadataRefreshOptions refreshOptions , IDirectoryService directoryService , CancellationToken cancellationToken )
2018-12-27 18:27:57 -05:00
{
2024-05-05 22:21:40 +08:00
if ( ! IsLibraryFolderAccessible ( directoryService , this , allowRemoveRoot ) )
2024-03-18 16:09:00 +01:00
{
return ;
}
2018-12-27 18:27:57 -05:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
var validChildren = new List < BaseItem > ( ) ;
var validChildrenNeedGeneration = false ;
if ( IsFileProtocol )
{
2024-06-06 14:30:32 -04:00
IEnumerable < BaseItem > nonCachedChildren = [ ] ;
2018-12-27 18:27:57 -05:00
try
{
nonCachedChildren = GetNonCachedChildren ( directoryService ) ;
}
2024-06-23 11:40:58 -04:00
catch ( IOException ex )
{
Logger . LogError ( ex , "Error retrieving children from file system" ) ;
}
catch ( SecurityException ex )
{
Logger . LogError ( ex , "Error retrieving children from file system" ) ;
}
2018-12-27 18:27:57 -05:00
catch ( Exception ex )
{
2024-06-23 11:40:58 -04:00
Logger . LogError ( ex , "Error retrieving children" ) ;
return ;
2018-12-27 18:27:57 -05:00
}
2020-09-30 19:33:34 -07:00
progress . Report ( ProgressHelpers . RetrievedChildren ) ;
2018-12-27 18:27:57 -05:00
if ( recursive )
{
2020-09-30 19:33:34 -07:00
ProviderManager . OnRefreshProgress ( this , ProgressHelpers . RetrievedChildren ) ;
2018-12-27 18:27:57 -05:00
}
2020-02-22 15:04:52 +09:00
// Build a dictionary of the current children we have now by Id so we can compare quickly and easily
2018-12-27 18:27:57 -05:00
var currentChildren = GetActualChildrenDictionary ( ) ;
2020-02-22 15:04:52 +09:00
// Create a list for our validated children
2018-12-27 18:27:57 -05:00
var newItems = new List < BaseItem > ( ) ;
cancellationToken . ThrowIfCancellationRequested ( ) ;
foreach ( var child in nonCachedChildren )
{
2024-05-05 22:21:40 +08:00
if ( ! IsLibraryFolderAccessible ( directoryService , child , allowRemoveRoot ) )
2024-03-18 16:09:00 +01:00
{
continue ;
}
2019-01-17 20:24:39 +01:00
if ( currentChildren . TryGetValue ( child . Id , out BaseItem currentChild ) )
2018-12-27 18:27:57 -05:00
{
validChildren . Add ( currentChild ) ;
if ( currentChild . UpdateFromResolvedItem ( child ) > ItemUpdateType . None )
{
2020-08-21 22:01:19 +02:00
await currentChild . UpdateToRepositoryAsync ( ItemUpdateType . MetadataImport , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-27 18:27:57 -05:00
}
2020-12-10 18:02:12 +01:00
else
{
// metadata is up-to-date; make sure DB has correct images dimensions and hash
await LibraryManager . UpdateImagesAsync ( currentChild ) . ConfigureAwait ( false ) ;
}
2018-12-27 18:27:57 -05:00
continue ;
}
// Brand new item - needs to be added
child . SetParent ( this ) ;
newItems . Add ( child ) ;
validChildren . Add ( child ) ;
}
2024-05-11 10:56:14 -04:00
// That's all the new and changed ones - now see if any have been removed and need cleanup
2024-05-11 03:54:41 +08:00
var itemsRemoved = currentChildren . Values . Except ( validChildren ) . ToList ( ) ;
2024-04-17 21:32:21 +08:00
var shouldRemove = ! IsRoot | | allowRemoveRoot ;
2024-03-18 16:09:00 +01:00
// If it's an AggregateFolder, don't remove
2024-05-11 03:54:41 +08:00
if ( shouldRemove & & itemsRemoved . Count > 0 )
2018-12-27 18:27:57 -05:00
{
foreach ( var item in itemsRemoved )
{
2018-12-20 11:38:42 +01:00
if ( item . IsFileProtocol )
2018-12-27 18:27:57 -05:00
{
2021-11-09 22:29:33 +01:00
Logger . LogDebug ( "Removed item: {Path}" , item . Path ) ;
2018-12-27 18:27:57 -05:00
item . SetParent ( null ) ;
LibraryManager . DeleteItem ( item , new DeleteOptions { DeleteFileLocation = false } , this , false ) ;
}
}
2024-03-18 16:09:00 +01:00
}
2018-12-27 18:27:57 -05:00
2024-03-18 16:09:00 +01:00
if ( newItems . Count > 0 )
{
2025-02-09 18:30:53 +01:00
LibraryManager . CreateItems ( newItems , this , cancellationToken ) ;
2018-12-27 18:27:57 -05:00
}
}
else
{
validChildrenNeedGeneration = true ;
}
2020-09-30 19:33:34 -07:00
progress . Report ( ProgressHelpers . UpdatedChildItems ) ;
2018-12-27 18:27:57 -05:00
if ( recursive )
{
2020-09-30 19:33:34 -07:00
ProviderManager . OnRefreshProgress ( this , ProgressHelpers . UpdatedChildItems ) ;
2018-12-27 18:27:57 -05:00
}
cancellationToken . ThrowIfCancellationRequested ( ) ;
if ( recursive )
{
var folder = this ;
2024-02-06 09:58:25 -05:00
var innerProgress = new Progress < double > ( innerPercent = >
2018-12-27 18:27:57 -05:00
{
2020-09-30 19:33:34 -07:00
var percent = ProgressHelpers . GetProgress ( ProgressHelpers . UpdatedChildItems , ProgressHelpers . ScannedSubfolders , innerPercent ) ;
progress . Report ( percent ) ;
2024-04-12 17:45:15 -06:00
ProviderManager . OnRefreshProgress ( folder , percent ) ;
2018-12-27 18:27:57 -05:00
} ) ;
if ( validChildrenNeedGeneration )
{
validChildren = Children . ToList ( ) ;
validChildrenNeedGeneration = false ;
}
await ValidateSubFolders ( validChildren . OfType < Folder > ( ) . ToList ( ) , directoryService , innerProgress , cancellationToken ) . ConfigureAwait ( false ) ;
}
if ( refreshChildMetadata )
{
2020-09-30 19:33:34 -07:00
progress . Report ( ProgressHelpers . ScannedSubfolders ) ;
2018-12-27 18:27:57 -05:00
if ( recursive )
{
2020-09-30 19:33:34 -07:00
ProviderManager . OnRefreshProgress ( this , ProgressHelpers . ScannedSubfolders ) ;
2018-12-27 18:27:57 -05:00
}
var container = this as IMetadataContainer ;
var folder = this ;
2024-02-06 09:58:25 -05:00
var innerProgress = new Progress < double > ( innerPercent = >
2018-12-27 18:27:57 -05:00
{
2020-09-30 19:33:34 -07:00
var percent = ProgressHelpers . GetProgress ( ProgressHelpers . ScannedSubfolders , ProgressHelpers . RefreshedMetadata , innerPercent ) ;
progress . Report ( percent ) ;
2018-12-27 18:27:57 -05:00
if ( recursive )
{
2024-04-12 17:45:15 -06:00
ProviderManager . OnRefreshProgress ( folder , percent ) ;
2018-12-27 18:27:57 -05:00
}
} ) ;
2022-12-05 15:01:13 +01:00
if ( container is not null )
2018-12-27 18:27:57 -05:00
{
await RefreshAllMetadataForContainer ( container , refreshOptions , innerProgress , cancellationToken ) . ConfigureAwait ( false ) ;
}
else
{
if ( validChildrenNeedGeneration )
{
2025-09-16 21:08:04 +02:00
Children = null ; // invalidate cached children.
2018-12-27 18:27:57 -05:00
validChildren = Children . ToList ( ) ;
}
2020-09-30 19:33:34 -07:00
await RefreshMetadataRecursive ( validChildren , refreshOptions , recursive , innerProgress , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-27 18:27:57 -05:00
}
}
}
2024-10-31 17:02:06 +01:00
private async Task RefreshMetadataRecursive ( IList < BaseItem > children , MetadataRefreshOptions refreshOptions , bool recursive , IProgress < double > progress , CancellationToken cancellationToken )
2018-12-27 18:27:57 -05:00
{
2024-10-31 17:02:06 +01:00
await RunTasks (
2020-10-01 16:24:35 -07:00
( baseItem , innerProgress ) = > RefreshChildMetadata ( baseItem , refreshOptions , recursive & & baseItem . IsFolder , innerProgress , cancellationToken ) ,
children ,
progress ,
2024-10-31 17:02:06 +01:00
cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-27 18:27:57 -05:00
}
private async Task RefreshAllMetadataForContainer ( IMetadataContainer container , MetadataRefreshOptions refreshOptions , IProgress < double > progress , CancellationToken cancellationToken )
{
2023-06-22 05:01:47 +02:00
if ( container is Series series )
{
await series . RefreshMetadata ( refreshOptions , cancellationToken ) . ConfigureAwait ( false ) ;
}
2020-06-16 09:43:52 +12:00
2023-06-22 05:01:47 +02:00
await container . RefreshAllMetadata ( refreshOptions , progress , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-27 18:27:57 -05:00
}
private async Task RefreshChildMetadata ( BaseItem child , MetadataRefreshOptions refreshOptions , bool recursive , IProgress < double > progress , CancellationToken cancellationToken )
{
2023-06-22 05:01:47 +02:00
if ( child is IMetadataContainer container )
2018-12-27 18:27:57 -05:00
{
await RefreshAllMetadataForContainer ( container , refreshOptions , progress , cancellationToken ) . ConfigureAwait ( false ) ;
}
else
{
if ( refreshOptions . RefreshItem ( child ) )
{
2023-06-22 05:01:47 +02:00
await child . RefreshMetadata ( refreshOptions , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-27 18:27:57 -05:00
}
2018-12-20 11:38:42 +01:00
if ( recursive & & child is Folder folder )
2018-12-27 18:27:57 -05:00
{
2025-09-16 21:08:04 +02:00
folder . Children = null ; // invalidate cached children.
2025-09-12 21:58:16 +02:00
await folder . RefreshMetadataRecursive ( folder . Children . Except ( [ this , child ] ) . ToList ( ) , refreshOptions , true , progress , cancellationToken ) . ConfigureAwait ( false ) ;
2018-12-27 18:27:57 -05:00
}
}
}
/// <summary>
/// Refreshes the children.
/// </summary>
/// <param name="children">The children.</param>
/// <param name="directoryService">The directory service.</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
2024-10-31 17:02:06 +01:00
private async Task ValidateSubFolders ( IList < Folder > children , IDirectoryService directoryService , IProgress < double > progress , CancellationToken cancellationToken )
2018-12-27 18:27:57 -05:00
{
2024-10-31 17:02:06 +01:00
await RunTasks (
2024-04-17 14:41:19 +08:00
( folder , innerProgress ) = > folder . ValidateChildrenInternal ( innerProgress , true , false , false , null , directoryService , cancellationToken ) ,
2020-10-01 16:24:35 -07:00
children ,
progress ,
2024-10-31 17:02:06 +01:00
cancellationToken ) . ConfigureAwait ( false ) ;
2020-09-30 19:33:34 -07:00
}
2018-12-27 18:27:57 -05:00
2020-09-30 19:33:34 -07:00
/// <summary>
2020-10-01 16:24:35 -07:00
/// Runs an action block on a list of children.
2020-09-30 19:33:34 -07:00
/// </summary>
2020-10-01 16:24:35 -07:00
/// <param name="task">The task to run for each child.</param>
/// <param name="children">The list of children.</param>
2020-09-30 19:33:34 -07:00
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
2020-10-01 16:24:35 -07:00
private async Task RunTasks < T > ( Func < T , IProgress < double > , Task > task , IList < T > children , IProgress < double > progress , CancellationToken cancellationToken )
2020-09-30 19:33:34 -07:00
{
2025-06-16 00:21:11 +03:00
await LimitedConcurrencyLibraryScheduler
. Enqueue (
children . ToArray ( ) ,
task ,
progress ,
cancellationToken )
. ConfigureAwait ( false ) ;
2018-12-27 18:27:57 -05:00
}
/// <summary>
2020-06-16 10:37:52 +12:00
/// Get the children of this folder from the actual file system.
2018-12-27 18:27:57 -05:00
/// </summary>
/// <returns>IEnumerable{BaseItem}.</returns>
2021-07-23 16:36:27 -07:00
/// <param name="directoryService">The directory service to use for operation.</param>
/// <returns>Returns set of base items.</returns>
2018-12-27 18:27:57 -05:00
protected virtual IEnumerable < BaseItem > GetNonCachedChildren ( IDirectoryService directoryService )
{
var collectionType = LibraryManager . GetContentType ( this ) ;
var libraryOptions = LibraryManager . GetLibraryOptions ( this ) ;
return LibraryManager . ResolvePaths ( GetFileSystemChildren ( directoryService ) , directoryService , this , libraryOptions , collectionType ) ;
}
/// <summary>
2020-06-16 10:37:52 +12:00
/// Get our children from the repo - stubbed for now.
2018-12-27 18:27:57 -05:00
/// </summary>
/// <returns>IEnumerable{BaseItem}.</returns>
2024-10-09 10:36:08 +00:00
protected IReadOnlyList < BaseItem > GetCachedChildren ( )
2018-12-27 18:27:57 -05:00
{
return ItemRepository . GetItemList ( new InternalItemsQuery
{
Parent = this ,
GroupByPresentationUniqueKey = false ,
DtoOptions = new DtoOptions ( true )
} ) ;
}
2020-05-20 13:07:53 -04:00
public virtual int GetChildCount ( User user )
2018-12-27 18:27:57 -05:00
{
if ( LinkedChildren . Length > 0 )
{
2021-08-28 16:32:50 -06:00
if ( this is not ICollectionFolder )
2018-12-27 18:27:57 -05:00
{
return GetChildren ( user , true ) . Count ;
}
}
var result = GetItems ( new InternalItemsQuery ( user )
{
Recursive = false ,
Limit = 0 ,
Parent = this ,
DtoOptions = new DtoOptions ( false )
{
EnableImages = false
}
} ) ;
return result . TotalRecordCount ;
}
2020-05-20 13:07:53 -04:00
public virtual int GetRecursiveChildCount ( User user )
2018-12-27 18:27:57 -05:00
{
return GetItems ( new InternalItemsQuery ( user )
{
Recursive = true ,
IsFolder = false ,
IsVirtualItem = false ,
EnableTotalRecordCount = true ,
Limit = 0 ,
DtoOptions = new DtoOptions ( false )
{
EnableImages = false
}
} ) . TotalRecordCount ;
}
public QueryResult < BaseItem > QueryRecursive ( InternalItemsQuery query )
{
var user = query . User ;
if ( ! query . ForceDirect & & RequiresPostFiltering ( query ) )
{
IEnumerable < BaseItem > items ;
Func < BaseItem , bool > filter = i = > UserViewBuilder . Filter ( i , user , query , UserDataManager , LibraryManager ) ;
2025-09-16 21:08:04 +02:00
var totalCount = 0 ;
2022-12-05 15:00:20 +01:00
if ( query . User is null )
2018-12-27 18:27:57 -05:00
{
items = GetRecursiveChildren ( filter ) ;
2025-09-16 21:08:04 +02:00
totalCount = items . Count ( ) ;
2018-12-27 18:27:57 -05:00
}
else
{
2025-09-16 21:08:04 +02:00
items = GetRecursiveChildren ( user , query , out totalCount ) ;
query . Limit = null ;
query . StartIndex = null ; // override these here as they have already been applied
2018-12-27 18:27:57 -05:00
}
2025-09-16 21:08:04 +02:00
var result = PostFilterAndSort ( items , query ) ;
result . TotalRecordCount = totalCount ;
return result ;
2018-12-27 18:27:57 -05:00
}
2022-02-21 14:15:09 +01:00
if ( this is not UserRootFolder
& & this is not AggregateFolder
2024-01-17 08:51:39 -07:00
& & query . ParentId . IsEmpty ( ) )
2018-12-27 18:27:57 -05:00
{
2019-01-24 18:48:37 +01:00
query . Parent = this ;
2018-12-27 18:27:57 -05:00
}
if ( RequiresPostFiltering2 ( query ) )
{
return QueryWithPostFiltering2 ( query ) ;
}
return LibraryManager . GetItemsResult ( query ) ;
}
2023-05-26 10:59:49 +02:00
protected QueryResult < BaseItem > QueryWithPostFiltering2 ( InternalItemsQuery query )
2018-12-27 18:27:57 -05:00
{
var startIndex = query . StartIndex ;
var limit = query . Limit ;
query . StartIndex = null ;
query . Limit = null ;
2019-09-02 08:19:29 +02:00
IEnumerable < BaseItem > itemsList = LibraryManager . GetItemList ( query ) ;
2018-12-27 18:27:57 -05:00
var user = query . User ;
2022-12-05 15:01:13 +01:00
if ( user is not null )
2018-12-27 18:27:57 -05:00
{
// needed for boxsets
2019-09-02 08:19:29 +02:00
itemsList = itemsList . Where ( i = > i . IsVisibleStandalone ( query . User ) ) ;
2018-12-27 18:27:57 -05:00
}
2019-09-02 08:19:29 +02:00
IEnumerable < BaseItem > returnItems ;
2018-12-27 18:27:57 -05:00
int totalCount = 0 ;
if ( query . EnableTotalRecordCount )
{
2019-09-02 08:19:29 +02:00
var itemArray = itemsList . ToArray ( ) ;
totalCount = itemArray . Length ;
returnItems = itemArray ;
2018-12-27 18:27:57 -05:00
}
else
{
2019-09-02 08:19:29 +02:00
returnItems = itemsList ;
2018-12-27 18:27:57 -05:00
}
if ( limit . HasValue )
{
2019-09-02 08:19:29 +02:00
returnItems = returnItems . Skip ( startIndex ? ? 0 ) . Take ( limit . Value ) ;
2018-12-27 18:27:57 -05:00
}
else if ( startIndex . HasValue )
{
2019-09-02 08:19:29 +02:00
returnItems = returnItems . Skip ( startIndex . Value ) ;
2018-12-27 18:27:57 -05:00
}
2022-01-20 08:46:17 -07:00
return new QueryResult < BaseItem > (
query . StartIndex ,
totalCount ,
returnItems . ToArray ( ) ) ;
2018-12-27 18:27:57 -05:00
}
private bool RequiresPostFiltering2 ( InternalItemsQuery query )
{
2021-12-11 19:31:30 -07:00
if ( query . IncludeItemTypes . Length = = 1 & & query . IncludeItemTypes [ 0 ] = = BaseItemKind . BoxSet )
2018-12-27 18:27:57 -05:00
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to BoxSet query" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
return false ;
}
private bool RequiresPostFiltering ( InternalItemsQuery query )
{
if ( LinkedChildren . Length > 0 )
{
2021-08-28 16:32:50 -06:00
if ( this is not ICollectionFolder )
2018-12-27 18:27:57 -05:00
{
2021-11-09 22:29:33 +01:00
Logger . LogDebug ( "{Type}: Query requires post-filtering due to LinkedChildren." , GetType ( ) . Name ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
}
// Filter by Video3DFormat
if ( query . Is3D . HasValue )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to Is3D" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( query . HasOfficialRating . HasValue )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to HasOfficialRating" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( query . IsPlaceHolder . HasValue )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to IsPlaceHolder" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( query . HasSpecialFeature . HasValue )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to HasSpecialFeature" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( query . HasSubtitles . HasValue )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to HasSubtitles" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( query . HasTrailer . HasValue )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to HasTrailer" ) ;
2018-12-27 18:27:57 -05:00
return true ;
2022-02-27 23:38:00 +01:00
}
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 ;
2018-12-27 18:27:57 -05:00
}
// Filter by VideoType
if ( query . VideoTypes . Length > 0 )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to VideoTypes" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( CollapseBoxSetItems ( query , this , query . User , ConfigurationManager ) )
{
2018-12-13 14:18:25 +01:00
Logger . LogDebug ( "Query requires post-filtering due to CollapseBoxSetItems" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
2024-01-17 08:51:39 -07:00
if ( ! query . AdjacentTo . IsNullOrEmpty ( ) )
2018-12-27 18:27:57 -05:00
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to AdjacentTo" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( query . SeriesStatuses . Length > 0 )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to SeriesStatuses" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( query . AiredDuringSeason . HasValue )
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to AiredDuringSeason" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
if ( query . IsPlayed . HasValue )
{
2021-12-11 19:31:30 -07:00
if ( query . IncludeItemTypes . Length = = 1 & & query . IncludeItemTypes . Contains ( BaseItemKind . Series ) )
2018-12-27 18:27:57 -05:00
{
2018-12-20 11:38:42 +01:00
Logger . LogDebug ( "Query requires post-filtering due to IsPlayed" ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
}
return false ;
}
2020-03-02 12:36:44 +03:00
private static BaseItem [ ] SortItemsByRequest ( InternalItemsQuery query , IReadOnlyList < BaseItem > items )
2020-02-27 20:21:34 +03:00
{
2022-09-23 23:09:34 -04:00
return items . OrderBy ( i = > Array . IndexOf ( query . ItemIds , i . Id ) ) . ToArray ( ) ;
2020-02-27 20:21:34 +03:00
}
2018-12-27 18:27:57 -05:00
public QueryResult < BaseItem > GetItems ( InternalItemsQuery query )
{
if ( query . ItemIds . Length > 0 )
{
2020-02-27 20:11:40 +03:00
var result = LibraryManager . GetItemsResult ( query ) ;
2020-02-27 20:14:56 +03:00
if ( query . OrderBy . Count = = 0 & & query . ItemIds . Length > 1 )
2020-02-27 20:11:40 +03:00
{
2020-02-27 20:21:34 +03:00
result . Items = SortItemsByRequest ( query , result . Items ) ;
2020-02-27 20:11:40 +03:00
}
2020-03-02 12:36:44 +03:00
2020-02-27 20:11:40 +03:00
return result ;
2018-12-27 18:27:57 -05:00
}
return GetItemsInternal ( query ) ;
}
2019-02-26 20:47:23 +01:00
public IReadOnlyList < BaseItem > GetItemList ( InternalItemsQuery query )
2018-12-27 18:27:57 -05:00
{
query . EnableTotalRecordCount = false ;
if ( query . ItemIds . Length > 0 )
{
2020-02-27 20:11:40 +03:00
var result = LibraryManager . GetItemList ( query ) ;
2020-02-27 20:14:56 +03:00
if ( query . OrderBy . Count = = 0 & & query . ItemIds . Length > 1 )
2020-02-27 20:11:40 +03:00
{
2020-02-27 20:21:34 +03:00
return SortItemsByRequest ( query , result ) ;
2020-02-27 20:11:40 +03:00
}
2020-03-02 12:36:44 +03:00
2020-04-19 11:16:09 +02:00
return result ;
2018-12-27 18:27:57 -05:00
}
return GetItemsInternal ( query ) . Items ;
}
protected virtual QueryResult < BaseItem > GetItemsInternal ( InternalItemsQuery query )
{
if ( SourceType = = SourceType . Channel )
{
try
{
query . Parent = this ;
2020-05-12 22:10:35 -04:00
query . ChannelIds = new [ ] { ChannelId } ;
2018-12-27 18:27:57 -05:00
// Don't blow up here because it could cause parent screens with other content to fail
2024-02-06 09:50:46 -05:00
return ChannelManager . GetChannelItemsInternal ( query , new Progress < double > ( ) , CancellationToken . None ) . GetAwaiter ( ) . GetResult ( ) ;
2018-12-27 18:27:57 -05:00
}
catch
{
// Already logged at lower levels
return new QueryResult < BaseItem > ( ) ;
}
}
if ( query . Recursive )
{
return QueryRecursive ( query ) ;
}
var user = query . User ;
Func < BaseItem , bool > filter = i = > UserViewBuilder . Filter ( i , user , query , UserDataManager , LibraryManager ) ;
IEnumerable < BaseItem > items ;
2025-09-16 21:08:04 +02:00
int totalItemCount = 0 ;
2022-12-05 15:00:20 +01:00
if ( query . User is null )
2018-12-27 18:27:57 -05:00
{
items = Children . Where ( filter ) ;
2025-09-16 21:08:04 +02:00
totalItemCount = items . Count ( ) ;
2018-12-27 18:27:57 -05:00
}
else
{
2021-05-17 23:34:50 +01:00
// need to pass this param to the children.
var childQuery = new InternalItemsQuery
{
2025-09-16 21:08:04 +02:00
DisplayAlbumFolders = query . DisplayAlbumFolders ,
Limit = query . Limit ,
StartIndex = query . StartIndex
2021-05-17 23:34:50 +01:00
} ;
2025-09-16 21:08:04 +02:00
items = GetChildren ( user , true , out totalItemCount , childQuery ) . Where ( filter ) ;
query . Limit = null ;
query . StartIndex = null ;
2018-12-27 18:27:57 -05:00
}
2025-09-16 21:08:04 +02:00
var result = PostFilterAndSort ( items , query ) ;
result . TotalRecordCount = totalItemCount ;
return result ;
2018-12-27 18:27:57 -05:00
}
2025-06-18 00:37:43 +02:00
protected QueryResult < BaseItem > PostFilterAndSort ( IEnumerable < BaseItem > items , InternalItemsQuery query )
2018-12-27 18:27:57 -05:00
{
var user = query . User ;
// Check recursive - don't substitute in plain folder views
2022-12-05 15:01:13 +01:00
if ( user is not null )
2018-12-27 18:27:57 -05:00
{
items = CollapseBoxSetItemsIfNeeded ( items , query , this , user , ConfigurationManager , CollectionManager ) ;
}
2025-06-16 00:21:11 +03:00
#pragma warning disable CA1309
2018-12-27 18:27:57 -05:00
if ( ! string . IsNullOrEmpty ( query . NameStartsWithOrGreater ) )
{
2021-09-09 15:59:13 +02:00
items = items . Where ( i = > string . Compare ( query . NameStartsWithOrGreater , i . SortName , StringComparison . InvariantCultureIgnoreCase ) < 1 ) ;
2018-12-27 18:27:57 -05:00
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( ! string . IsNullOrEmpty ( query . NameStartsWith ) )
{
2021-09-09 15:59:13 +02:00
items = items . Where ( i = > i . SortName . StartsWith ( query . NameStartsWith , StringComparison . InvariantCultureIgnoreCase ) ) ;
2018-12-27 18:27:57 -05:00
}
if ( ! string . IsNullOrEmpty ( query . NameLessThan ) )
{
2021-09-09 15:59:13 +02:00
items = items . Where ( i = > string . Compare ( query . NameLessThan , i . SortName , StringComparison . InvariantCultureIgnoreCase ) = = 1 ) ;
2018-12-27 18:27:57 -05:00
}
2025-06-16 00:21:11 +03:00
#pragma warning restore CA1309
2018-12-27 18:27:57 -05:00
// This must be the last filter
2024-01-17 08:51:39 -07:00
if ( ! query . AdjacentTo . IsNullOrEmpty ( ) )
2018-12-27 18:27:57 -05:00
{
2022-08-14 12:47:25 +02:00
items = UserViewBuilder . FilterForAdjacency ( items . ToList ( ) , query . AdjacentTo . Value ) ;
2018-12-27 18:27:57 -05:00
}
2025-06-18 00:37:43 +02:00
return UserViewBuilder . SortAndPage ( items , null , query , LibraryManager ) ;
2018-12-27 18:27:57 -05:00
}
2020-05-12 22:10:35 -04:00
private static IEnumerable < BaseItem > CollapseBoxSetItemsIfNeeded (
IEnumerable < BaseItem > items ,
2018-12-27 18:27:57 -05:00
InternalItemsQuery query ,
BaseItem queryParent ,
2020-05-20 13:07:53 -04:00
User user ,
2020-05-12 22:10:35 -04:00
IServerConfigurationManager configurationManager ,
ICollectionManager collectionManager )
2018-12-27 18:27:57 -05:00
{
2022-10-06 20:21:23 +02:00
ArgumentNullException . ThrowIfNull ( items ) ;
2018-12-27 18:27:57 -05:00
if ( CollapseBoxSetItems ( query , queryParent , user , configurationManager ) )
{
items = collectionManager . CollapseItemsWithinBoxSets ( items , user ) ;
}
return items ;
}
2020-10-12 20:05:11 +02:00
private static bool CollapseBoxSetItems (
InternalItemsQuery query ,
2018-12-27 18:27:57 -05:00
BaseItem queryParent ,
2020-05-20 13:07:53 -04:00
User user ,
2018-12-27 18:27:57 -05:00
IServerConfigurationManager configurationManager )
{
// Could end up stuck in a loop like this
if ( queryParent is BoxSet )
{
return false ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( queryParent is Season )
{
return false ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( queryParent is MusicAlbum )
{
return false ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( queryParent is MusicArtist )
{
return false ;
}
var param = query . CollapseBoxSetItems ;
if ( ! param . HasValue )
{
2025-03-28 13:54:12 +01:00
if ( user is not null & & query . IncludeItemTypes . Any ( type = >
( type = = BaseItemKind . Movie & & ! configurationManager . Configuration . EnableGroupingMoviesIntoCollections ) | |
( type = = BaseItemKind . Series & & ! configurationManager . Configuration . EnableGroupingShowsIntoCollections ) ) )
2018-12-27 18:27:57 -05:00
{
return false ;
}
2025-03-28 13:54:12 +01:00
if ( query . IncludeItemTypes . Length = = 0
| | query . IncludeItemTypes . Any ( type = > type = = BaseItemKind . Movie | | type = = BaseItemKind . Series ) )
2018-12-27 18:27:57 -05:00
{
param = true ;
}
}
return param . HasValue & & param . Value & & AllowBoxSetCollapsing ( query ) ;
}
private static bool AllowBoxSetCollapsing ( InternalItemsQuery request )
{
if ( request . IsFavorite . HasValue )
{
return false ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( request . IsFavoriteOrLiked . HasValue )
{
return false ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( request . IsLiked . HasValue )
{
return false ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( request . IsPlayed . HasValue )
{
return false ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( request . IsResumable . HasValue )
{
return false ;
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
if ( request . IsFolder . HasValue )
{
return false ;
}
2020-11-16 20:29:46 -07:00
if ( request . Genres . Count > 0 )
2018-12-27 18:27:57 -05:00
{
return false ;
}
2020-11-16 20:29:46 -07:00
if ( request . GenreIds . Count > 0 )
2018-12-27 18:27:57 -05:00
{
return false ;
}
if ( request . HasImdbId . HasValue )
{
return false ;
}
if ( request . HasOfficialRating . HasValue )
{
return false ;
}
if ( request . HasOverview . HasValue )
{
return false ;
}
if ( request . HasParentalRating . HasValue )
{
return false ;
}
if ( request . HasSpecialFeature . HasValue )
{
return false ;
}
if ( request . HasSubtitles . HasValue )
{
return false ;
}
if ( request . HasThemeSong . HasValue )
{
return false ;
}
if ( request . HasThemeVideo . HasValue )
{
return false ;
}
if ( request . HasTmdbId . HasValue )
{
return false ;
}
if ( request . HasTrailer . HasValue )
{
return false ;
}
if ( request . ImageTypes . Length > 0 )
{
return false ;
}
if ( request . Is3D . HasValue )
{
return false ;
}
2025-02-21 05:39:38 -05:00
if ( request . Is4K . HasValue )
{
return false ;
}
2018-12-27 18:27:57 -05:00
if ( request . IsHD . HasValue )
{
return false ;
}
if ( request . IsLocked . HasValue )
{
return false ;
}
if ( request . IsPlaceHolder . HasValue )
{
return false ;
}
if ( ! string . IsNullOrWhiteSpace ( request . Person ) )
{
return false ;
}
if ( request . PersonIds . Length > 0 )
{
return false ;
}
if ( request . ItemIds . Length > 0 )
{
return false ;
}
if ( request . StudioIds . Length > 0 )
{
return false ;
}
if ( request . VideoTypes . Length > 0 )
{
return false ;
}
if ( request . Years . Length > 0 )
{
return false ;
}
if ( request . Tags . Length > 0 )
{
return false ;
}
if ( request . OfficialRatings . Length > 0 )
{
return false ;
}
2025-05-22 09:05:14 -04:00
if ( request . MinIndexNumber . HasValue )
2018-12-27 18:27:57 -05:00
{
return false ;
}
2025-05-22 09:05:14 -04:00
if ( request . OrderBy . Any ( o = >
o . OrderBy = = ItemSortBy . CommunityRating | |
o . OrderBy = = ItemSortBy . CriticRating | |
o . OrderBy = = ItemSortBy . Runtime ) )
2018-12-27 18:27:57 -05:00
{
return false ;
}
return true ;
}
2025-09-16 21:08:04 +02:00
public virtual IReadOnlyList < BaseItem > GetChildren ( User user , bool includeLinkedChildren , out int totalItemCount , InternalItemsQuery query = null )
2018-12-27 18:27:57 -05:00
{
2022-10-06 20:21:23 +02:00
ArgumentNullException . ThrowIfNull ( user ) ;
2025-09-16 21:08:04 +02:00
query ? ? = new InternalItemsQuery ( ) ;
query . User = user ;
2018-12-27 18:27:57 -05:00
2020-06-14 21:11:11 +12:00
// the true root should return our users root folder children
2019-10-20 21:12:03 +02:00
if ( IsPhysicalRoot )
{
2025-09-16 21:08:04 +02:00
return LibraryManager . GetUserRootFolder ( ) . GetChildren ( user , includeLinkedChildren , out totalItemCount ) ;
2019-10-20 21:12:03 +02:00
}
2018-12-27 18:27:57 -05:00
var result = new Dictionary < Guid , BaseItem > ( ) ;
2025-09-16 21:08:04 +02:00
totalItemCount = AddChildren ( user , includeLinkedChildren , result , false , query ) ;
2018-12-27 18:27:57 -05:00
2024-11-17 11:03:43 +00:00
return result . Values . ToArray ( ) ;
2018-12-27 18:27:57 -05:00
}
2025-09-16 21:08:04 +02:00
public virtual IReadOnlyList < BaseItem > GetChildren ( User user , bool includeLinkedChildren , InternalItemsQuery query = null )
{
return GetChildren ( user , includeLinkedChildren , out _ , query ) ;
}
2020-05-20 13:07:53 -04:00
protected virtual IEnumerable < BaseItem > GetEligibleChildrenForRecursiveChildren ( User user )
2018-12-27 18:27:57 -05:00
{
return Children ;
}
/// <summary>
/// Adds the children to list.
/// </summary>
2025-09-16 21:08:04 +02:00
private int AddChildren ( User user , bool includeLinkedChildren , Dictionary < Guid , BaseItem > result , bool recursive , InternalItemsQuery query , HashSet < Folder > visitedFolders = null )
2018-12-27 18:27:57 -05:00
{
2023-01-08 01:48:14 +07:00
// Prevent infinite recursion of nested folders
visitedFolders ? ? = new HashSet < Folder > ( ) ;
if ( ! visitedFolders . Add ( this ) )
{
2025-09-16 21:08:04 +02:00
return 0 ;
2023-01-08 01:48:14 +07:00
}
2021-05-17 23:34:50 +01:00
// If Query.AlbumFolders is set, then enforce the format as per the db in that it permits sub-folders in music albums.
IEnumerable < BaseItem > children = null ;
if ( ( query ? . DisplayAlbumFolders ? ? false ) & & ( this is MusicAlbum ) )
{
children = Children ;
query = null ;
}
// If there are not sub-folders, proceed as normal.
2022-12-05 15:00:20 +01:00
if ( children is null )
2021-05-17 23:34:50 +01:00
{
children = GetEligibleChildrenForRecursiveChildren ( user ) ;
}
2023-01-08 01:48:14 +07:00
if ( includeLinkedChildren )
2018-12-27 18:27:57 -05:00
{
2025-09-16 21:08:04 +02:00
children = children . Concat ( GetLinkedChildren ( user ) ) . ToArray ( ) ;
2023-01-08 01:48:14 +07:00
}
2025-09-16 21:08:04 +02:00
return AddChildrenFromCollection ( children , user , includeLinkedChildren , result , recursive , query , visitedFolders ) ;
2023-01-08 01:48:14 +07:00
}
2018-12-27 18:27:57 -05:00
2025-09-16 21:08:04 +02:00
private int AddChildrenFromCollection ( IEnumerable < BaseItem > children , User user , bool includeLinkedChildren , Dictionary < Guid , BaseItem > result , bool recursive , InternalItemsQuery query , HashSet < Folder > visitedFolders )
2023-01-08 01:48:14 +07:00
{
2025-09-16 21:08:04 +02:00
query ? ? = new InternalItemsQuery ( ) ;
var limit = query . Limit ;
query . Limit = 100 ; // this is a bit of a dirty hack thats in favor of specifically the webUI as it does not show more then +99 elements in its badges so there is no point in reading more then that.
var visibileChildren = children
. Where ( e = > e . IsVisible ( user ) )
. ToArray ( ) ;
2018-12-27 18:27:57 -05:00
2025-09-16 21:08:04 +02:00
var realChildren = visibileChildren
. Where ( e = > query is null | | UserViewBuilder . FilterItem ( e , query ) )
. ToArray ( ) ;
var childCount = realChildren . Count ( ) ;
if ( result . Count < query . Limit )
{
foreach ( var child in realChildren
. Skip ( query . StartIndex ? ? 0 )
. TakeWhile ( e = > query . Limit > = result . Count ) )
2018-12-27 18:27:57 -05:00
{
2023-01-08 01:48:14 +07:00
result [ child . Id ] = child ;
2018-12-27 18:27:57 -05:00
}
2025-09-16 21:08:04 +02:00
}
2018-12-27 18:27:57 -05:00
2025-09-16 21:08:04 +02:00
if ( recursive )
{
foreach ( var child in visibileChildren
. Where ( e = > e . IsFolder )
. OfType < Folder > ( ) )
2018-12-27 18:27:57 -05:00
{
2025-09-16 21:08:04 +02:00
childCount + = child . AddChildren ( user , includeLinkedChildren , result , true , query , visitedFolders ) ;
2018-12-27 18:27:57 -05:00
}
}
2025-09-16 21:08:04 +02:00
return childCount ;
2018-12-27 18:27:57 -05:00
}
2025-09-16 21:08:04 +02:00
public virtual IReadOnlyList < BaseItem > GetRecursiveChildren ( User user , InternalItemsQuery query , out int totalCount )
2018-12-27 18:27:57 -05:00
{
2022-10-06 20:21:23 +02:00
ArgumentNullException . ThrowIfNull ( user ) ;
2018-12-27 18:27:57 -05:00
var result = new Dictionary < Guid , BaseItem > ( ) ;
2025-09-16 21:08:04 +02:00
totalCount = AddChildren ( user , true , result , true , query ) ;
2018-12-27 18:27:57 -05:00
2024-11-17 11:03:43 +00:00
return result . Values . ToArray ( ) ;
2018-12-27 18:27:57 -05:00
}
/// <summary>
/// Gets the recursive children.
/// </summary>
/// <returns>IList{BaseItem}.</returns>
2024-10-09 10:36:08 +00:00
public IReadOnlyList < BaseItem > GetRecursiveChildren ( )
2018-12-27 18:27:57 -05:00
{
return GetRecursiveChildren ( true ) ;
}
2024-10-09 10:36:08 +00:00
public IReadOnlyList < BaseItem > GetRecursiveChildren ( bool includeLinkedChildren )
2018-12-27 18:27:57 -05:00
{
return GetRecursiveChildren ( i = > true , includeLinkedChildren ) ;
}
2024-10-09 10:36:08 +00:00
public IReadOnlyList < BaseItem > GetRecursiveChildren ( Func < BaseItem , bool > filter )
2018-12-27 18:27:57 -05:00
{
return GetRecursiveChildren ( filter , true ) ;
}
2024-10-09 10:36:08 +00:00
public IReadOnlyList < BaseItem > GetRecursiveChildren ( Func < BaseItem , bool > filter , bool includeLinkedChildren )
2018-12-27 18:27:57 -05:00
{
var result = new Dictionary < Guid , BaseItem > ( ) ;
AddChildrenToList ( result , includeLinkedChildren , true , filter ) ;
2024-11-17 11:03:43 +00:00
return result . Values . ToArray ( ) ;
2018-12-27 18:27:57 -05:00
}
/// <summary>
/// Adds the children to list.
/// </summary>
private void AddChildrenToList ( Dictionary < Guid , BaseItem > result , bool includeLinkedChildren , bool recursive , Func < BaseItem , bool > filter )
{
foreach ( var child in Children )
{
2022-12-05 15:00:20 +01:00
if ( filter is null | | filter ( child ) )
2018-12-27 18:27:57 -05:00
{
result [ child . Id ] = child ;
}
if ( recursive & & child . IsFolder )
{
var folder = ( Folder ) child ;
// We can only support includeLinkedChildren for the first folder, or we might end up stuck in a loop of linked items
folder . AddChildrenToList ( result , false , true , filter ) ;
}
}
if ( includeLinkedChildren )
{
foreach ( var child in GetLinkedChildren ( ) )
{
2022-12-05 15:00:20 +01:00
if ( filter is null | | filter ( child ) )
2018-12-27 18:27:57 -05:00
{
result [ child . Id ] = child ;
}
}
}
}
/// <summary>
/// Gets the linked children.
/// </summary>
/// <returns>IEnumerable{BaseItem}.</returns>
public List < BaseItem > GetLinkedChildren ( )
{
var linkedChildren = LinkedChildren ;
var list = new List < BaseItem > ( linkedChildren . Length ) ;
foreach ( var i in linkedChildren )
{
var child = GetLinkedChild ( i ) ;
2022-12-05 15:01:13 +01:00
if ( child is not null )
2018-12-27 18:27:57 -05:00
{
list . Add ( child ) ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
return list ;
}
public bool ContainsLinkedChildByItemId ( Guid itemId )
{
var linkedChildren = LinkedChildren ;
foreach ( var i in linkedChildren )
{
2021-04-13 15:37:11 +02:00
if ( i . ItemId . HasValue )
2018-12-27 18:27:57 -05:00
{
2022-02-21 14:15:09 +01:00
if ( i . ItemId . Value . Equals ( itemId ) )
2021-04-13 20:12:50 +02:00
{
return true ;
}
continue ;
2018-12-27 18:27:57 -05:00
}
var child = GetLinkedChild ( i ) ;
2022-12-05 15:01:13 +01:00
if ( child is not null & & child . Id . Equals ( itemId ) )
2018-12-27 18:27:57 -05:00
{
return true ;
}
}
2020-06-16 09:43:52 +12:00
2018-12-27 18:27:57 -05:00
return false ;
}
2020-05-20 13:07:53 -04:00
public List < BaseItem > GetLinkedChildren ( User user )
2018-12-27 18:27:57 -05:00
{
2022-12-05 15:00:20 +01:00
if ( ! FilterLinkedChildrenPerUser | | user is null )
2018-12-27 18:27:57 -05:00
{
return GetLinkedChildren ( ) ;
}
var linkedChildren = LinkedChildren ;
var list = new List < BaseItem > ( linkedChildren . Length ) ;
if ( linkedChildren . Length = = 0 )
{
return list ;
}
var allUserRootChildren = LibraryManager . GetUserRootFolder ( )
. GetChildren ( user , true )
. OfType < Folder > ( )
. ToList ( ) ;
var collectionFolderIds = allUserRootChildren
. Select ( i = > i . Id )
. ToList ( ) ;
foreach ( var i in linkedChildren )
{
var child = GetLinkedChild ( i ) ;
2022-12-05 15:00:20 +01:00
if ( child is null )
2018-12-27 18:27:57 -05:00
{
continue ;
}
var childOwner = child . GetOwner ( ) ? ? child ;
2021-08-28 16:32:50 -06:00
if ( child is not IItemByName )
2018-12-27 18:27:57 -05:00
{
var childProtocol = childOwner . PathProtocol ;
if ( ! childProtocol . HasValue | | childProtocol . Value ! = Model . MediaInfo . MediaProtocol . File )
{
if ( ! childOwner . IsVisibleStandalone ( user ) )
{
continue ;
}
}
else
{
var itemCollectionFolderIds =
LibraryManager . GetCollectionFolders ( childOwner , allUserRootChildren ) . Select ( f = > f . Id ) ;
if ( ! itemCollectionFolderIds . Any ( collectionFolderIds . Contains ) )
{
continue ;
}
}
}
list . Add ( child ) ;
}
return list ;
}
/// <summary>
/// Gets the linked children.
/// </summary>
/// <returns>IEnumerable{BaseItem}.</returns>
2024-10-09 10:36:08 +00:00
public IReadOnlyList < Tuple < LinkedChild , BaseItem > > GetLinkedChildrenInfos ( )
2018-12-27 18:27:57 -05:00
{
return LinkedChildren
. Select ( i = > new Tuple < LinkedChild , BaseItem > ( i , GetLinkedChild ( i ) ) )
2024-10-09 10:36:08 +00:00
. Where ( i = > i . Item2 is not null )
2024-11-17 11:03:43 +00:00
. ToArray ( ) ;
2018-12-27 18:27:57 -05:00
}
2022-01-28 12:21:40 +01:00
protected override async Task < bool > RefreshedOwnedItems ( MetadataRefreshOptions options , IReadOnlyList < FileSystemMetadata > fileSystemChildren , CancellationToken cancellationToken )
2018-12-27 18:27:57 -05:00
{
var changesFound = false ;
if ( IsFileProtocol )
{
if ( RefreshLinkedChildren ( fileSystemChildren ) )
{
changesFound = true ;
}
}
var baseHasChanges = await base . RefreshedOwnedItems ( options , fileSystemChildren , cancellationToken ) . ConfigureAwait ( false ) ;
return baseHasChanges | | changesFound ;
}
/// <summary>
/// Refreshes the linked children.
/// </summary>
2021-06-06 09:16:41 -06:00
/// <param name="fileSystemChildren">The enumerable of file system metadata.</param>
/// <returns><c>true</c> if the linked children were updated, <c>false</c> otherwise.</returns>
2018-12-27 18:27:57 -05:00
protected virtual bool RefreshLinkedChildren ( IEnumerable < FileSystemMetadata > fileSystemChildren )
{
if ( SupportsShortcutChildren )
{
var newShortcutLinks = fileSystemChildren
. Where ( i = > ! i . IsDirectory & & FileSystem . IsShortcut ( i . FullName ) )
. Select ( i = >
{
try
{
2018-12-13 14:18:25 +01:00
Logger . LogDebug ( "Found shortcut at {0}" , i . FullName ) ;
2018-12-27 18:27:57 -05:00
var resolvedPath = CollectionFolder . ApplicationHost . ExpandVirtualPath ( FileSystem . ResolveShortcut ( i . FullName ) ) ;
if ( ! string . IsNullOrEmpty ( resolvedPath ) )
{
return new LinkedChild
{
Path = resolvedPath ,
Type = LinkedChildType . Shortcut
} ;
}
2018-12-13 14:18:25 +01:00
Logger . LogError ( "Error resolving shortcut {0}" , i . FullName ) ;
2018-12-27 18:27:57 -05:00
return null ;
}
catch ( IOException ex )
{
2018-12-20 11:38:42 +01:00
Logger . LogError ( ex , "Error resolving shortcut {0}" , i . FullName ) ;
2018-12-27 18:27:57 -05:00
return null ;
}
} )
2022-12-05 15:01:13 +01:00
. Where ( i = > i is not null )
2018-12-27 18:27:57 -05:00
. ToList ( ) ;
var currentShortcutLinks = LinkedChildren . Where ( i = > i . Type = = LinkedChildType . Shortcut ) . ToList ( ) ;
if ( ! newShortcutLinks . SequenceEqual ( currentShortcutLinks , new LinkedChildComparer ( FileSystem ) ) )
{
2018-12-13 14:18:25 +01:00
Logger . LogInformation ( "Shortcut links have changed for {0}" , Path ) ;
2018-12-27 18:27:57 -05:00
newShortcutLinks . AddRange ( LinkedChildren . Where ( i = > i . Type = = LinkedChildType . Manual ) ) ;
2018-12-28 16:48:26 +01:00
LinkedChildren = newShortcutLinks . ToArray ( ) ;
2018-12-27 18:27:57 -05:00
return true ;
}
}
foreach ( var child in LinkedChildren )
{
// Reset the cached value
child . ItemId = null ;
}
return false ;
}
/// <summary>
/// Marks the played.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="datePlayed">The date played.</param>
/// <param name="resetPosition">if set to <c>true</c> [reset position].</param>
2020-10-12 20:05:11 +02:00
public override void MarkPlayed (
User user ,
2018-12-27 18:27:57 -05:00
DateTime ? datePlayed ,
bool resetPosition )
{
var query = new InternalItemsQuery
{
User = user ,
Recursive = true ,
IsFolder = false ,
EnableTotalRecordCount = false
} ;
2020-05-12 22:10:35 -04:00
if ( ! user . DisplayMissingEpisodes )
2018-12-27 18:27:57 -05:00
{
query . IsVirtualItem = false ;
}
var itemsResult = GetItemList ( query ) ;
// Sweep through recursively and update status
foreach ( var item in itemsResult )
{
if ( item . IsVirtualItem )
{
// The querying doesn't support virtual unaired
var episode = item as Episode ;
2022-12-05 15:01:13 +01:00
if ( episode is not null & & episode . IsUnaired )
2018-12-27 18:27:57 -05:00
{
continue ;
}
}
item . MarkPlayed ( user , datePlayed , resetPosition ) ;
}
}
/// <summary>
/// Marks the unplayed.
/// </summary>
/// <param name="user">The user.</param>
2020-05-20 13:07:53 -04:00
public override void MarkUnplayed ( User user )
2018-12-27 18:27:57 -05:00
{
var itemsResult = GetItemList ( new InternalItemsQuery
{
User = user ,
Recursive = true ,
IsFolder = false ,
EnableTotalRecordCount = false
} ) ;
// Sweep through recursively and update status
foreach ( var item in itemsResult )
{
item . MarkUnplayed ( user ) ;
}
}
2025-09-14 11:18:21 -06:00
public override bool IsPlayed ( User user , UserItemData userItemData )
2018-12-27 18:27:57 -05:00
{
2025-09-16 21:08:04 +02:00
return ItemRepository . GetIsPlayed ( user , Id , true ) ;
2018-12-27 18:27:57 -05:00
}
2025-09-14 11:18:21 -06:00
public override bool IsUnplayed ( User user , UserItemData userItemData )
2018-12-27 18:27:57 -05:00
{
2025-09-14 11:18:21 -06:00
return ! IsPlayed ( user , userItemData ) ;
2018-12-27 18:27:57 -05:00
}
2020-05-20 13:07:53 -04:00
public override void FillUserDataDtoValues ( UserItemDataDto dto , UserItemData userData , BaseItemDto itemDto , User user , DtoOptions fields )
2018-12-27 18:27:57 -05:00
{
if ( ! SupportsUserDataFromChildren )
{
return ;
}
2024-08-30 15:08:56 +02:00
if ( itemDto is not null & & fields . ContainsField ( ItemFields . RecursiveItemCount ) )
2018-12-27 18:27:57 -05:00
{
2024-08-30 15:08:56 +02:00
itemDto . RecursiveItemCount = GetRecursiveChildCount ( user ) ;
2018-12-27 18:27:57 -05:00
}
if ( SupportsPlayedStatus )
{
var unplayedQueryResult = GetItems ( new InternalItemsQuery ( user )
{
Recursive = true ,
IsFolder = false ,
IsVirtualItem = false ,
EnableTotalRecordCount = true ,
Limit = 0 ,
IsPlayed = false ,
DtoOptions = new DtoOptions ( false )
{
EnableImages = false
}
2021-04-30 15:09:36 +02:00
} ) . TotalRecordCount ;
2018-12-27 18:27:57 -05:00
2021-04-30 15:09:36 +02:00
dto . UnplayedItemCount = unplayedQueryResult ;
2018-12-27 18:27:57 -05:00
2021-04-30 15:09:36 +02:00
if ( itemDto ? . RecursiveItemCount > 0 )
2018-12-27 18:27:57 -05:00
{
2021-04-30 15:09:36 +02:00
var unplayedPercentage = ( ( double ) unplayedQueryResult / itemDto . RecursiveItemCount . Value ) * 100 ;
dto . PlayedPercentage = 100 - unplayedPercentage ;
dto . Played = dto . PlayedPercentage . Value > = 100 ;
2018-12-27 18:27:57 -05:00
}
else
{
dto . Played = ( dto . UnplayedItemCount ? ? 0 ) = = 0 ;
}
}
}
2020-10-01 16:24:35 -07:00
/// <summary>
/// Contains constants used when reporting scan progress.
/// </summary>
private static class ProgressHelpers
{
/// <summary>
/// Reported after the folders immediate children are retrieved.
/// </summary>
public const int RetrievedChildren = 5 ;
/// <summary>
/// Reported after add, updating, or deleting child items from the LibraryManager.
/// </summary>
public const int UpdatedChildItems = 10 ;
/// <summary>
/// Reported once subfolders are scanned.
/// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders].
/// </summary>
public const int ScannedSubfolders = 50 ;
/// <summary>
/// Reported once metadata is refreshed.
/// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed].
/// </summary>
public const int RefreshedMetadata = 100 ;
/// <summary>
/// Gets the current progress given the previous step, next step, and progress in between.
/// </summary>
/// <param name="previousProgressStep">The previous progress step.</param>
/// <param name="nextProgressStep">The next progress step.</param>
/// <param name="currentProgress">The current progress step.</param>
/// <returns>The progress.</returns>
public static double GetProgress ( int previousProgressStep , int nextProgressStep , double currentProgress )
{
return previousProgressStep + ( ( nextProgressStep - previousProgressStep ) * ( currentProgress / 100 ) ) ;
}
}
2018-12-27 18:27:57 -05:00
}
}