Folder.cs 52 KB

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