Folder.cs 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Common.Progress;
  3. using MediaBrowser.Controller.Entities.TV;
  4. using MediaBrowser.Controller.IO;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.Localization;
  7. using MediaBrowser.Controller.Persistence;
  8. using MediaBrowser.Controller.Resolvers;
  9. using MediaBrowser.Model.Entities;
  10. using System;
  11. using System.Collections;
  12. using System.Collections.Concurrent;
  13. using System.Collections.Generic;
  14. using System.IO;
  15. using System.Linq;
  16. using System.Runtime.Serialization;
  17. using System.Threading;
  18. using System.Threading.Tasks;
  19. using MoreLinq;
  20. namespace MediaBrowser.Controller.Entities
  21. {
  22. /// <summary>
  23. /// Class Folder
  24. /// </summary>
  25. public class Folder : BaseItem
  26. {
  27. public Folder()
  28. {
  29. LinkedChildren = new List<LinkedChild>();
  30. }
  31. /// <summary>
  32. /// Gets a value indicating whether this instance is folder.
  33. /// </summary>
  34. /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
  35. [IgnoreDataMember]
  36. public override bool IsFolder
  37. {
  38. get
  39. {
  40. return true;
  41. }
  42. }
  43. /// <summary>
  44. /// Gets or sets a value indicating whether this instance is physical root.
  45. /// </summary>
  46. /// <value><c>true</c> if this instance is physical root; otherwise, <c>false</c>.</value>
  47. public bool IsPhysicalRoot { get; set; }
  48. /// <summary>
  49. /// Gets or sets a value indicating whether this instance is root.
  50. /// </summary>
  51. /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
  52. public bool IsRoot { get; set; }
  53. /// <summary>
  54. /// Gets a value indicating whether this instance is virtual folder.
  55. /// </summary>
  56. /// <value><c>true</c> if this instance is virtual folder; otherwise, <c>false</c>.</value>
  57. [IgnoreDataMember]
  58. public virtual bool IsVirtualFolder
  59. {
  60. get
  61. {
  62. return false;
  63. }
  64. }
  65. public virtual List<LinkedChild> LinkedChildren { get; set; }
  66. protected virtual bool SupportsShortcutChildren
  67. {
  68. get { return true; }
  69. }
  70. /// <summary>
  71. /// Adds the child.
  72. /// </summary>
  73. /// <param name="item">The item.</param>
  74. /// <param name="cancellationToken">The cancellation token.</param>
  75. /// <returns>Task.</returns>
  76. /// <exception cref="System.InvalidOperationException">Unable to add + item.Name</exception>
  77. public async Task AddChild(BaseItem item, CancellationToken cancellationToken)
  78. {
  79. item.Parent = this;
  80. if (item.Id == Guid.Empty)
  81. {
  82. item.Id = item.Path.GetMBId(item.GetType());
  83. }
  84. if (item.DateCreated == DateTime.MinValue)
  85. {
  86. item.DateCreated = DateTime.UtcNow;
  87. }
  88. if (item.DateModified == DateTime.MinValue)
  89. {
  90. item.DateModified = DateTime.UtcNow;
  91. }
  92. _children.Add(item);
  93. await LibraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
  94. await ItemRepository.SaveChildren(Id, _children.ToList().Select(i => i.Id), cancellationToken).ConfigureAwait(false);
  95. }
  96. /// <summary>
  97. /// Never want folders to be blocked by "BlockNotRated"
  98. /// </summary>
  99. [IgnoreDataMember]
  100. public override string OfficialRatingForComparison
  101. {
  102. get
  103. {
  104. if (this is Series)
  105. {
  106. return base.OfficialRatingForComparison;
  107. }
  108. return !string.IsNullOrEmpty(base.OfficialRatingForComparison) ? base.OfficialRatingForComparison : "None";
  109. }
  110. }
  111. /// <summary>
  112. /// Removes the child.
  113. /// </summary>
  114. /// <param name="item">The item.</param>
  115. /// <param name="cancellationToken">The cancellation token.</param>
  116. /// <returns>Task.</returns>
  117. /// <exception cref="System.InvalidOperationException">Unable to remove + item.Name</exception>
  118. public Task RemoveChild(BaseItem item, CancellationToken cancellationToken)
  119. {
  120. List<BaseItem> newChildren;
  121. lock (ChildrenSyncLock)
  122. {
  123. newChildren = _children.ToList();
  124. newChildren.Remove(item);
  125. _children = new ConcurrentBag<BaseItem>(newChildren);
  126. }
  127. item.Parent = null;
  128. LibraryManager.ReportItemRemoved(item);
  129. return ItemRepository.SaveChildren(Id, newChildren.Select(i => i.Id), cancellationToken);
  130. }
  131. #region Indexing
  132. /// <summary>
  133. /// The _index by options
  134. /// </summary>
  135. private Dictionary<string, Func<User, IEnumerable<BaseItem>>> _indexByOptions;
  136. /// <summary>
  137. /// Dictionary of index options - consists of a display value and an indexing function
  138. /// which takes User as a parameter and returns an IEnum of BaseItem
  139. /// </summary>
  140. /// <value>The index by options.</value>
  141. [IgnoreDataMember]
  142. public Dictionary<string, Func<User, IEnumerable<BaseItem>>> IndexByOptions
  143. {
  144. get { return _indexByOptions ?? (_indexByOptions = GetIndexByOptions()); }
  145. }
  146. /// <summary>
  147. /// Returns the valid set of index by options for this folder type.
  148. /// Override or extend to modify.
  149. /// </summary>
  150. /// <returns>Dictionary{System.StringFunc{UserIEnumerable{BaseItem}}}.</returns>
  151. protected virtual Dictionary<string, Func<User, IEnumerable<BaseItem>>> GetIndexByOptions()
  152. {
  153. return new Dictionary<string, Func<User, IEnumerable<BaseItem>>> {
  154. {LocalizedStrings.Instance.GetString("NoneDispPref"), null},
  155. {LocalizedStrings.Instance.GetString("PerformerDispPref"), GetIndexByPerformer},
  156. {LocalizedStrings.Instance.GetString("GenreDispPref"), GetIndexByGenre},
  157. {LocalizedStrings.Instance.GetString("DirectorDispPref"), GetIndexByDirector},
  158. {LocalizedStrings.Instance.GetString("YearDispPref"), GetIndexByYear},
  159. //{LocalizedStrings.Instance.GetString("OfficialRatingDispPref"), null},
  160. {LocalizedStrings.Instance.GetString("StudioDispPref"), GetIndexByStudio}
  161. };
  162. }
  163. /// <summary>
  164. /// Gets the index by actor.
  165. /// </summary>
  166. /// <param name="user">The user.</param>
  167. /// <returns>IEnumerable{BaseItem}.</returns>
  168. protected IEnumerable<BaseItem> GetIndexByPerformer(User user)
  169. {
  170. return GetIndexByPerson(user, new List<string> { PersonType.Actor, PersonType.GuestStar }, true, LocalizedStrings.Instance.GetString("PerformerDispPref"));
  171. }
  172. /// <summary>
  173. /// Gets the index by director.
  174. /// </summary>
  175. /// <param name="user">The user.</param>
  176. /// <returns>IEnumerable{BaseItem}.</returns>
  177. protected IEnumerable<BaseItem> GetIndexByDirector(User user)
  178. {
  179. return GetIndexByPerson(user, new List<string> { PersonType.Director }, false, LocalizedStrings.Instance.GetString("DirectorDispPref"));
  180. }
  181. /// <summary>
  182. /// Gets the index by person.
  183. /// </summary>
  184. /// <param name="user">The user.</param>
  185. /// <param name="personTypes">The person types we should match on</param>
  186. /// <param name="includeAudio">if set to <c>true</c> [include audio].</param>
  187. /// <param name="indexName">Name of the index.</param>
  188. /// <returns>IEnumerable{BaseItem}.</returns>
  189. private IEnumerable<BaseItem> GetIndexByPerson(User user, List<string> personTypes, bool includeAudio, string indexName)
  190. {
  191. // Even though this implementation means multiple iterations over the target list - it allows us to defer
  192. // the retrieval of the individual children for each index value until they are requested.
  193. using (new Profiler(indexName + " Index Build for " + Name, Logger))
  194. {
  195. // Put this in a local variable to avoid an implicitly captured closure
  196. var currentIndexName = indexName;
  197. var us = this;
  198. var recursiveChildren = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList();
  199. // Get the candidates, but handle audio separately
  200. var candidates = recursiveChildren.Where(i => i.AllPeople != null && !(i is Audio.Audio)).ToList();
  201. var indexFolders = candidates.AsParallel().SelectMany(i => i.AllPeople.Where(p => personTypes.Contains(p.Type))
  202. .Select(a => a.Name))
  203. .Distinct()
  204. .Select(i =>
  205. {
  206. try
  207. {
  208. return LibraryManager.GetPerson(i);
  209. }
  210. catch (IOException ex)
  211. {
  212. Logger.ErrorException("Error getting person {0}", ex, i);
  213. return null;
  214. }
  215. catch (AggregateException ex)
  216. {
  217. Logger.ErrorException("Error getting person {0}", ex, i);
  218. return null;
  219. }
  220. })
  221. .Where(i => i != null)
  222. .Select(a => new IndexFolder(us, a,
  223. candidates.Where(i => i.AllPeople.Any(p => personTypes.Contains(p.Type) && p.Name.Equals(a.Name, StringComparison.OrdinalIgnoreCase))
  224. ), currentIndexName)).AsEnumerable();
  225. if (includeAudio)
  226. {
  227. var songs = recursiveChildren.OfType<Audio.Audio>().ToList();
  228. indexFolders = songs.SelectMany(i => i.Artists)
  229. .Distinct(StringComparer.OrdinalIgnoreCase)
  230. .Select(i =>
  231. {
  232. try
  233. {
  234. return LibraryManager.GetArtist(i);
  235. }
  236. catch (IOException ex)
  237. {
  238. Logger.ErrorException("Error getting artist {0}", ex, i);
  239. return null;
  240. }
  241. catch (AggregateException ex)
  242. {
  243. Logger.ErrorException("Error getting artist {0}", ex, i);
  244. return null;
  245. }
  246. })
  247. .Where(i => i != null)
  248. .Select(a => new IndexFolder(us, a,
  249. songs.Where(i => i.Artists.Contains(a.Name, StringComparer.OrdinalIgnoreCase)
  250. ), currentIndexName)).Concat(indexFolders);
  251. }
  252. return indexFolders;
  253. }
  254. }
  255. /// <summary>
  256. /// Gets the index by studio.
  257. /// </summary>
  258. /// <param name="user">The user.</param>
  259. /// <returns>IEnumerable{BaseItem}.</returns>
  260. protected IEnumerable<BaseItem> GetIndexByStudio(User user)
  261. {
  262. // Even though this implementation means multiple iterations over the target list - it allows us to defer
  263. // the retrieval of the individual children for each index value until they are requested.
  264. using (new Profiler("Studio Index Build for " + Name, Logger))
  265. {
  266. var indexName = LocalizedStrings.Instance.GetString("StudioDispPref");
  267. var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList();
  268. return candidates.AsParallel().SelectMany(i => i.AllStudios)
  269. .Distinct()
  270. .Select(i =>
  271. {
  272. try
  273. {
  274. return LibraryManager.GetStudio(i);
  275. }
  276. catch (IOException ex)
  277. {
  278. Logger.ErrorException("Error getting studio {0}", ex, i);
  279. return null;
  280. }
  281. catch (AggregateException ex)
  282. {
  283. Logger.ErrorException("Error getting studio {0}", ex, i);
  284. return null;
  285. }
  286. })
  287. .Where(i => i != null)
  288. .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.AllStudios.Any(s => s.Equals(ndx.Name, StringComparison.OrdinalIgnoreCase))), indexName));
  289. }
  290. }
  291. /// <summary>
  292. /// Gets the index by genre.
  293. /// </summary>
  294. /// <param name="user">The user.</param>
  295. /// <returns>IEnumerable{BaseItem}.</returns>
  296. protected IEnumerable<BaseItem> GetIndexByGenre(User user)
  297. {
  298. // Even though this implementation means multiple iterations over the target list - it allows us to defer
  299. // the retrieval of the individual children for each index value until they are requested.
  300. using (new Profiler("Genre Index Build for " + Name, Logger))
  301. {
  302. var indexName = LocalizedStrings.Instance.GetString("GenreDispPref");
  303. //we need a copy of this so we don't double-recurse
  304. var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex).ToList();
  305. return candidates.AsParallel().SelectMany(i => i.AllGenres)
  306. .Distinct(StringComparer.OrdinalIgnoreCase)
  307. .Select(i =>
  308. {
  309. try
  310. {
  311. return LibraryManager.GetGenre(i);
  312. }
  313. catch (Exception ex)
  314. {
  315. Logger.ErrorException("Error getting genre {0}", ex, i);
  316. return null;
  317. }
  318. })
  319. .Where(i => i != null)
  320. .Select(genre => new IndexFolder(this, genre, candidates.Where(i => i.AllGenres.Any(g => g.Equals(genre.Name, StringComparison.OrdinalIgnoreCase))), indexName)
  321. );
  322. }
  323. }
  324. /// <summary>
  325. /// Gets the index by year.
  326. /// </summary>
  327. /// <param name="user">The user.</param>
  328. /// <returns>IEnumerable{BaseItem}.</returns>
  329. protected IEnumerable<BaseItem> GetIndexByYear(User user)
  330. {
  331. // Even though this implementation means multiple iterations over the target list - it allows us to defer
  332. // the retrieval of the individual children for each index value until they are requested.
  333. using (new Profiler("Production Year Index Build for " + Name, Logger))
  334. {
  335. var indexName = LocalizedStrings.Instance.GetString("YearDispPref");
  336. //we need a copy of this so we don't double-recurse
  337. var candidates = GetRecursiveChildren(user).Where(i => i.IncludeInIndex && i.ProductionYear.HasValue).ToList();
  338. return candidates.AsParallel().Select(i => i.ProductionYear.Value)
  339. .Distinct()
  340. .Select(i =>
  341. {
  342. try
  343. {
  344. return LibraryManager.GetYear(i);
  345. }
  346. catch (IOException ex)
  347. {
  348. Logger.ErrorException("Error getting year {0}", ex, i);
  349. return null;
  350. }
  351. catch (AggregateException ex)
  352. {
  353. Logger.ErrorException("Error getting year {0}", ex, i);
  354. return null;
  355. }
  356. })
  357. .Where(i => i != null)
  358. .Select(ndx => new IndexFolder(this, ndx, candidates.Where(i => i.ProductionYear == int.Parse(ndx.Name)), indexName));
  359. }
  360. }
  361. /// <summary>
  362. /// Returns the indexed children for this user from the cache. Caches them if not already there.
  363. /// </summary>
  364. /// <param name="user">The user.</param>
  365. /// <param name="indexBy">The index by.</param>
  366. /// <returns>IEnumerable{BaseItem}.</returns>
  367. private IEnumerable<BaseItem> GetIndexedChildren(User user, string indexBy)
  368. {
  369. List<BaseItem> result = null;
  370. var cacheKey = user.Name + indexBy;
  371. if (IndexCache != null)
  372. {
  373. IndexCache.TryGetValue(cacheKey, out result);
  374. }
  375. if (result == null)
  376. {
  377. //not cached - cache it
  378. Func<User, IEnumerable<BaseItem>> indexing;
  379. IndexByOptions.TryGetValue(indexBy, out indexing);
  380. result = BuildIndex(indexBy, indexing, user);
  381. }
  382. return result;
  383. }
  384. /// <summary>
  385. /// Get the list of indexy by choices for this folder (localized).
  386. /// </summary>
  387. /// <value>The index by option strings.</value>
  388. [IgnoreDataMember]
  389. public IEnumerable<string> IndexByOptionStrings
  390. {
  391. get { return IndexByOptions.Keys; }
  392. }
  393. /// <summary>
  394. /// The index cache
  395. /// </summary>
  396. protected ConcurrentDictionary<string, List<BaseItem>> IndexCache;
  397. /// <summary>
  398. /// Builds the index.
  399. /// </summary>
  400. /// <param name="indexKey">The index key.</param>
  401. /// <param name="indexFunction">The index function.</param>
  402. /// <param name="user">The user.</param>
  403. /// <returns>List{BaseItem}.</returns>
  404. protected virtual List<BaseItem> BuildIndex(string indexKey, Func<User, IEnumerable<BaseItem>> indexFunction, User user)
  405. {
  406. if (IndexCache == null)
  407. {
  408. IndexCache = new ConcurrentDictionary<string, List<BaseItem>>();
  409. }
  410. return indexFunction != null
  411. ? IndexCache[user.Name + indexKey] = indexFunction(user).ToList()
  412. : null;
  413. }
  414. #endregion
  415. /// <summary>
  416. /// The children
  417. /// </summary>
  418. private ConcurrentBag<BaseItem> _children;
  419. /// <summary>
  420. /// The _children initialized
  421. /// </summary>
  422. private bool _childrenInitialized;
  423. /// <summary>
  424. /// The _children sync lock
  425. /// </summary>
  426. protected object ChildrenSyncLock = new object();
  427. /// <summary>
  428. /// Gets or sets the actual children.
  429. /// </summary>
  430. /// <value>The actual children.</value>
  431. protected virtual IEnumerable<BaseItem> ActualChildren
  432. {
  433. get
  434. {
  435. LazyInitializer.EnsureInitialized(ref _children, ref _childrenInitialized, ref ChildrenSyncLock, LoadChildrenInternal);
  436. return _children;
  437. }
  438. }
  439. /// <summary>
  440. /// thread-safe access to the actual children of this folder - without regard to user
  441. /// </summary>
  442. /// <value>The children.</value>
  443. [IgnoreDataMember]
  444. public IEnumerable<BaseItem> Children
  445. {
  446. get { return ActualChildren; }
  447. }
  448. /// <summary>
  449. /// thread-safe access to all recursive children of this folder - without regard to user
  450. /// </summary>
  451. /// <value>The recursive children.</value>
  452. [IgnoreDataMember]
  453. public IEnumerable<BaseItem> RecursiveChildren
  454. {
  455. get
  456. {
  457. foreach (var item in Children)
  458. {
  459. yield return item;
  460. if (item.IsFolder)
  461. {
  462. var subFolder = (Folder)item;
  463. foreach (var subitem in subFolder.RecursiveChildren)
  464. {
  465. yield return subitem;
  466. }
  467. }
  468. }
  469. }
  470. }
  471. private ConcurrentBag<BaseItem> LoadChildrenInternal()
  472. {
  473. return new ConcurrentBag<BaseItem>(LoadChildren());
  474. }
  475. /// <summary>
  476. /// Loads our children. Validation will occur externally.
  477. /// We want this sychronous.
  478. /// </summary>
  479. /// <returns>ConcurrentBag{BaseItem}.</returns>
  480. protected virtual IEnumerable<BaseItem> LoadChildren()
  481. {
  482. //just load our children from the repo - the library will be validated and maintained in other processes
  483. return GetCachedChildren();
  484. }
  485. /// <summary>
  486. /// Gets or sets the current validation cancellation token source.
  487. /// </summary>
  488. /// <value>The current validation cancellation token source.</value>
  489. private CancellationTokenSource CurrentValidationCancellationTokenSource { get; set; }
  490. /// <summary>
  491. /// Validates that the children of the folder still exist
  492. /// </summary>
  493. /// <param name="progress">The progress.</param>
  494. /// <param name="cancellationToken">The cancellation token.</param>
  495. /// <param name="recursive">if set to <c>true</c> [recursive].</param>
  496. /// <param name="forceRefreshMetadata">if set to <c>true</c> [force refresh metadata].</param>
  497. /// <returns>Task.</returns>
  498. public async Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false)
  499. {
  500. cancellationToken.ThrowIfCancellationRequested();
  501. // Cancel the current validation, if any
  502. if (CurrentValidationCancellationTokenSource != null)
  503. {
  504. CurrentValidationCancellationTokenSource.Cancel();
  505. }
  506. // Create an inner cancellation token. This can cancel all validations from this level on down,
  507. // but nothing above this
  508. var innerCancellationTokenSource = new CancellationTokenSource();
  509. try
  510. {
  511. CurrentValidationCancellationTokenSource = innerCancellationTokenSource;
  512. var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken);
  513. await ValidateChildrenInternal(progress, linkedCancellationTokenSource.Token, recursive, forceRefreshMetadata).ConfigureAwait(false);
  514. }
  515. catch (OperationCanceledException ex)
  516. {
  517. Logger.Info("ValidateChildren cancelled for " + Name);
  518. // If the outer cancelletion token in the cause for the cancellation, throw it
  519. if (cancellationToken.IsCancellationRequested && ex.CancellationToken == cancellationToken)
  520. {
  521. throw;
  522. }
  523. }
  524. finally
  525. {
  526. // Null out the token source
  527. if (CurrentValidationCancellationTokenSource == innerCancellationTokenSource)
  528. {
  529. CurrentValidationCancellationTokenSource = null;
  530. }
  531. innerCancellationTokenSource.Dispose();
  532. }
  533. }
  534. /// <summary>
  535. /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
  536. /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
  537. /// </summary>
  538. /// <param name="progress">The progress.</param>
  539. /// <param name="cancellationToken">The cancellation token.</param>
  540. /// <param name="recursive">if set to <c>true</c> [recursive].</param>
  541. /// <param name="forceRefreshMetadata">if set to <c>true</c> [force refresh metadata].</param>
  542. /// <returns>Task.</returns>
  543. protected async virtual Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool? recursive = null, bool forceRefreshMetadata = false)
  544. {
  545. var locationType = LocationType;
  546. // Nothing to do here
  547. if (locationType == LocationType.Remote || locationType == LocationType.Virtual)
  548. {
  549. return;
  550. }
  551. cancellationToken.ThrowIfCancellationRequested();
  552. IEnumerable<BaseItem> nonCachedChildren;
  553. try
  554. {
  555. nonCachedChildren = GetNonCachedChildren();
  556. }
  557. catch (IOException ex)
  558. {
  559. nonCachedChildren = new BaseItem[] { };
  560. Logger.ErrorException("Error getting file system entries for {0}", ex, Path);
  561. }
  562. if (nonCachedChildren == null) return; //nothing to validate
  563. progress.Report(5);
  564. //build a dictionary of the current children we have now by Id so we can compare quickly and easily
  565. var currentChildren = ActualChildren.ToDictionary(i => i.Id);
  566. //create a list for our validated children
  567. var validChildren = new ConcurrentBag<Tuple<BaseItem, bool>>();
  568. var newItems = new ConcurrentBag<BaseItem>();
  569. cancellationToken.ThrowIfCancellationRequested();
  570. var options = new ParallelOptions
  571. {
  572. MaxDegreeOfParallelism = 20
  573. };
  574. Parallel.ForEach(nonCachedChildren, options, child =>
  575. {
  576. BaseItem currentChild;
  577. if (currentChildren.TryGetValue(child.Id, out currentChild))
  578. {
  579. currentChild.ResolveArgs = child.ResolveArgs;
  580. //existing item - check if it has changed
  581. if (currentChild.HasChanged(child))
  582. {
  583. EntityResolutionHelper.EnsureDates(currentChild, child.ResolveArgs, false);
  584. validChildren.Add(new Tuple<BaseItem, bool>(currentChild, true));
  585. }
  586. else
  587. {
  588. validChildren.Add(new Tuple<BaseItem, bool>(currentChild, false));
  589. }
  590. currentChild.IsOffline = false;
  591. }
  592. else
  593. {
  594. //brand new item - needs to be added
  595. newItems.Add(child);
  596. validChildren.Add(new Tuple<BaseItem, bool>(child, true));
  597. }
  598. });
  599. // If any items were added or removed....
  600. if (!newItems.IsEmpty || currentChildren.Count != validChildren.Count)
  601. {
  602. var newChildren = validChildren.Select(c => c.Item1).ToList();
  603. //that's all the new and changed ones - now see if there are any that are missing
  604. var itemsRemoved = currentChildren.Values.Except(newChildren).ToList();
  605. var actualRemovals = new List<BaseItem>();
  606. foreach (var item in itemsRemoved)
  607. {
  608. if (IsRootPathAvailable(item.Path))
  609. {
  610. item.IsOffline = false;
  611. actualRemovals.Add(item);
  612. }
  613. else
  614. {
  615. item.IsOffline = true;
  616. validChildren.Add(new Tuple<BaseItem, bool>(item, false));
  617. }
  618. }
  619. if (actualRemovals.Count > 0)
  620. {
  621. lock (ChildrenSyncLock)
  622. {
  623. _children = new ConcurrentBag<BaseItem>(_children.Except(actualRemovals));
  624. }
  625. }
  626. await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false);
  627. foreach (var item in newItems)
  628. {
  629. _children.Add(item);
  630. Logger.Debug("** " + item.Name + " Added to library.");
  631. }
  632. await ItemRepository.SaveChildren(Id, _children.ToList().Select(i => i.Id), cancellationToken).ConfigureAwait(false);
  633. //force the indexes to rebuild next time
  634. if (IndexCache != null)
  635. {
  636. IndexCache.Clear();
  637. }
  638. }
  639. progress.Report(10);
  640. cancellationToken.ThrowIfCancellationRequested();
  641. await RefreshChildren(validChildren, progress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false);
  642. progress.Report(100);
  643. }
  644. /// <summary>
  645. /// Refreshes the children.
  646. /// </summary>
  647. /// <param name="children">The children.</param>
  648. /// <param name="progress">The progress.</param>
  649. /// <param name="cancellationToken">The cancellation token.</param>
  650. /// <param name="recursive">if set to <c>true</c> [recursive].</param>
  651. /// <param name="forceRefreshMetadata">if set to <c>true</c> [force refresh metadata].</param>
  652. /// <returns>Task.</returns>
  653. private async Task RefreshChildren(IEnumerable<Tuple<BaseItem, bool>> children, IProgress<double> progress, CancellationToken cancellationToken, bool? recursive, bool forceRefreshMetadata = false)
  654. {
  655. var list = children.ToList();
  656. var percentages = new Dictionary<Guid, double>(list.Count);
  657. var tasks = new List<Task>();
  658. foreach (var tuple in list)
  659. {
  660. if (tasks.Count > 8)
  661. {
  662. await Task.WhenAll(tasks).ConfigureAwait(false);
  663. }
  664. Tuple<BaseItem, bool> currentTuple = tuple;
  665. tasks.Add(Task.Run(async () =>
  666. {
  667. cancellationToken.ThrowIfCancellationRequested();
  668. var child = currentTuple.Item1;
  669. //refresh it
  670. await child.RefreshMetadata(cancellationToken, forceSave: currentTuple.Item2, forceRefresh: forceRefreshMetadata, resetResolveArgs: false).ConfigureAwait(false);
  671. // Refresh children if a folder and the item changed or recursive is set to true
  672. var refreshChildren = child.IsFolder && (currentTuple.Item2 || (recursive.HasValue && recursive.Value));
  673. if (refreshChildren)
  674. {
  675. // Don't refresh children if explicitly set to false
  676. if (recursive.HasValue && recursive.Value == false)
  677. {
  678. refreshChildren = false;
  679. }
  680. }
  681. if (refreshChildren)
  682. {
  683. cancellationToken.ThrowIfCancellationRequested();
  684. var innerProgress = new ActionableProgress<double>();
  685. innerProgress.RegisterAction(p =>
  686. {
  687. lock (percentages)
  688. {
  689. percentages[child.Id] = p / 100;
  690. var percent = percentages.Values.Sum();
  691. percent /= list.Count;
  692. progress.Report((90 * percent) + 10);
  693. }
  694. });
  695. await ((Folder)child).ValidateChildren(innerProgress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false);
  696. // Some folder providers are unable to refresh until children have been refreshed.
  697. await child.RefreshMetadata(cancellationToken, resetResolveArgs: false).ConfigureAwait(false);
  698. }
  699. else
  700. {
  701. lock (percentages)
  702. {
  703. percentages[child.Id] = 1;
  704. var percent = percentages.Values.Sum();
  705. percent /= list.Count;
  706. progress.Report((90 * percent) + 10);
  707. }
  708. }
  709. }));
  710. }
  711. cancellationToken.ThrowIfCancellationRequested();
  712. await Task.WhenAll(tasks).ConfigureAwait(false);
  713. }
  714. /// <summary>
  715. /// Determines if a path's root is available or not
  716. /// </summary>
  717. /// <param name="path"></param>
  718. /// <returns></returns>
  719. private bool IsRootPathAvailable(string path)
  720. {
  721. if (File.Exists(path))
  722. {
  723. return true;
  724. }
  725. // Depending on whether the path is local or unc, it may return either null or '\' at the top
  726. while (!string.IsNullOrEmpty(path) && path.Length > 1)
  727. {
  728. if (Directory.Exists(path))
  729. {
  730. return true;
  731. }
  732. path = System.IO.Path.GetDirectoryName(path);
  733. }
  734. return false;
  735. }
  736. /// <summary>
  737. /// Get the children of this folder from the actual file system
  738. /// </summary>
  739. /// <returns>IEnumerable{BaseItem}.</returns>
  740. protected virtual IEnumerable<BaseItem> GetNonCachedChildren()
  741. {
  742. if (ResolveArgs == null || ResolveArgs.FileSystemDictionary == null)
  743. {
  744. Logger.Error("Null for {0}", Path);
  745. }
  746. return LibraryManager.ResolvePaths<BaseItem>(ResolveArgs.FileSystemChildren, this);
  747. }
  748. /// <summary>
  749. /// Get our children from the repo - stubbed for now
  750. /// </summary>
  751. /// <returns>IEnumerable{BaseItem}.</returns>
  752. protected IEnumerable<BaseItem> GetCachedChildren()
  753. {
  754. return ItemRepository.GetChildren(Id).Select(RetrieveChild).Where(i => i != null);
  755. }
  756. /// <summary>
  757. /// Retrieves the child.
  758. /// </summary>
  759. /// <param name="child">The child.</param>
  760. /// <returns>BaseItem.</returns>
  761. private BaseItem RetrieveChild(Guid child)
  762. {
  763. var item = LibraryManager.RetrieveItem(child);
  764. if (item != null)
  765. {
  766. if (item is IByReferenceItem)
  767. {
  768. return LibraryManager.GetOrAddByReferenceItem(item);
  769. }
  770. item.Parent = this;
  771. }
  772. return item;
  773. }
  774. /// <summary>
  775. /// Gets allowed children of an item
  776. /// </summary>
  777. /// <param name="user">The user.</param>
  778. /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
  779. /// <param name="indexBy">The index by.</param>
  780. /// <returns>IEnumerable{BaseItem}.</returns>
  781. /// <exception cref="System.ArgumentNullException"></exception>
  782. public virtual IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren, string indexBy = null)
  783. {
  784. if (user == null)
  785. {
  786. throw new ArgumentNullException();
  787. }
  788. //the true root should return our users root folder children
  789. if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, includeLinkedChildren, indexBy);
  790. IEnumerable<BaseItem> result = null;
  791. if (!string.IsNullOrEmpty(indexBy))
  792. {
  793. result = GetIndexedChildren(user, indexBy);
  794. }
  795. if (result != null)
  796. {
  797. return result;
  798. }
  799. var children = Children;
  800. if (includeLinkedChildren)
  801. {
  802. children = children.Concat(GetLinkedChildren());
  803. }
  804. // If indexed is false or the indexing function is null
  805. return children.AsParallel().Where(c => c.IsVisible(user)).AsEnumerable();
  806. }
  807. /// <summary>
  808. /// Gets allowed recursive children of an item
  809. /// </summary>
  810. /// <param name="user">The user.</param>
  811. /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
  812. /// <returns>IEnumerable{BaseItem}.</returns>
  813. /// <exception cref="System.ArgumentNullException"></exception>
  814. public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
  815. {
  816. if (user == null)
  817. {
  818. throw new ArgumentNullException();
  819. }
  820. var children = GetRecursiveChildrenInternal(user, includeLinkedChildren);
  821. if (includeLinkedChildren)
  822. {
  823. children = children.Distinct();
  824. }
  825. return children;
  826. }
  827. /// <summary>
  828. /// Gets allowed recursive children of an item
  829. /// </summary>
  830. /// <param name="user">The user.</param>
  831. /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
  832. /// <returns>IEnumerable{BaseItem}.</returns>
  833. /// <exception cref="System.ArgumentNullException"></exception>
  834. private IEnumerable<BaseItem> GetRecursiveChildrenInternal(User user, bool includeLinkedChildren)
  835. {
  836. if (user == null)
  837. {
  838. throw new ArgumentNullException();
  839. }
  840. foreach (var item in GetChildren(user, includeLinkedChildren))
  841. {
  842. yield return item;
  843. var subFolder = item as Folder;
  844. if (subFolder != null)
  845. {
  846. foreach (var subitem in subFolder.GetRecursiveChildrenInternal(user, includeLinkedChildren))
  847. {
  848. yield return subitem;
  849. }
  850. }
  851. }
  852. }
  853. /// <summary>
  854. /// Gets the linked children.
  855. /// </summary>
  856. /// <returns>IEnumerable{BaseItem}.</returns>
  857. public IEnumerable<BaseItem> GetLinkedChildren()
  858. {
  859. return LinkedChildren
  860. .Select(GetLinkedChild)
  861. .Where(i => i != null);
  862. }
  863. /// <summary>
  864. /// Gets the linked child.
  865. /// </summary>
  866. /// <param name="info">The info.</param>
  867. /// <returns>BaseItem.</returns>
  868. private BaseItem GetLinkedChild(LinkedChild info)
  869. {
  870. if (string.IsNullOrEmpty(info.Path))
  871. {
  872. throw new ArgumentException("Encountered linked child with empty path.");
  873. }
  874. var item = LibraryManager.RootFolder.FindByPath(info.Path);
  875. if (item == null)
  876. {
  877. Logger.Warn("Unable to find linked item at {0}", info.Path);
  878. }
  879. return item;
  880. }
  881. public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
  882. {
  883. var changed = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
  884. return changed || (SupportsShortcutChildren && LocationType == LocationType.FileSystem && RefreshLinkedChildren());
  885. }
  886. /// <summary>
  887. /// Refreshes the linked children.
  888. /// </summary>
  889. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  890. private bool RefreshLinkedChildren()
  891. {
  892. ItemResolveArgs resolveArgs;
  893. try
  894. {
  895. resolveArgs = ResolveArgs;
  896. if (!resolveArgs.IsDirectory)
  897. {
  898. return false;
  899. }
  900. }
  901. catch (IOException ex)
  902. {
  903. Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path);
  904. return false;
  905. }
  906. var currentManualLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Manual).ToList();
  907. var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
  908. var newShortcutLinks = resolveArgs.FileSystemChildren
  909. .Where(i => (i.Attributes & FileAttributes.Directory) != FileAttributes.Directory && FileSystem.IsShortcut(i.FullName))
  910. .Select(i =>
  911. {
  912. try
  913. {
  914. Logger.Debug("Found shortcut at {0}", i.FullName);
  915. var resolvedPath = FileSystem.ResolveShortcut(i.FullName);
  916. if (!string.IsNullOrEmpty(resolvedPath))
  917. {
  918. return new LinkedChild
  919. {
  920. Path = resolvedPath,
  921. Type = LinkedChildType.Shortcut
  922. };
  923. }
  924. Logger.Error("Error resolving shortcut {0}", i.FullName);
  925. return null;
  926. }
  927. catch (IOException ex)
  928. {
  929. Logger.ErrorException("Error resolving shortcut {0}", ex, i.FullName);
  930. return null;
  931. }
  932. })
  933. .Where(i => i != null)
  934. .ToList();
  935. if (!newShortcutLinks.SequenceEqual(currentShortcutLinks))
  936. {
  937. Logger.Info("Shortcut links have changed for {0}", Path);
  938. newShortcutLinks.AddRange(currentManualLinks);
  939. LinkedChildren = newShortcutLinks;
  940. return true;
  941. }
  942. return false;
  943. }
  944. /// <summary>
  945. /// Folders need to validate and refresh
  946. /// </summary>
  947. /// <returns>Task.</returns>
  948. public override async Task ChangedExternally()
  949. {
  950. await base.ChangedExternally().ConfigureAwait(false);
  951. var progress = new Progress<double>();
  952. await ValidateChildren(progress, CancellationToken.None).ConfigureAwait(false);
  953. }
  954. /// <summary>
  955. /// Marks the item as either played or unplayed
  956. /// </summary>
  957. /// <param name="user">The user.</param>
  958. /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
  959. /// <param name="userManager">The user manager.</param>
  960. /// <returns>Task.</returns>
  961. public override async Task SetPlayedStatus(User user, bool wasPlayed, IUserDataRepository userManager)
  962. {
  963. // Sweep through recursively and update status
  964. var tasks = GetRecursiveChildren(user, true).Where(i => !i.IsFolder).Select(c => c.SetPlayedStatus(user, wasPlayed, userManager));
  965. await Task.WhenAll(tasks).ConfigureAwait(false);
  966. }
  967. /// <summary>
  968. /// Finds an item by path, recursively
  969. /// </summary>
  970. /// <param name="path">The path.</param>
  971. /// <returns>BaseItem.</returns>
  972. /// <exception cref="System.ArgumentNullException"></exception>
  973. public BaseItem FindByPath(string path)
  974. {
  975. if (string.IsNullOrEmpty(path))
  976. {
  977. throw new ArgumentNullException();
  978. }
  979. try
  980. {
  981. if (ResolveArgs.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
  982. {
  983. return this;
  984. }
  985. }
  986. catch (IOException ex)
  987. {
  988. Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path);
  989. }
  990. //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
  991. return RecursiveChildren.FirstOrDefault(i =>
  992. {
  993. try
  994. {
  995. return i.ResolveArgs.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase);
  996. }
  997. catch (IOException ex)
  998. {
  999. Logger.ErrorException("Error getting ResolveArgs for {0}", ex, Path);
  1000. return false;
  1001. }
  1002. });
  1003. }
  1004. }
  1005. }