using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Logging; using MediaBrowser.Common.Win32; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Tasks; using SortOrder = MediaBrowser.Controller.Sorting.SortOrder; namespace MediaBrowser.Controller.Entities { /// /// Class Folder /// public class Folder : BaseItem { /// /// Gets a value indicating whether this instance is folder. /// /// true if this instance is folder; otherwise, false. [IgnoreDataMember] public override bool IsFolder { get { return true; } } /// /// Gets or sets a value indicating whether this instance is physical root. /// /// true if this instance is physical root; otherwise, false. public bool IsPhysicalRoot { get; set; } /// /// Gets or sets a value indicating whether this instance is root. /// /// true if this instance is root; otherwise, false. public bool IsRoot { get; set; } /// /// Gets a value indicating whether this instance is virtual folder. /// /// true if this instance is virtual folder; otherwise, false. [IgnoreDataMember] public virtual bool IsVirtualFolder { get { return false; } } /// /// Return the id that should be used to key display prefs for this item. /// Default is based on the type for everything except actual generic folders. /// /// The display prefs id. [IgnoreDataMember] public virtual Guid DisplayPrefsId { get { var thisType = GetType(); return thisType == typeof(Folder) ? Id : thisType.FullName.GetMD5(); } } /// /// The _display prefs /// private IEnumerable _displayPrefs; /// /// The _display prefs initialized /// private bool _displayPrefsInitialized; /// /// The _display prefs sync lock /// private object _displayPrefsSyncLock = new object(); /// /// Gets the display prefs. /// /// The display prefs. [IgnoreDataMember] public IEnumerable DisplayPrefs { get { // Call ToList to exhaust the stream because we'll be iterating over this multiple times LazyInitializer.EnsureInitialized(ref _displayPrefs, ref _displayPrefsInitialized, ref _displayPrefsSyncLock, () => Kernel.Instance.DisplayPreferencesRepository.RetrieveDisplayPrefs(this).ToList()); return _displayPrefs; } private set { _displayPrefs = value; if (value == null) { _displayPrefsInitialized = false; } } } /// /// Gets the display prefs. /// /// The user. /// if set to true [create if null]. /// DisplayPreferences. /// public DisplayPreferences GetDisplayPrefs(User user, bool createIfNull) { if (user == null) { throw new ArgumentNullException(); } if (DisplayPrefs == null) { if (!createIfNull) { return null; } AddOrUpdateDisplayPrefs(user, new DisplayPreferences { UserId = user.Id }); } var data = DisplayPrefs.FirstOrDefault(u => u.UserId == user.Id); if (data == null && createIfNull) { data = new DisplayPreferences { UserId = user.Id }; AddOrUpdateDisplayPrefs(user, data); } return data; } /// /// Adds the or update display prefs. /// /// The user. /// The data. /// public void AddOrUpdateDisplayPrefs(User user, DisplayPreferences data) { if (user == null) { throw new ArgumentNullException(); } if (data == null) { throw new ArgumentNullException(); } data.UserId = user.Id; if (DisplayPrefs == null) { DisplayPrefs = new[] { data }; } else { var list = DisplayPrefs.Where(u => u.UserId != user.Id).ToList(); list.Add(data); DisplayPrefs = list; } } #region Sorting /// /// The _sort by options /// private Dictionary> _sortByOptions; /// /// Dictionary of sort options - consists of a display value (localized) and an IComparer of BaseItem /// /// The sort by options. [IgnoreDataMember] public Dictionary> SortByOptions { get { return _sortByOptions ?? (_sortByOptions = GetSortByOptions()); } } /// /// Returns the valid set of sort by options for this folder type. /// Override or extend to modify. /// /// Dictionary{System.StringIComparer{BaseItem}}. protected virtual Dictionary> GetSortByOptions() { return new Dictionary> { {LocalizedStrings.Instance.GetString("NameDispPref"), new BaseItemComparer(SortOrder.Name)}, {LocalizedStrings.Instance.GetString("DateDispPref"), new BaseItemComparer(SortOrder.Date)}, {LocalizedStrings.Instance.GetString("RatingDispPref"), new BaseItemComparer(SortOrder.Rating)}, {LocalizedStrings.Instance.GetString("RuntimeDispPref"), new BaseItemComparer(SortOrder.Runtime)}, {LocalizedStrings.Instance.GetString("YearDispPref"), new BaseItemComparer(SortOrder.Year)} }; } /// /// Get a sorting comparer by name /// /// The name. /// IComparer{BaseItem}. private IComparer GetSortingFunction(string name) { IComparer sorting; SortByOptions.TryGetValue(name ?? "", out sorting); return sorting ?? new BaseItemComparer(SortOrder.Name); } /// /// Get the list of sort by choices for this folder (localized). /// /// The sort by option strings. [IgnoreDataMember] public IEnumerable SortByOptionStrings { get { return SortByOptions.Keys; } } #endregion #region Indexing /// /// The _index by options /// private Dictionary>> _indexByOptions; /// /// Dictionary of index options - consists of a display value and an indexing function /// which takes User as a parameter and returns an IEnum of BaseItem /// /// The index by options. [IgnoreDataMember] public Dictionary>> IndexByOptions { get { return _indexByOptions ?? (_indexByOptions = GetIndexByOptions()); } } /// /// Returns the valid set of index by options for this folder type. /// Override or extend to modify. /// /// Dictionary{System.StringFunc{UserIEnumerable{BaseItem}}}. protected virtual Dictionary>> GetIndexByOptions() { return new Dictionary>> { {LocalizedStrings.Instance.GetString("NoneDispPref"), null}, {LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer}, {LocalizedStrings.Instance.GetString("GenreDispPref"), GetIndexByGenre}, {LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector}, {LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear}, {LocalizedStrings.Instance.GetString("OfficialRatingDispPref"), null}, {LocalizedStrings.Instance.GetString("StudioDispPref"), GetIndexByStudio} }; } /// /// Gets the index by actor. /// /// The user. /// IEnumerable{BaseItem}. protected IEnumerable GetIndexByPerformer(User user) { return GetIndexByPerson(user, new List { PersonType.Actor, PersonType.MusicArtist }, LocalizedStrings.Instance.GetString("PerformerDispPref")); } /// /// Gets the index by director. /// /// The user. /// IEnumerable{BaseItem}. protected IEnumerable GetIndexByDirector(User user) { return GetIndexByPerson(user, new List { PersonType.Director }, LocalizedStrings.Instance.GetString("DirectorDispPref")); } /// /// Gets the index by person. /// /// The user. /// The person types we should match on /// Name of the index. /// IEnumerable{BaseItem}. protected IEnumerable GetIndexByPerson(User user, List personTypes, string indexName) { // Even though this implementation means multiple iterations over the target list - it allows us to defer // the retrieval of the individual children for each index value until they are requested. using (new Profiler(indexName + " Index Build for " + Name)) { // Put this in a local variable to avoid an implicitly captured closure var currentIndexName = indexName; var us = this; var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.AllPeople != null).ToList(); return candidates.AsParallel().SelectMany(i => i.AllPeople.Where(p => personTypes.Contains(p.Type)) .Select(a => a.Name)) .Distinct() .Select(i => { try { return Kernel.Instance.LibraryManager.GetPerson(i).Result; } catch (IOException ex) { Logger.LogException("Error getting person {0}", ex, i); return null; } catch (AggregateException ex) { Logger.LogException("Error getting person {0}", ex, i); return null; } }) .Where(i => i != null) .Select(a => new IndexFolder(us, a, candidates.Where(i => i.AllPeople.Any(p => personTypes.Contains(p.Type) && p.Name.Equals(a.Name, StringComparison.OrdinalIgnoreCase)) ), currentIndexName)); } } /// /// Gets the index by studio. /// /// The user. /// IEnumerable{BaseItem}. protected IEnumerable GetIndexByStudio(User user) { // Even though this implementation means multiple iterations over the target list - it allows us to defer // the retrieval of the individual children for each index value until they are requested. using (new Profiler("Studio Index Build for " + Name)) { var indexName = LocalizedStrings.Instance.GetString("StudioDispPref"); var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.Studios != null).ToList(); return candidates.AsParallel().SelectMany(i => i.Studios) .Distinct() .Select(i => { try { return Kernel.Instance.LibraryManager.GetStudio(i).Result; } catch (IOException ex) { Logger.LogException("Error getting studio {0}", ex, i); return null; } catch (AggregateException ex) { Logger.LogException("Error getting studio {0}", ex, i); return null; } }) .Where(i => i != null) .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.Studios.Any(s => s.Equals(ndx.Name, StringComparison.OrdinalIgnoreCase))), indexName)); } } /// /// Gets the index by genre. /// /// The user. /// IEnumerable{BaseItem}. protected IEnumerable GetIndexByGenre(User user) { // Even though this implementation means multiple iterations over the target list - it allows us to defer // the retrieval of the individual children for each index value until they are requested. using (new Profiler("Genre Index Build for " + Name)) { var indexName = LocalizedStrings.Instance.GetString("GenreDispPref"); //we need a copy of this so we don't double-recurse var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.Genres != null).ToList(); return candidates.AsParallel().SelectMany(i => i.Genres) .Distinct() .Select(i => { try { return Kernel.Instance.LibraryManager.GetGenre(i).Result; } catch (IOException ex) { Logger.LogException("Error getting genre {0}", ex, i); return null; } catch (AggregateException ex) { Logger.LogException("Error getting genre {0}", ex, i); return null; } }) .Where(i => i != null) .Select(genre => new IndexFolder(this, genre, candidates.Where(i => i.Genres.Any(g => g.Equals(genre.Name, StringComparison.OrdinalIgnoreCase))), indexName) ); } } /// /// Gets the index by year. /// /// The user. /// IEnumerable{BaseItem}. protected IEnumerable GetIndexByYear(User user) { // Even though this implementation means multiple iterations over the target list - it allows us to defer // the retrieval of the individual children for each index value until they are requested. using (new Profiler("Production Year Index Build for " + Name)) { var indexName = LocalizedStrings.Instance.GetString("YearDispPref"); //we need a copy of this so we don't double-recurse var candidates = RecursiveChildren.Where(i => i.IncludeInIndex && i.ProductionYear.HasValue).ToList(); return candidates.AsParallel().Select(i => i.ProductionYear.Value) .Distinct() .Select(i => { try { return Kernel.Instance.LibraryManager.GetYear(i).Result; } catch (IOException ex) { Logger.LogException("Error getting year {0}", ex, i); return null; } catch (AggregateException ex) { Logger.LogException("Error getting year {0}", ex, i); return null; } }) .Where(i => i != null) .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.ProductionYear == int.Parse(ndx.Name)), indexName)); } } /// /// Returns the indexed children for this user from the cache. Caches them if not already there. /// /// The user. /// The index by. /// IEnumerable{BaseItem}. private IEnumerable GetIndexedChildren(User user, string indexBy) { List result; var cacheKey = user.Name + indexBy; IndexCache.TryGetValue(cacheKey, out result); if (result == null) { //not cached - cache it Func> indexing; IndexByOptions.TryGetValue(indexBy, out indexing); result = BuildIndex(indexBy, indexing, user); } return result; } /// /// Get the list of indexy by choices for this folder (localized). /// /// The index by option strings. [IgnoreDataMember] public IEnumerable IndexByOptionStrings { get { return IndexByOptions.Keys; } } /// /// The index cache /// protected ConcurrentDictionary> IndexCache = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); /// /// Builds the index. /// /// The index key. /// The index function. /// The user. /// List{BaseItem}. protected virtual List BuildIndex(string indexKey, Func> indexFunction, User user) { return indexFunction != null ? IndexCache[user.Name + indexKey] = indexFunction(user).ToList() : null; } #endregion /// /// The children /// private ConcurrentBag _children; /// /// The _children initialized /// private bool _childrenInitialized; /// /// The _children sync lock /// private object _childrenSyncLock = new object(); /// /// Gets or sets the actual children. /// /// The actual children. protected virtual ConcurrentBag ActualChildren { get { LazyInitializer.EnsureInitialized(ref _children, ref _childrenInitialized, ref _childrenSyncLock, LoadChildren); return _children; } private set { _children = value; if (value == null) { _childrenInitialized = false; } } } /// /// thread-safe access to the actual children of this folder - without regard to user /// /// The children. [IgnoreDataMember] public ConcurrentBag Children { get { return ActualChildren; } } /// /// thread-safe access to all recursive children of this folder - without regard to user /// /// The recursive children. [IgnoreDataMember] public IEnumerable RecursiveChildren { get { foreach (var item in Children) { yield return item; if (item.IsFolder) { var subFolder = (Folder)item; foreach (var subitem in subFolder.RecursiveChildren) { yield return subitem; } } } } } /// /// Loads our children. Validation will occur externally. /// We want this sychronous. /// /// ConcurrentBag{BaseItem}. protected virtual ConcurrentBag LoadChildren() { //just load our children from the repo - the library will be validated and maintained in other processes return new ConcurrentBag(GetCachedChildren()); } /// /// Gets or sets the current validation cancellation token source. /// /// The current validation cancellation token source. private CancellationTokenSource CurrentValidationCancellationTokenSource { get; set; } /// /// Validates that the children of the folder still exist /// /// The progress. /// The cancellation token. /// if set to true [recursive]. /// Task. public async Task ValidateChildren(IProgress progress, CancellationToken cancellationToken, bool? recursive = null) { cancellationToken.ThrowIfCancellationRequested(); // Cancel the current validation, if any if (CurrentValidationCancellationTokenSource != null) { CurrentValidationCancellationTokenSource.Cancel(); } // Create an inner cancellation token. This can cancel all validations from this level on down, // but nothing above this var innerCancellationTokenSource = new CancellationTokenSource(); try { CurrentValidationCancellationTokenSource = innerCancellationTokenSource; var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken); await ValidateChildrenInternal(progress, linkedCancellationTokenSource.Token, recursive).ConfigureAwait(false); } catch (OperationCanceledException ex) { Logger.LogInfo("ValidateChildren cancelled for " + Name); // If the outer cancelletion token in the cause for the cancellation, throw it if (cancellationToken.IsCancellationRequested && ex.CancellationToken == cancellationToken) { throw; } } finally { // Null out the token source if (CurrentValidationCancellationTokenSource == innerCancellationTokenSource) { CurrentValidationCancellationTokenSource = null; } innerCancellationTokenSource.Dispose(); } } /// /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes /// ***Currently does not contain logic to maintain items that are unavailable in the file system*** /// /// The progress. /// The cancellation token. /// if set to true [recursive]. /// Task. protected async virtual Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool? recursive = null) { // Nothing to do here if (LocationType != LocationType.FileSystem) { return; } cancellationToken.ThrowIfCancellationRequested(); var changedArgs = new ChildrenChangedEventArgs(this); //get the current valid children from filesystem (or wherever) var nonCachedChildren = GetNonCachedChildren(); if (nonCachedChildren == null) return; //nothing to validate progress.Report(new TaskProgress { PercentComplete = 5 }); //build a dictionary of the current children we have now by Id so we can compare quickly and easily var currentChildren = ActualChildren.ToDictionary(i => i.Id); //create a list for our validated children var validChildren = new ConcurrentBag>(); cancellationToken.ThrowIfCancellationRequested(); Parallel.ForEach(nonCachedChildren, child => { BaseItem currentChild; if (currentChildren.TryGetValue(child.Id, out currentChild)) { currentChild.ResolveArgs = child.ResolveArgs; //existing item - check if it has changed if (currentChild.HasChanged(child)) { EntityResolutionHelper.EnsureDates(currentChild, child.ResolveArgs); changedArgs.AddUpdatedItem(currentChild); validChildren.Add(new Tuple(currentChild, true)); } else { validChildren.Add(new Tuple(currentChild, false)); } } else { //brand new item - needs to be added changedArgs.AddNewItem(child); validChildren.Add(new Tuple(child, true)); } }); // If any items were added or removed.... if (!changedArgs.ItemsAdded.IsEmpty || currentChildren.Count != validChildren.Count) { var newChildren = validChildren.Select(c => c.Item1).ToList(); //that's all the new and changed ones - now see if there are any that are missing changedArgs.ItemsRemoved = currentChildren.Values.Except(newChildren).ToList(); foreach (var item in changedArgs.ItemsRemoved) { Logger.LogInfo("** " + item.Name + " Removed from library."); } var childrenReplaced = false; if (changedArgs.ItemsRemoved.Count > 0) { ActualChildren = new ConcurrentBag(newChildren); childrenReplaced = true; } var saveTasks = new List(); foreach (var item in changedArgs.ItemsAdded) { Logger.LogInfo("** " + item.Name + " Added to library."); if (!childrenReplaced) { _children.Add(item); } saveTasks.Add(Kernel.Instance.ItemRepository.SaveItem(item, CancellationToken.None)); } await Task.WhenAll(saveTasks).ConfigureAwait(false); //and save children in repo... Logger.LogInfo("*** Saving " + newChildren.Count + " children for " + Name); await Kernel.Instance.ItemRepository.SaveChildren(Id, newChildren, CancellationToken.None).ConfigureAwait(false); } if (changedArgs.HasChange) { //force the indexes to rebuild next time IndexCache.Clear(); //and fire event Kernel.Instance.LibraryManager.OnLibraryChanged(changedArgs); } progress.Report(new TaskProgress { PercentComplete = 15 }); cancellationToken.ThrowIfCancellationRequested(); await RefreshChildren(validChildren, progress, cancellationToken, recursive).ConfigureAwait(false); progress.Report(new TaskProgress { PercentComplete = 100 }); } /// /// Refreshes the children. /// /// The children. /// The progress. /// The cancellation token. /// if set to true [recursive]. /// Task. private Task RefreshChildren(IEnumerable> children, IProgress progress, CancellationToken cancellationToken, bool? recursive) { var numComplete = 0; var list = children.ToList(); var tasks = list.Select(tuple => Task.Run(async () => { cancellationToken.ThrowIfCancellationRequested(); var child = tuple.Item1; //refresh it await child.RefreshMetadata(cancellationToken, resetResolveArgs: child.IsFolder).ConfigureAwait(false); //and add it to our valid children //fire an added event...? //if it is a folder we need to validate its children as well // Refresh children if a folder and the item changed or recursive is set to true var refreshChildren = child.IsFolder && (tuple.Item2 || (recursive.HasValue && recursive.Value)); if (refreshChildren) { // Don't refresh children if explicitly set to false if (recursive.HasValue && recursive.Value == false) { refreshChildren = false; } } if (refreshChildren) { cancellationToken.ThrowIfCancellationRequested(); await ((Folder)child).ValidateChildren(new Progress { }, cancellationToken, recursive: recursive).ConfigureAwait(false); } lock (progress) { numComplete++; double percent = numComplete; percent /= list.Count; progress.Report(new TaskProgress { PercentComplete = (85 * percent) + 15 }); } })); cancellationToken.ThrowIfCancellationRequested(); return Task.WhenAll(tasks); } /// /// Get the children of this folder from the actual file system /// /// IEnumerable{BaseItem}. protected virtual IEnumerable GetNonCachedChildren() { IEnumerable fileSystemChildren; try { fileSystemChildren = ResolveArgs.FileSystemChildren; } catch (IOException ex) { Logger.LogException("Error getting ResolveArgs for {0}", ex, Path); return new List { }; } return Kernel.Instance.LibraryManager.GetItems(fileSystemChildren, this); } /// /// Get our children from the repo - stubbed for now /// /// IEnumerable{BaseItem}. protected virtual IEnumerable GetCachedChildren() { return Kernel.Instance.ItemRepository.RetrieveChildren(this); } /// /// Gets allowed children of an item /// /// The user. /// The index by. /// The sort by. /// The sort order. /// IEnumerable{BaseItem}. /// public virtual IEnumerable GetChildren(User user, string indexBy = null, string sortBy = null, Model.Entities.SortOrder? sortOrder = null) { if (user == null) { throw new ArgumentNullException(); } //the true root should return our users root folder children if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, indexBy, sortBy, sortOrder); IEnumerable result = null; if (!string.IsNullOrEmpty(indexBy)) { result = GetIndexedChildren(user, indexBy); } // If indexed is false or the indexing function is null if (result == null) { result = ActualChildren.Where(c => c.IsVisible(user)); } if (string.IsNullOrEmpty(sortBy)) { return result; } return sortOrder.HasValue && sortOrder.Value == Model.Entities.SortOrder.Descending ? result.OrderByDescending(i => i, GetSortingFunction(sortBy)) : result.OrderBy(i => i, GetSortingFunction(sortBy)); } /// /// Gets allowed recursive children of an item /// /// The user. /// IEnumerable{BaseItem}. /// public IEnumerable GetRecursiveChildren(User user) { if (user == null) { throw new ArgumentNullException(); } foreach (var item in GetChildren(user)) { yield return item; var subFolder = item as Folder; if (subFolder != null) { foreach (var subitem in subFolder.GetRecursiveChildren(user)) { yield return subitem; } } } } /// /// Folders need to validate and refresh /// /// Task. public override async Task ChangedExternally() { await base.ChangedExternally().ConfigureAwait(false); var progress = new Progress { }; await ValidateChildren(progress, CancellationToken.None).ConfigureAwait(false); } /// /// Marks the item as either played or unplayed /// /// The user. /// if set to true [was played]. /// Task. public override async Task SetPlayedStatus(User user, bool wasPlayed) { await base.SetPlayedStatus(user, wasPlayed).ConfigureAwait(false); // Now sweep through recursively and update status var tasks = GetChildren(user).Select(c => c.SetPlayedStatus(user, wasPlayed)); await Task.WhenAll(tasks).ConfigureAwait(false); } /// /// Finds an item by ID, recursively /// /// The id. /// The user. /// BaseItem. public override BaseItem FindItemById(Guid id, User user) { var result = base.FindItemById(id, user); if (result != null) { return result; } var children = user == null ? ActualChildren : GetChildren(user); foreach (var child in children) { result = child.FindItemById(id, user); if (result != null) { return result; } } return null; } /// /// Finds an item by path, recursively /// /// The path. /// BaseItem. /// public BaseItem FindByPath(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException(); } try { if (ResolveArgs.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase)) { return this; } } catch (IOException ex) { Logger.LogException("Error getting ResolveArgs for {0}", ex, Path); } //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy return RecursiveChildren.FirstOrDefault(i => { try { return i.ResolveArgs.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase); } catch (IOException ex) { Logger.LogException("Error getting ResolveArgs for {0}", ex, Path); return false; } }); } } }