Folder.cs 44 KB

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