Folder.cs 40 KB

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