using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using System; using System.Collections; 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; namespace MediaBrowser.Controller.Entities { /// /// Class Folder /// public class Folder : BaseItem { public Folder() { LinkedChildren = new List(); } /// /// 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; } } public virtual List LinkedChildren { get; set; } protected virtual bool SupportsShortcutChildren { get { return true; } } /// /// Adds the child. /// /// The item. /// The cancellation token. /// Task. /// Unable to add + item.Name public async Task AddChild(BaseItem item, CancellationToken cancellationToken) { item.Parent = this; if (item.Id == Guid.Empty) { item.Id = item.Path.GetMBId(item.GetType()); } if (item.DateCreated == DateTime.MinValue) { item.DateCreated = DateTime.UtcNow; } if (item.DateModified == DateTime.MinValue) { item.DateModified = DateTime.UtcNow; } AddChildInternal(item); await LibraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); await ItemRepository.SaveChildren(Id, _children.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); } protected void AddChildrenInternal(IEnumerable children) { lock (_childrenSyncLock) { var newChildren = _children.ToList(); newChildren.AddRange(children); _children = newChildren; } } protected void AddChildInternal(BaseItem child) { lock (_childrenSyncLock) { var newChildren = _children.ToList(); newChildren.Add(child); _children = newChildren; } } protected void RemoveChildrenInternal(IEnumerable children) { lock (_childrenSyncLock) { _children = _children.Except(children).ToList(); } } protected void ClearChildrenInternal() { lock (_childrenSyncLock) { _children = new List(); } } /// /// Never want folders to be blocked by "BlockNotRated" /// [IgnoreDataMember] public override string OfficialRatingForComparison { get { if (this is Series) { return base.OfficialRatingForComparison; } return !string.IsNullOrEmpty(base.OfficialRatingForComparison) ? base.OfficialRatingForComparison : "None"; } } /// /// Removes the child. /// /// The item. /// The cancellation token. /// Task. /// Unable to remove + item.Name public Task RemoveChild(BaseItem item, CancellationToken cancellationToken) { RemoveChildrenInternal(new[] { item }); item.Parent = null; LibraryManager.ReportItemRemoved(item); return ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken); } /// /// Clears the children. /// /// The cancellation token. /// Task. public Task ClearChildren(CancellationToken cancellationToken) { var items = ActualChildren.ToList(); ClearChildrenInternal(); foreach (var item in items) { LibraryManager.ReportItemRemoved(item); } return ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken); } #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.GuestStar }, true, 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 }, false, LocalizedStrings.Instance.GetString("DirectorDispPref")); } /// /// Gets the index by person. /// /// The user. /// The person types we should match on /// if set to true [include audio]. /// Name of the index. /// IEnumerable{BaseItem}. private IEnumerable GetIndexByPerson(User user, List personTypes, bool includeAudio, 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, Logger)) { // Put this in a local variable to avoid an implicitly captured closure var currentIndexName = indexName; var us = this; var recursiveChildren = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList(); // Get the candidates, but handle audio separately var candidates = recursiveChildren.Where(i => i.AllPeople != null && !(i is Audio.Audio)).ToList(); var indexFolders = candidates.AsParallel().SelectMany(i => i.AllPeople.Where(p => personTypes.Contains(p.Type)) .Select(a => a.Name)) .Distinct() .Select(i => { try { return LibraryManager.GetPerson(i); } catch (IOException ex) { Logger.ErrorException("Error getting person {0}", ex, i); return null; } catch (AggregateException ex) { Logger.ErrorException("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)).AsEnumerable(); if (includeAudio) { var songs = recursiveChildren.OfType().ToList(); indexFolders = songs.SelectMany(i => i.Artists) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(i => { try { return LibraryManager.GetArtist(i); } catch (IOException ex) { Logger.ErrorException("Error getting artist {0}", ex, i); return null; } catch (AggregateException ex) { Logger.ErrorException("Error getting artist {0}", ex, i); return null; } }) .Where(i => i != null) .Select(a => new IndexFolder(us, a, songs.Where(i => i.Artists.Contains(a.Name, StringComparer.OrdinalIgnoreCase) ), currentIndexName)).Concat(indexFolders); } return indexFolders; } } /// /// 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, Logger)) { var indexName = LocalizedStrings.Instance.GetString("StudioDispPref"); var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList(); return candidates.AsParallel().SelectMany(i => i.AllStudios) .Distinct() .Select(i => { try { return LibraryManager.GetStudio(i); } catch (IOException ex) { Logger.ErrorException("Error getting studio {0}", ex, i); return null; } catch (AggregateException ex) { Logger.ErrorException("Error getting studio {0}", ex, i); return null; } }) .Where(i => i != null) .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.AllStudios.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, Logger)) { var indexName = LocalizedStrings.Instance.GetString("GenreDispPref"); //we need a copy of this so we don't double-recurse var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList(); return candidates.AsParallel().SelectMany(i => i.AllGenres) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(i => { try { return LibraryManager.GetGenre(i); } catch (Exception ex) { Logger.ErrorException("Error getting genre {0}", ex, i); return null; } }) .Where(i => i != null) .Select(genre => new IndexFolder(this, genre, candidates.Where(i => i.AllGenres.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, Logger)) { var indexName = LocalizedStrings.Instance.GetString("YearDispPref"); //we need a copy of this so we don't double-recurse var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex && i.ProductionYear.HasValue).ToList(); return candidates.AsParallel().Select(i => i.ProductionYear.Value) .Distinct() .Select(i => { try { return LibraryManager.GetYear(i); } catch (IOException ex) { Logger.ErrorException("Error getting year {0}", ex, i); return null; } catch (AggregateException ex) { Logger.ErrorException("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 = null; var cacheKey = user.Name + indexBy; if (IndexCache != null) { 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; /// /// Builds the index. /// /// The index key. /// The index function. /// The user. /// List{BaseItem}. protected virtual List BuildIndex(string indexKey, Func> indexFunction, User user) { if (IndexCache == null) { IndexCache = new ConcurrentDictionary>(); } return indexFunction != null ? IndexCache[user.Name + indexKey] = indexFunction(user).ToList() : null; } #endregion /// /// The children /// private IReadOnlyList _children = new List(); /// /// The _children sync lock /// private readonly object _childrenSyncLock = new object(); /// /// Gets or sets the actual children. /// /// The actual children. protected virtual IEnumerable ActualChildren { get { return _children; } } public void LoadSavedChildren() { _children = LoadChildrenInternal(); } /// /// thread-safe access to the actual children of this folder - without regard to user /// /// The children. [IgnoreDataMember] public IEnumerable 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; } } } } } private List LoadChildrenInternal() { return LoadChildren().ToList(); } /// /// Loads our children. Validation will occur externally. /// We want this sychronous. /// protected virtual IEnumerable LoadChildren() { //just load our children from the repo - the library will be validated and maintained in other processes return 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]. /// if set to true [force refresh metadata]. /// Task. public async Task ValidateChildren(IProgress progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false) { 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, forceRefreshMetadata).ConfigureAwait(false); } catch (OperationCanceledException ex) { Logger.Info("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]. /// if set to true [force refresh metadata]. /// Task. protected async virtual Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false) { var locationType = LocationType; // Nothing to do here if (locationType == LocationType.Remote || locationType == LocationType.Virtual) { return; } cancellationToken.ThrowIfCancellationRequested(); IEnumerable nonCachedChildren; try { nonCachedChildren = GetNonCachedChildren(); } catch (IOException ex) { nonCachedChildren = new BaseItem[] { }; Logger.ErrorException("Error getting file system entries for {0}", ex, Path); } if (nonCachedChildren == null) return; //nothing to validate progress.Report(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>(); var newItems = new ConcurrentBag(); cancellationToken.ThrowIfCancellationRequested(); var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; Parallel.ForEach(nonCachedChildren, options, child => { BaseItem currentChild; if (currentChildren.TryGetValue(child.Id, out currentChild)) { currentChild.ResetResolveArgs(child.ResolveArgs); //existing item - check if it has changed if (currentChild.HasChanged(child)) { EntityResolutionHelper.EnsureDates(currentChild, child.ResolveArgs, false); validChildren.Add(new Tuple(currentChild, true)); } else { validChildren.Add(new Tuple(currentChild, false)); } currentChild.IsOffline = false; } else { //brand new item - needs to be added newItems.Add(child); validChildren.Add(new Tuple(child, true)); } }); // If any items were added or removed.... if (!newItems.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 var itemsRemoved = currentChildren.Values.Except(newChildren).ToList(); var actualRemovals = new List(); foreach (var item in itemsRemoved) { if (IsRootPathAvailable(item.Path)) { item.IsOffline = false; actualRemovals.Add(item); } else { item.IsOffline = true; validChildren.Add(new Tuple(item, false)); } } if (actualRemovals.Count > 0) { RemoveChildrenInternal(actualRemovals); foreach (var item in actualRemovals) { LibraryManager.ReportItemRemoved(item); } } await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false); AddChildrenInternal(newItems); await ItemRepository.SaveChildren(Id, _children.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); //force the indexes to rebuild next time if (IndexCache != null) { IndexCache.Clear(); } } progress.Report(10); cancellationToken.ThrowIfCancellationRequested(); await RefreshChildren(validChildren, progress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false); progress.Report(100); } /// /// Refreshes the children. /// /// The children. /// The progress. /// The cancellation token. /// if set to true [recursive]. /// if set to true [force refresh metadata]. /// Task. private async Task RefreshChildren(IEnumerable> children, IProgress progress, CancellationToken cancellationToken, bool? recursive, bool forceRefreshMetadata = false) { var list = children.ToList(); var percentages = new Dictionary(list.Count); var tasks = new List(); foreach (var tuple in list) { if (tasks.Count > 5) { await Task.WhenAll(tasks).ConfigureAwait(false); } Tuple currentTuple = tuple; tasks.Add(Task.Run(async () => { cancellationToken.ThrowIfCancellationRequested(); var child = currentTuple.Item1; //refresh it await child.RefreshMetadata(cancellationToken, forceSave: currentTuple.Item2, forceRefresh: forceRefreshMetadata, resetResolveArgs: false).ConfigureAwait(false); // Refresh children if a folder and the item changed or recursive is set to true var refreshChildren = child.IsFolder && (currentTuple.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(); var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(p => { lock (percentages) { percentages[child.Id] = p / 100; var percent = percentages.Values.Sum(); percent /= list.Count; progress.Report((90 * percent) + 10); } }); await ((Folder)child).ValidateChildren(innerProgress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false); // Some folder providers are unable to refresh until children have been refreshed. await child.RefreshMetadata(cancellationToken, resetResolveArgs: false).ConfigureAwait(false); } else { lock (percentages) { percentages[child.Id] = 1; var percent = percentages.Values.Sum(); percent /= list.Count; progress.Report((90 * percent) + 10); } } })); } cancellationToken.ThrowIfCancellationRequested(); await Task.WhenAll(tasks).ConfigureAwait(false); } /// /// Determines if a path's root is available or not /// /// /// private bool IsRootPathAvailable(string path) { if (File.Exists(path)) { return true; } // Depending on whether the path is local or unc, it may return either null or '\' at the top while (!string.IsNullOrEmpty(path) && path.Length > 1) { if (Directory.Exists(path)) { return true; } path = System.IO.Path.GetDirectoryName(path); } return false; } /// /// Get the children of this folder from the actual file system /// /// IEnumerable{BaseItem}. protected virtual IEnumerable GetNonCachedChildren() { if (ResolveArgs == null || ResolveArgs.FileSystemDictionary == null) { Logger.Error("Null for {0}", Path); } return LibraryManager.ResolvePaths(ResolveArgs.FileSystemChildren, this); } /// /// Get our children from the repo - stubbed for now /// /// IEnumerable{BaseItem}. protected IEnumerable GetCachedChildren() { return ItemRepository.GetChildren(Id).Select(RetrieveChild).Where(i => i != null); } /// /// Retrieves the child. /// /// The child. /// BaseItem. private BaseItem RetrieveChild(Guid child) { var item = LibraryManager.RetrieveItem(child); if (item != null) { if (item is IByReferenceItem) { return LibraryManager.GetOrAddByReferenceItem(item); } item.Parent = this; } return item; } /// /// Gets allowed children of an item /// /// The user. /// if set to true [include linked children]. /// The index by. /// IEnumerable{BaseItem}. /// public virtual IEnumerable GetChildren(User user, bool includeLinkedChildren, string indexBy = null) { if (user == null) { throw new ArgumentNullException(); } //the true root should return our users root folder children if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, includeLinkedChildren, indexBy); IEnumerable result = null; if (!string.IsNullOrEmpty(indexBy)) { result = GetIndexedChildren(user, indexBy); } if (result != null) { return result; } return GetChildrenList(user, includeLinkedChildren); } /// /// Gets the children list. /// /// The user. /// if set to true [include linked children]. /// List{BaseItem}. private List GetChildrenList(User user, bool includeLinkedChildren) { var list = new List(); foreach (var child in Children) { if (child.IsVisible(user)) { list.Add(child); } } if (includeLinkedChildren) { foreach (var child in GetLinkedChildren()) { if (child.IsVisible(user)) { list.Add(child); } } } return list; } /// /// Gets allowed recursive children of an item /// /// The user. /// if set to true [include linked children]. /// IEnumerable{BaseItem}. /// public IEnumerable GetRecursiveChildren(User user, bool includeLinkedChildren = true) { if (user == null) { throw new ArgumentNullException(); } var initialCount = _children.Count; var list = new List(initialCount); AddRecursiveChildrenInternal(user, includeLinkedChildren, list); if (includeLinkedChildren) { list = list.Distinct().ToList(); } return list; } /// /// Adds the recursive children internal. /// /// The user. /// if set to true [include linked children]. /// The list. private void AddRecursiveChildrenInternal(User user, bool includeLinkedChildren, List list) { foreach (var item in GetChildrenList(user, includeLinkedChildren)) { list.Add(item); var subFolder = item as Folder; if (subFolder != null) { subFolder.AddRecursiveChildrenInternal(user, includeLinkedChildren, list); } } } /// /// Gets the linked children. /// /// IEnumerable{BaseItem}. public IEnumerable GetLinkedChildren() { return LinkedChildren .Select(GetLinkedChild) .Where(i => i != null); } /// /// Gets the linked child. /// /// The info. /// BaseItem. private BaseItem GetLinkedChild(LinkedChild info) { if (string.IsNullOrEmpty(info.Path)) { throw new ArgumentException("Encountered linked child with empty path."); } var item = LibraryManager.RootFolder.FindByPath(info.Path); if (item == null) { Logger.Warn("Unable to find linked item at {0}", info.Path); } return item; } public override async Task RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true) { var changed = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false); return changed || (SupportsShortcutChildren && LocationType == LocationType.FileSystem && RefreshLinkedChildren()); } /// /// Refreshes the linked children. /// /// true if XXXX, false otherwise private bool RefreshLinkedChildren() { ItemResolveArgs resolveArgs; try { resolveArgs = ResolveArgs; if (!resolveArgs.IsDirectory) { return false; } } catch (IOException ex) { Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path); return false; } var currentManualLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Manual).ToList(); var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList(); var newShortcutLinks = resolveArgs.FileSystemChildren .Where(i => (i.Attributes & FileAttributes.Directory) != FileAttributes.Directory && FileSystem.IsShortcut(i.FullName)) .Select(i => { try { Logger.Debug("Found shortcut at {0}", i.FullName); var resolvedPath = FileSystem.ResolveShortcut(i.FullName); if (!string.IsNullOrEmpty(resolvedPath)) { return new LinkedChild { Path = resolvedPath, Type = LinkedChildType.Shortcut }; } Logger.Error("Error resolving shortcut {0}", i.FullName); return null; } catch (IOException ex) { Logger.ErrorException("Error resolving shortcut {0}", ex, i.FullName); return null; } }) .Where(i => i != null) .ToList(); if (!newShortcutLinks.SequenceEqual(currentShortcutLinks)) { Logger.Info("Shortcut links have changed for {0}", Path); newShortcutLinks.AddRange(currentManualLinks); LinkedChildren = newShortcutLinks; return true; } return false; } /// /// 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 played. /// /// The user. /// The date played. /// The user manager. /// Task. public override async Task MarkPlayed(User user, DateTime? datePlayed, IUserDataRepository userManager) { // Sweep through recursively and update status var tasks = GetRecursiveChildren(user, true).Where(i => !i.IsFolder).Select(c => c.MarkPlayed(user, datePlayed, userManager)); await Task.WhenAll(tasks).ConfigureAwait(false); } /// /// Marks the unplayed. /// /// The user. /// The user manager. /// Task. public override async Task MarkUnplayed(User user, IUserDataRepository userManager) { // Sweep through recursively and update status var tasks = GetRecursiveChildren(user, true).Where(i => !i.IsFolder).Select(c => c.MarkUnplayed(user, userManager)); await Task.WhenAll(tasks).ConfigureAwait(false); } /// /// 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.ErrorException("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.ErrorException("Error getting ResolveArgs for {0}", ex, Path); return false; } }); } } }