Folder.cs 44 KB

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