using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MoreLinq; using System; using System.Collections; 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, IHasThemeMedia, IHasTags { public static IUserManager UserManager { get; set; } public List ThemeSongIds { get; set; } public List ThemeVideoIds { get; set; } public List Tags { get; set; } public Folder() { LinkedChildren = new List(); ThemeSongIds = new List(); ThemeVideoIds = new List(); Tags = 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 (ActualChildren.Any(i => i.Id == item.Id)) { throw new ArgumentException(string.Format("A child with the Id {0} already exists.", item.Id)); } 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, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); } protected void AddChildrenInternal(IEnumerable children) { lock (_childrenSyncLock) { var newChildren = ActualChildren.ToList(); newChildren.AddRange(children); _children = newChildren; } } protected void AddChildInternal(BaseItem child) { lock (_childrenSyncLock) { var newChildren = ActualChildren.ToList(); newChildren.Add(child); _children = newChildren; } } protected void RemoveChildrenInternal(IEnumerable children) { var ids = children.Select(i => i.Id).ToList(); lock (_childrenSyncLock) { _children = ActualChildren.Where(i => !ids.Contains(i.Id)).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; 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 /// /// Returns the valid set of index by options for this folder type. /// Override or extend to modify. /// /// Dictionary{System.StringFunc{UserIEnumerable{BaseItem}}}. protected virtual IEnumerable GetIndexByOptions() { return new List { {LocalizedStrings.Instance.GetString("NoneDispPref")}, {LocalizedStrings.Instance.GetString("PerformerDispPref")}, {LocalizedStrings.Instance.GetString("GenreDispPref")}, {LocalizedStrings.Instance.GetString("DirectorDispPref")}, {LocalizedStrings.Instance.GetString("YearDispPref")}, {LocalizedStrings.Instance.GetString("StudioDispPref")} }; } /// /// Get the list of indexy by choices for this folder (localized). /// /// The index by option strings. [IgnoreDataMember] public IEnumerable IndexByOptionStrings { get { return GetIndexByOptions(); } } #endregion /// /// The children /// private IReadOnlyList _children; /// /// 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 ?? (_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 { return GetRecursiveChildren(); } } public override bool IsVisible(User user) { if (this is ICollectionFolder) { if (user.Configuration.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) || // Backwards compatibility user.Configuration.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase)) { return false; } } return base.IsVisible(user); } 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(); } public Task ValidateChildren(IProgress progress, CancellationToken cancellationToken) { return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions()); } /// /// Validates that the children of the folder still exist /// /// The progress. /// The cancellation token. /// The metadata refresh options. /// if set to true [recursive]. /// Task. public Task ValidateChildren(IProgress progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true) { metadataRefreshOptions.DirectoryService = metadataRefreshOptions.DirectoryService ?? new DirectoryService(Logger); return ValidateChildrenWithCancellationSupport(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService); } private Task ValidateChildrenWithCancellationSupport(IProgress progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { return ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); } private Dictionary GetActualChildrenDictionary() { var dictionary = new Dictionary(); foreach (var child in ActualChildren) { var id = child.Id; if (dictionary.ContainsKey(id)) { Logger.Error("Found folder containing items with duplicate id. Path: {0}, Child Name: {1}", Path ?? Name, child.Path ?? child.Name); } else { dictionary[id] = child; } } return dictionary; } private bool IsValidFromResolver(BaseItem current, BaseItem newItem) { var currentAsVideo = current as Video; if (currentAsVideo != null) { var newAsVideo = newItem as Video; if (newAsVideo != null) { if (currentAsVideo.IsPlaceHolder != newAsVideo.IsPlaceHolder) { return false; } if (currentAsVideo.IsMultiPart != newAsVideo.IsMultiPart) { return false; } if (currentAsVideo.HasLocalAlternateVersions != newAsVideo.HasLocalAlternateVersions) { return false; } } } else { var currentAsPlaceHolder = current as ISupportsPlaceHolders; if (currentAsPlaceHolder != null) { var newHasPlaceHolder = newItem as ISupportsPlaceHolders; if (newHasPlaceHolder != null) { if (currentAsPlaceHolder.IsPlaceHolder != newHasPlaceHolder.IsPlaceHolder) { return false; } } } } return current.IsInMixedFolder == newItem.IsInMixedFolder; } /// /// Validates the children internal. /// /// The progress. /// The cancellation token. /// if set to true [recursive]. /// if set to true [refresh child metadata]. /// The refresh options. /// The directory service. /// Task. protected async virtual Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { var locationType = LocationType; cancellationToken.ThrowIfCancellationRequested(); var validChildren = new List(); if (locationType != LocationType.Remote && locationType != LocationType.Virtual) { IEnumerable nonCachedChildren; try { nonCachedChildren = GetNonCachedChildren(directoryService); } 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 = GetActualChildrenDictionary(); //create a list for our validated children var newItems = new List(); cancellationToken.ThrowIfCancellationRequested(); foreach (var child in nonCachedChildren) { BaseItem currentChild; if (currentChildren.TryGetValue(child.Id, out currentChild)) { if (IsValidFromResolver(currentChild, child)) { var currentChildLocationType = currentChild.LocationType; if (currentChildLocationType != LocationType.Remote && currentChildLocationType != LocationType.Virtual) { currentChild.DateModified = child.DateModified; } currentChild.IsOffline = false; validChildren.Add(currentChild); } else { validChildren.Add(child); } } else { // Brand new item - needs to be added newItems.Add(child); validChildren.Add(child); } } // If any items were added or removed.... if (newItems.Count > 0 || currentChildren.Count != validChildren.Count) { // That's all the new and changed ones - now see if there are any that are missing var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); var actualRemovals = new List(); foreach (var item in itemsRemoved) { if (item.LocationType == LocationType.Virtual || item.LocationType == LocationType.Remote) { // Don't remove these because there's no way to accurately validate them. validChildren.Add(item); } else if (!string.IsNullOrEmpty(item.Path) && IsPathOffline(item.Path)) { item.IsOffline = true; validChildren.Add(item); } else { item.IsOffline = false; actualRemovals.Add(item); } } 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, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); } } progress.Report(10); cancellationToken.ThrowIfCancellationRequested(); if (recursive) { await ValidateSubFolders(ActualChildren.OfType().ToList(), directoryService, progress, cancellationToken).ConfigureAwait(false); } progress.Report(20); if (refreshChildMetadata) { var container = this as IMetadataContainer; var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(p => progress.Report((.80 * p) + 20)); if (container != null) { await container.RefreshAllMetadata(refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false); } else { await RefreshMetadataRecursive(refreshOptions, recursive, innerProgress, cancellationToken); } } progress.Report(100); } private async Task RefreshMetadataRecursive(MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { var children = ActualChildren.ToList(); var percentages = new Dictionary(children.Count); var tasks = new List(); foreach (var child in children) { if (tasks.Count >= 3) { await Task.WhenAll(tasks).ConfigureAwait(false); tasks.Clear(); } cancellationToken.ThrowIfCancellationRequested(); var innerProgress = new ActionableProgress(); // Avoid implicitly captured closure var currentChild = child; innerProgress.RegisterAction(p => { lock (percentages) { percentages[currentChild.Id] = p / 100; var percent = percentages.Values.Sum(); percent /= children.Count; percent *= 100; progress.Report(percent); } }); if (child.IsFolder) { await RefreshChildMetadata(child, refreshOptions, recursive, innerProgress, cancellationToken) .ConfigureAwait(false); } else { // Avoid implicitly captured closure var taskChild = child; tasks.Add(Task.Run(async () => await RefreshChildMetadata(taskChild, refreshOptions, false, innerProgress, cancellationToken).ConfigureAwait(false), cancellationToken)); } } await Task.WhenAll(tasks).ConfigureAwait(false); progress.Report(100); } private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { var container = child as IMetadataContainer; if (container != null) { await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); } else { await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); if (recursive) { var folder = child as Folder; if (folder != null) { await folder.RefreshMetadataRecursive(refreshOptions, true, progress, cancellationToken); } } } progress.Report(100); } /// /// Refreshes the children. /// /// The children. /// The directory service. /// The progress. /// The cancellation token. /// Task. private async Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) { var list = children; var childCount = list.Count; var percentages = new Dictionary(list.Count); foreach (var item in list) { cancellationToken.ThrowIfCancellationRequested(); var child = item; var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(p => { lock (percentages) { percentages[child.Id] = p / 100; var percent = percentages.Values.Sum(); percent /= childCount; progress.Report((10 * percent) + 10); } }); await child.ValidateChildrenWithCancellationSupport(innerProgress, cancellationToken, true, false, null, directoryService) .ConfigureAwait(false); } } /// /// Determines whether the specified path is offline. /// /// The path. /// true if the specified path is offline; otherwise, false. private bool IsPathOffline(string path) { if (File.Exists(path)) { return false; } var originalPath = path; // 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 false; } path = System.IO.Path.GetDirectoryName(path); } if (ContainsPath(LibraryManager.GetDefaultVirtualFolders(), originalPath)) { return true; } return UserManager.Users.Any(user => ContainsPath(LibraryManager.GetVirtualFolders(user), originalPath)); } /// /// Determines whether the specified folders contains path. /// /// The folders. /// The path. /// true if the specified folders contains path; otherwise, false. private bool ContainsPath(IEnumerable folders, string path) { return folders.SelectMany(i => i.Locations).Any(i => ContainsPath(i, path)); } private bool ContainsPath(string parent, string path) { return string.Equals(parent, path, StringComparison.OrdinalIgnoreCase) || FileSystem.ContainsSubPath(parent, path); } /// /// Get the children of this folder from the actual file system /// /// IEnumerable{BaseItem}. protected virtual IEnumerable GetNonCachedChildren(IDirectoryService directoryService) { return LibraryManager.ResolvePaths(GetFileSystemChildren(directoryService), directoryService, 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.GetItemById(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]. /// IEnumerable{BaseItem}. /// public virtual IEnumerable GetChildren(User user, bool includeLinkedChildren) { return GetChildren(user, includeLinkedChildren, false); } internal IEnumerable GetChildren(User user, bool includeLinkedChildren, bool includeHidden) { if (user == null) { throw new ArgumentNullException(); } //the true root should return our users root folder children if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, includeLinkedChildren); var list = new List(); var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, includeHidden, false); return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list; } protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(User user) { return Children; } /// /// Adds the children to list. /// /// The user. /// if set to true [include linked children]. /// The list. /// if set to true [include hidden]. /// if set to true [recursive]. /// true if XXXX, false otherwise private bool AddChildrenToList(User user, bool includeLinkedChildren, List list, bool includeHidden, bool recursive) { var hasLinkedChildren = false; foreach (var child in GetEligibleChildrenForRecursiveChildren(user)) { if (child.IsVisible(user)) { if (includeHidden || !child.IsHiddenFromUser(user)) { list.Add(child); } if (recursive && child.IsFolder) { var folder = (Folder)child; if (folder.AddChildrenToList(user, includeLinkedChildren, list, includeHidden, true)) { hasLinkedChildren = true; } } } } if (includeLinkedChildren) { foreach (var child in GetLinkedChildren()) { if (child.IsVisible(user)) { hasLinkedChildren = true; list.Add(child); } } } return hasLinkedChildren; } /// /// 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("user"); } var list = new List(); var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, false, true); return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list; } /// /// Gets the recursive children. /// /// IList{BaseItem}. public IList GetRecursiveChildren() { var list = new List(); AddChildrenToList(list, true, null); return list; } /// /// Adds the children to list. /// /// The list. /// if set to true [recursive]. /// The filter. private void AddChildrenToList(List list, bool recursive, Func filter) { foreach (var child in Children) { if (filter == null || filter(child)) { list.Add(child); } if (recursive && child.IsFolder) { var folder = (Folder)child; folder.AddChildrenToList(list, true, filter); } } } /// /// Gets the linked children. /// /// IEnumerable{BaseItem}. public IEnumerable GetLinkedChildren() { return LinkedChildren .Select(GetLinkedChild) .Where(i => i != null); } protected override async Task RefreshedOwnedItems(MetadataRefreshOptions options, List fileSystemChildren, CancellationToken cancellationToken) { var changesFound = false; if (SupportsShortcutChildren && LocationType == LocationType.FileSystem) { if (RefreshLinkedChildren(fileSystemChildren)) { changesFound = true; } } var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); return baseHasChanges || changesFound; } /// /// Refreshes the linked children. /// /// true if XXXX, false otherwise private bool RefreshLinkedChildren(IEnumerable fileSystemChildren) { var currentManualLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Manual).ToList(); var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList(); var newShortcutLinks = 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, new LinkedChildComparer())) { Logger.Info("Shortcut links have changed for {0}", Path); newShortcutLinks.AddRange(currentManualLinks); LinkedChildren = newShortcutLinks; return true; } foreach (var child in LinkedChildren) { // Reset the cached value if (child.ItemId.HasValue && child.ItemId.Value == Guid.Empty) { child.ItemId = null; } } return false; } /// /// Folders need to validate and refresh /// /// Task. public override async Task ChangedExternally() { var progress = new Progress(); await ValidateChildren(progress, CancellationToken.None).ConfigureAwait(false); await base.ChangedExternally().ConfigureAwait(false); } /// /// Marks the played. /// /// The user. /// The date played. /// The user manager. /// Task. public override async Task MarkPlayed(User user, DateTime? datePlayed, IUserDataManager userManager) { // Sweep through recursively and update status var tasks = GetRecursiveChildren(user, true).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual).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, IUserDataManager userManager) { // Sweep through recursively and update status var tasks = GetRecursiveChildren(user, true).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual).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(); } if (string.Equals(Path, path, StringComparison.OrdinalIgnoreCase)) { return this; } if (PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase)) { return this; } return RecursiveChildren.FirstOrDefault(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) || (!i.IsFolder && !i.IsInMixedFolder && string.Equals(i.ContainingFolderPath, path, StringComparison.OrdinalIgnoreCase)) || i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase)); } public override bool IsPlayed(User user) { return GetRecursiveChildren(user).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual) .All(i => i.IsPlayed(user)); } public override bool IsUnplayed(User user) { return GetRecursiveChildren(user).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual) .All(i => i.IsUnplayed(user)); } public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, User user) { var recursiveItemCount = 0; var unplayed = 0; double totalPercentPlayed = 0; IEnumerable children; var folder = this; var season = folder as Season; if (season != null) { children = season.GetEpisodes(user).Where(i => i.LocationType != LocationType.Virtual); } else { children = folder.GetRecursiveChildren(user) .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual); } // Loop through each recursive child foreach (var child in children) { recursiveItemCount++; var isUnplayed = true; var itemUserData = UserDataManager.GetUserData(user.Id, child.GetUserDataKey()); // Incrememt totalPercentPlayed if (itemUserData != null) { if (itemUserData.Played) { totalPercentPlayed += 100; isUnplayed = false; } else if (itemUserData.PlaybackPositionTicks > 0 && child.RunTimeTicks.HasValue && child.RunTimeTicks.Value > 0) { double itemPercent = itemUserData.PlaybackPositionTicks; itemPercent /= child.RunTimeTicks.Value; totalPercentPlayed += itemPercent; } } if (isUnplayed) { unplayed++; } } dto.UnplayedItemCount = unplayed; if (recursiveItemCount > 0) { dto.PlayedPercentage = totalPercentPlayed / recursiveItemCount; dto.Played = dto.PlayedPercentage.Value >= 100; } } } }