|
@@ -6,7 +6,6 @@ using MediaBrowser.Controller.Providers;
|
|
|
using MediaBrowser.Model.Dto;
|
|
|
using MediaBrowser.Model.Entities;
|
|
|
using MediaBrowser.Model.Querying;
|
|
|
-using MoreLinq;
|
|
|
using System;
|
|
|
using System.Collections;
|
|
|
using System.Collections.Generic;
|
|
@@ -15,13 +14,14 @@ using System.Linq;
|
|
|
using System.Runtime.Serialization;
|
|
|
using System.Threading;
|
|
|
using System.Threading.Tasks;
|
|
|
+using MediaBrowser.Model.Users;
|
|
|
|
|
|
namespace MediaBrowser.Controller.Entities
|
|
|
{
|
|
|
/// <summary>
|
|
|
/// Class Folder
|
|
|
/// </summary>
|
|
|
- public class Folder : BaseItem, IHasThemeMedia, IHasTags
|
|
|
+ public class Folder : BaseItem, IHasThemeMedia, IHasTags, IHasPreferredMetadataLanguage
|
|
|
{
|
|
|
public static IUserManager UserManager { get; set; }
|
|
|
public static IUserViewManager UserViewManager { get; set; }
|
|
@@ -30,6 +30,14 @@ namespace MediaBrowser.Controller.Entities
|
|
|
public List<Guid> ThemeVideoIds { get; set; }
|
|
|
public List<string> Tags { get; set; }
|
|
|
|
|
|
+ public string PreferredMetadataLanguage { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets the preferred metadata country code.
|
|
|
+ /// </summary>
|
|
|
+ /// <value>The preferred metadata country code.</value>
|
|
|
+ public string PreferredMetadataCountryCode { get; set; }
|
|
|
+
|
|
|
public Folder()
|
|
|
{
|
|
|
LinkedChildren = new List<LinkedChild>();
|
|
@@ -72,6 +80,19 @@ namespace MediaBrowser.Controller.Entities
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ protected override bool IsAllowTagFilterEnforced()
|
|
|
+ {
|
|
|
+ if (this is ICollectionFolder)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (this is UserView)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// Gets or sets a value indicating whether this instance is physical root.
|
|
|
/// </summary>
|
|
@@ -98,6 +119,7 @@ namespace MediaBrowser.Controller.Entities
|
|
|
|
|
|
public virtual List<LinkedChild> LinkedChildren { get; set; }
|
|
|
|
|
|
+ [IgnoreDataMember]
|
|
|
protected virtual bool SupportsShortcutChildren
|
|
|
{
|
|
|
get { return true; }
|
|
@@ -237,14 +259,13 @@ namespace MediaBrowser.Controller.Entities
|
|
|
protected virtual IEnumerable<string> GetIndexByOptions()
|
|
|
{
|
|
|
return new List<string> {
|
|
|
- {LocalizedStrings.Instance.GetString("NoneDispPref")},
|
|
|
- {LocalizedStrings.Instance.GetString("PerformerDispPref")},
|
|
|
- {LocalizedStrings.Instance.GetString("GenreDispPref")},
|
|
|
- {LocalizedStrings.Instance.GetString("DirectorDispPref")},
|
|
|
- {LocalizedStrings.Instance.GetString("YearDispPref")},
|
|
|
- {LocalizedStrings.Instance.GetString("StudioDispPref")}
|
|
|
+ {"None"},
|
|
|
+ {"Performer"},
|
|
|
+ {"Genre"},
|
|
|
+ {"Director"},
|
|
|
+ {"Year"},
|
|
|
+ {"Studio"}
|
|
|
};
|
|
|
-
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -275,7 +296,17 @@ namespace MediaBrowser.Controller.Entities
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
- return _children ?? (_children = LoadChildrenInternal());
|
|
|
+ if (_children == null)
|
|
|
+ {
|
|
|
+ lock (_childrenSyncLock)
|
|
|
+ {
|
|
|
+ if (_children == null)
|
|
|
+ {
|
|
|
+ _children = LoadChildrenInternal();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return _children;
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -301,14 +332,24 @@ namespace MediaBrowser.Controller.Entities
|
|
|
|
|
|
public override bool IsVisible(User user)
|
|
|
{
|
|
|
- if (this is ICollectionFolder)
|
|
|
+ if (this is ICollectionFolder && !(this is BasePluginFolder))
|
|
|
{
|
|
|
- if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
|
|
|
+ if (user.Policy.BlockedMediaFolders != null)
|
|
|
+ {
|
|
|
+ if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
|
|
|
|
|
|
- // Backwards compatibility
|
|
|
- user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
|
|
|
+ // Backwards compatibility
|
|
|
+ user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
{
|
|
|
- return false;
|
|
|
+ if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -345,12 +386,7 @@ namespace MediaBrowser.Controller.Entities
|
|
|
/// <returns>Task.</returns>
|
|
|
public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true)
|
|
|
{
|
|
|
- return ValidateChildrenWithCancellationSupport(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
|
|
|
- }
|
|
|
-
|
|
|
- private Task ValidateChildrenWithCancellationSupport(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
|
|
|
- {
|
|
|
- return ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
|
|
|
+ return ValidateChildrenInternal(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
|
|
|
}
|
|
|
|
|
|
private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
|
|
@@ -540,50 +576,49 @@ namespace MediaBrowser.Controller.Entities
|
|
|
var children = ActualChildren.ToList();
|
|
|
|
|
|
var percentages = new Dictionary<Guid, double>(children.Count);
|
|
|
-
|
|
|
- var tasks = new List<Task>();
|
|
|
+ var numComplete = 0;
|
|
|
+ var count = children.Count;
|
|
|
|
|
|
foreach (var child in children)
|
|
|
{
|
|
|
- if (tasks.Count >= 2)
|
|
|
- {
|
|
|
- await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
|
- tasks.Clear();
|
|
|
- }
|
|
|
-
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
- var innerProgress = new ActionableProgress<double>();
|
|
|
|
|
|
- // Avoid implicitly captured closure
|
|
|
- var currentChild = child;
|
|
|
- innerProgress.RegisterAction(p =>
|
|
|
+ if (child.IsFolder)
|
|
|
{
|
|
|
- lock (percentages)
|
|
|
+ var innerProgress = new ActionableProgress<double>();
|
|
|
+
|
|
|
+ // Avoid implicitly captured closure
|
|
|
+ var currentChild = child;
|
|
|
+ innerProgress.RegisterAction(p =>
|
|
|
{
|
|
|
- percentages[currentChild.Id] = p / 100;
|
|
|
+ lock (percentages)
|
|
|
+ {
|
|
|
+ percentages[currentChild.Id] = p / 100;
|
|
|
|
|
|
- var percent = percentages.Values.Sum();
|
|
|
- percent /= children.Count;
|
|
|
- percent *= 100;
|
|
|
- progress.Report(percent);
|
|
|
- }
|
|
|
- });
|
|
|
+ var innerPercent = percentages.Values.Sum();
|
|
|
+ innerPercent /= count;
|
|
|
+ innerPercent *= 100;
|
|
|
+ progress.Report(innerPercent);
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- 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 RefreshChildMetadata(child, refreshOptions, false, new Progress<double>(), cancellationToken)
|
|
|
+ .ConfigureAwait(false);
|
|
|
}
|
|
|
+
|
|
|
+ numComplete++;
|
|
|
+ double percent = numComplete;
|
|
|
+ percent /= count;
|
|
|
+ percent *= 100;
|
|
|
+
|
|
|
+ progress.Report(percent);
|
|
|
}
|
|
|
|
|
|
- await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
|
progress.Report(100);
|
|
|
}
|
|
|
|
|
@@ -648,7 +683,7 @@ namespace MediaBrowser.Controller.Entities
|
|
|
}
|
|
|
});
|
|
|
|
|
|
- await child.ValidateChildrenWithCancellationSupport(innerProgress, cancellationToken, true, false, null, directoryService)
|
|
|
+ await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService)
|
|
|
.ConfigureAwait(false);
|
|
|
}
|
|
|
}
|
|
@@ -678,12 +713,12 @@ namespace MediaBrowser.Controller.Entities
|
|
|
path = System.IO.Path.GetDirectoryName(path);
|
|
|
}
|
|
|
|
|
|
- if (ContainsPath(LibraryManager.GetDefaultVirtualFolders(), originalPath))
|
|
|
+ if (ContainsPath(LibraryManager.GetVirtualFolders(), originalPath))
|
|
|
{
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- return UserManager.Users.Any(user => ContainsPath(LibraryManager.GetVirtualFolders(user), originalPath));
|
|
|
+ return ContainsPath(LibraryManager.GetVirtualFolders(), originalPath);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -731,28 +766,6 @@ namespace MediaBrowser.Controller.Entities
|
|
|
return childrenItems;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Retrieves the child.
|
|
|
- /// </summary>
|
|
|
- /// <param name="child">The child.</param>
|
|
|
- /// <returns>BaseItem.</returns>
|
|
|
- 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;
|
|
|
- }
|
|
|
-
|
|
|
private BaseItem RetrieveChild(BaseItem child)
|
|
|
{
|
|
|
if (child.Id == Guid.Empty)
|
|
@@ -786,18 +799,31 @@ namespace MediaBrowser.Controller.Entities
|
|
|
{
|
|
|
var user = query.User;
|
|
|
|
|
|
- var items = query.Recursive
|
|
|
- ? GetRecursiveChildren(user)
|
|
|
- : GetChildren(user, true);
|
|
|
+ Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
|
|
+
|
|
|
+ IEnumerable<BaseItem> items;
|
|
|
+
|
|
|
+ if (query.User == null)
|
|
|
+ {
|
|
|
+ items = query.Recursive
|
|
|
+ ? GetRecursiveChildren(filter)
|
|
|
+ : Children.Where(filter);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ items = query.Recursive
|
|
|
+ ? GetRecursiveChildren(user, filter)
|
|
|
+ : GetChildren(user, true).Where(filter);
|
|
|
+ }
|
|
|
|
|
|
- var result = SortAndFilter(items, query);
|
|
|
+ var result = PostFilterAndSort(items, query);
|
|
|
|
|
|
return Task.FromResult(result);
|
|
|
}
|
|
|
|
|
|
- protected QueryResult<BaseItem> SortAndFilter(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
|
|
+ protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
|
|
{
|
|
|
- return UserViewBuilder.SortAndFilter(items, this, null, query, LibraryManager, UserDataManager);
|
|
|
+ return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -822,11 +848,11 @@ namespace MediaBrowser.Controller.Entities
|
|
|
//the true root should return our users root folder children
|
|
|
if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, includeLinkedChildren);
|
|
|
|
|
|
- var list = new List<BaseItem>();
|
|
|
+ var result = new Dictionary<Guid, BaseItem>();
|
|
|
|
|
|
- var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, includeHidden, false);
|
|
|
+ AddChildren(user, includeLinkedChildren, result, includeHidden, false, null);
|
|
|
|
|
|
- return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
|
|
|
+ return result.Values;
|
|
|
}
|
|
|
|
|
|
protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
|
@@ -839,31 +865,30 @@ namespace MediaBrowser.Controller.Entities
|
|
|
/// </summary>
|
|
|
/// <param name="user">The user.</param>
|
|
|
/// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
|
|
|
- /// <param name="list">The list.</param>
|
|
|
+ /// <param name="result">The result.</param>
|
|
|
/// <param name="includeHidden">if set to <c>true</c> [include hidden].</param>
|
|
|
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
|
|
|
+ /// <param name="filter">The filter.</param>
|
|
|
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
|
|
- private bool AddChildrenToList(User user, bool includeLinkedChildren, List<BaseItem> list, bool includeHidden, bool recursive)
|
|
|
+ private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool includeHidden, bool recursive, Func<BaseItem, bool> filter)
|
|
|
{
|
|
|
- var hasLinkedChildren = false;
|
|
|
-
|
|
|
foreach (var child in GetEligibleChildrenForRecursiveChildren(user))
|
|
|
{
|
|
|
if (child.IsVisible(user))
|
|
|
{
|
|
|
if (includeHidden || !child.IsHiddenFromUser(user))
|
|
|
{
|
|
|
- list.Add(child);
|
|
|
+ if (filter == null || filter(child))
|
|
|
+ {
|
|
|
+ result[child.Id] = child;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
if (recursive && child.IsFolder)
|
|
|
{
|
|
|
var folder = (Folder)child;
|
|
|
|
|
|
- if (folder.AddChildrenToList(user, includeLinkedChildren, list, includeHidden, true))
|
|
|
- {
|
|
|
- hasLinkedChildren = true;
|
|
|
- }
|
|
|
+ folder.AddChildren(user, includeLinkedChildren, result, includeHidden, true, filter);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -874,14 +899,13 @@ namespace MediaBrowser.Controller.Entities
|
|
|
{
|
|
|
if (child.IsVisible(user))
|
|
|
{
|
|
|
- hasLinkedChildren = true;
|
|
|
-
|
|
|
- list.Add(child);
|
|
|
+ if (filter == null || filter(child))
|
|
|
+ {
|
|
|
+ result[child.Id] = child;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- return hasLinkedChildren;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -891,18 +915,23 @@ namespace MediaBrowser.Controller.Entities
|
|
|
/// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
|
|
|
/// <returns>IEnumerable{BaseItem}.</returns>
|
|
|
/// <exception cref="System.ArgumentNullException"></exception>
|
|
|
- public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
|
|
|
+ public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
|
|
|
+ {
|
|
|
+ return GetRecursiveChildren(user, i => true);
|
|
|
+ }
|
|
|
+
|
|
|
+ public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, Func<BaseItem, bool> filter)
|
|
|
{
|
|
|
if (user == null)
|
|
|
{
|
|
|
throw new ArgumentNullException("user");
|
|
|
}
|
|
|
|
|
|
- var list = new List<BaseItem>();
|
|
|
+ var result = new Dictionary<Guid, BaseItem>();
|
|
|
|
|
|
- var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, false, true);
|
|
|
+ AddChildren(user, true, result, false, true, filter);
|
|
|
|
|
|
- return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
|
|
|
+ return result.Values;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -910,10 +939,15 @@ namespace MediaBrowser.Controller.Entities
|
|
|
/// </summary>
|
|
|
/// <returns>IList{BaseItem}.</returns>
|
|
|
public IList<BaseItem> GetRecursiveChildren()
|
|
|
+ {
|
|
|
+ return GetRecursiveChildren(i => true);
|
|
|
+ }
|
|
|
+
|
|
|
+ public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
|
|
|
{
|
|
|
var list = new List<BaseItem>();
|
|
|
|
|
|
- AddChildrenToList(list, true, null);
|
|
|
+ AddChildrenToList(list, true, filter);
|
|
|
|
|
|
return list;
|
|
|
}
|
|
@@ -1022,6 +1056,15 @@ namespace MediaBrowser.Controller.Entities
|
|
|
.Where(i => i.Item2 != null);
|
|
|
}
|
|
|
|
|
|
+ [IgnoreDataMember]
|
|
|
+ protected override bool SupportsOwnedItems
|
|
|
+ {
|
|
|
+ get
|
|
|
+ {
|
|
|
+ return base.SupportsOwnedItems || SupportsShortcutChildren;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
|
|
|
{
|
|
|
var changesFound = false;
|
|
@@ -1126,8 +1169,7 @@ namespace MediaBrowser.Controller.Entities
|
|
|
bool resetPosition)
|
|
|
{
|
|
|
// Sweep through recursively and update status
|
|
|
- var tasks = GetRecursiveChildren(user, true)
|
|
|
- .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
|
|
|
+ var tasks = GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)
|
|
|
.Select(c => c.MarkPlayed(user, datePlayed, resetPosition));
|
|
|
|
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
@@ -1141,8 +1183,7 @@ namespace MediaBrowser.Controller.Entities
|
|
|
public override async Task MarkUnplayed(User user)
|
|
|
{
|
|
|
// Sweep through recursively and update status
|
|
|
- var tasks = GetRecursiveChildren(user, true)
|
|
|
- .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
|
|
|
+ var tasks = GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)
|
|
|
.Select(c => c.MarkUnplayed(user));
|
|
|
|
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
@@ -1171,15 +1212,15 @@ namespace MediaBrowser.Controller.Entities
|
|
|
return this;
|
|
|
}
|
|
|
|
|
|
- return RecursiveChildren.FirstOrDefault(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) ||
|
|
|
+ return GetRecursiveChildren(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));
|
|
|
+ i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
|
|
|
+ .FirstOrDefault();
|
|
|
}
|
|
|
|
|
|
public override bool IsPlayed(User user)
|
|
|
{
|
|
|
- return GetRecursiveChildren(user)
|
|
|
- .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
|
|
|
+ return GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual)
|
|
|
.All(i => i.IsPlayed(user));
|
|
|
}
|
|
|
|
|
@@ -1206,8 +1247,7 @@ namespace MediaBrowser.Controller.Entities
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- children = folder.GetRecursiveChildren(user)
|
|
|
- .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual);
|
|
|
+ children = folder.GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual);
|
|
|
}
|
|
|
|
|
|
// Loop through each recursive child
|