Folder.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. using MediaBrowser.Model.Entities;
  2. using MediaBrowser.Controller.IO;
  3. using MediaBrowser.Controller.Library;
  4. using MediaBrowser.Common.Logging;
  5. using MediaBrowser.Controller.Resolvers;
  6. using System;
  7. using System.Threading.Tasks;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. namespace MediaBrowser.Controller.Entities
  11. {
  12. public class Folder : BaseItem
  13. {
  14. #region Events
  15. /// <summary>
  16. /// Fires whenever a validation routine updates our children. The added and removed children are properties of the args.
  17. /// *** Will fire asynchronously. ***
  18. /// </summary>
  19. public event EventHandler<ChildrenChangedEventArgs> ChildrenChanged;
  20. protected void OnChildrenChanged(ChildrenChangedEventArgs args)
  21. {
  22. if (ChildrenChanged != null)
  23. {
  24. Task.Run( () =>
  25. {
  26. ChildrenChanged(this, args);
  27. Kernel.Instance.OnLibraryChanged(args);
  28. });
  29. }
  30. }
  31. #endregion
  32. public override bool IsFolder
  33. {
  34. get
  35. {
  36. return true;
  37. }
  38. }
  39. public bool IsRoot { get; set; }
  40. public bool IsVirtualFolder
  41. {
  42. get
  43. {
  44. return Parent != null && Parent.IsRoot;
  45. }
  46. }
  47. protected object childLock = new object();
  48. protected List<BaseItem> children;
  49. protected virtual List<BaseItem> ActualChildren
  50. {
  51. get
  52. {
  53. if (children == null)
  54. {
  55. LoadChildren();
  56. }
  57. return children;
  58. }
  59. set
  60. {
  61. children = value;
  62. }
  63. }
  64. /// <summary>
  65. /// thread-safe access to the actual children of this folder - without regard to user
  66. /// </summary>
  67. public IEnumerable<BaseItem> Children
  68. {
  69. get
  70. {
  71. lock (childLock)
  72. return ActualChildren.ToList();
  73. }
  74. }
  75. /// <summary>
  76. /// thread-safe access to all recursive children of this folder - without regard to user
  77. /// </summary>
  78. public IEnumerable<BaseItem> RecursiveChildren
  79. {
  80. get
  81. {
  82. foreach (var item in Children)
  83. {
  84. yield return item;
  85. var subFolder = item as Folder;
  86. if (subFolder != null)
  87. {
  88. foreach (var subitem in subFolder.RecursiveChildren)
  89. {
  90. yield return subitem;
  91. }
  92. }
  93. }
  94. }
  95. }
  96. /// <summary>
  97. /// Loads and validates our children
  98. /// </summary>
  99. protected virtual void LoadChildren()
  100. {
  101. //first - load our children from the repo
  102. lock (childLock)
  103. children = GetCachedChildren();
  104. //then kick off a validation against the actual file system
  105. Task.Run(() => ValidateChildren());
  106. }
  107. protected bool ChildrenValidating = false;
  108. /// <summary>
  109. /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
  110. /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
  111. /// </summary>
  112. /// <returns></returns>
  113. protected async virtual void ValidateChildren()
  114. {
  115. if (ChildrenValidating) return; //only ever want one of these going at once and don't want them to fire off in sequence so don't use lock
  116. ChildrenValidating = true;
  117. bool changed = false; //this will save us a little time at the end if nothing changes
  118. var changedArgs = new ChildrenChangedEventArgs(this);
  119. //get the current valid children from filesystem (or wherever)
  120. var nonCachedChildren = await GetNonCachedChildren();
  121. if (nonCachedChildren == null) return; //nothing to validate
  122. //build a dictionary of the current children we have now by Id so we can compare quickly and easily
  123. Dictionary<Guid, BaseItem> currentChildren;
  124. lock (childLock)
  125. currentChildren = ActualChildren.ToDictionary(i => i.Id);
  126. //create a list for our validated children
  127. var validChildren = new List<BaseItem>();
  128. //now traverse the valid children and find any changed or new items
  129. foreach (var child in nonCachedChildren)
  130. {
  131. BaseItem currentChild;
  132. currentChildren.TryGetValue(child.Id, out currentChild);
  133. if (currentChild == null)
  134. {
  135. //brand new item - needs to be added
  136. changed = true;
  137. changedArgs.ItemsAdded.Add(child);
  138. //refresh it
  139. child.RefreshMetadata();
  140. Logger.LogInfo("New Item Added to Library: ("+child.GetType().Name+") "+ child.Name + " (" + child.Path + ")");
  141. //save it in repo...
  142. //and add it to our valid children
  143. validChildren.Add(child);
  144. //fire an added event...?
  145. //if it is a folder we need to validate its children as well
  146. Folder folder = child as Folder;
  147. if (folder != null)
  148. {
  149. folder.ValidateChildren();
  150. //probably need to refresh too...
  151. }
  152. }
  153. else
  154. {
  155. //existing item - check if it has changed
  156. if (currentChild.IsChanged(child))
  157. {
  158. changed = true;
  159. //update resolve args and refresh meta
  160. // Note - we are refreshing the existing child instead of the newly found one so the "Except" operation below
  161. // will identify this item as the same one
  162. currentChild.ResolveArgs = child.ResolveArgs;
  163. currentChild.RefreshMetadata();
  164. Logger.LogInfo("Item Changed: ("+currentChild.GetType().Name+") "+ currentChild.Name + " (" + currentChild.Path + ")");
  165. //save it in repo...
  166. validChildren.Add(currentChild);
  167. }
  168. else
  169. {
  170. //current child that didn't change - just put it in the valid children
  171. validChildren.Add(currentChild);
  172. }
  173. }
  174. }
  175. //that's all the new and changed ones - now see if there are any that are missing
  176. changedArgs.ItemsRemoved = currentChildren.Values.Except(validChildren);
  177. changed |= changedArgs.ItemsRemoved != null;
  178. //now, if anything changed - replace our children
  179. if (changed)
  180. {
  181. if (changedArgs.ItemsRemoved != null) foreach (var item in changedArgs.ItemsRemoved) Logger.LogDebugInfo("** " + item.Name + " Removed from library.");
  182. lock (childLock)
  183. ActualChildren = validChildren;
  184. //and save children in repo...
  185. //and fire event
  186. this.OnChildrenChanged(changedArgs);
  187. }
  188. ChildrenValidating = false;
  189. }
  190. /// <summary>
  191. /// Get the children of this folder from the actual file system
  192. /// </summary>
  193. /// <returns></returns>
  194. protected async virtual Task<IEnumerable<BaseItem>> GetNonCachedChildren()
  195. {
  196. ItemResolveEventArgs args = new ItemResolveEventArgs()
  197. {
  198. FileInfo = FileData.GetFileData(this.Path),
  199. Parent = this.Parent,
  200. Cancel = false,
  201. Path = this.Path
  202. };
  203. // Gather child folder and files
  204. if (args.IsDirectory)
  205. {
  206. args.FileSystemChildren = FileData.GetFileSystemEntries(this.Path, "*").ToArray();
  207. bool isVirtualFolder = Parent != null && Parent.IsRoot;
  208. args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
  209. }
  210. else
  211. {
  212. Logger.LogError("Folder has a path that is not a directory: " + this.Path);
  213. return null;
  214. }
  215. if (!EntityResolutionHelper.ShouldResolvePathContents(args))
  216. {
  217. return null;
  218. }
  219. return (await Task.WhenAll<BaseItem>(GetChildren(args.FileSystemChildren)).ConfigureAwait(false))
  220. .Where(i => i != null).OrderBy(f =>
  221. {
  222. return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
  223. });
  224. }
  225. /// <summary>
  226. /// Resolves a path into a BaseItem
  227. /// </summary>
  228. protected async Task<BaseItem> GetChild(string path, WIN32_FIND_DATA? fileInfo = null)
  229. {
  230. ItemResolveEventArgs args = new ItemResolveEventArgs()
  231. {
  232. FileInfo = fileInfo ?? FileData.GetFileData(path),
  233. Parent = this,
  234. Cancel = false,
  235. Path = path
  236. };
  237. args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
  238. args = FileSystemHelper.FilterChildFileSystemEntries(args, false);
  239. return Kernel.Instance.ResolveItem(args);
  240. }
  241. /// <summary>
  242. /// Finds child BaseItems for us
  243. /// </summary>
  244. protected Task<BaseItem>[] GetChildren(WIN32_FIND_DATA[] fileSystemChildren)
  245. {
  246. Task<BaseItem>[] tasks = new Task<BaseItem>[fileSystemChildren.Length];
  247. for (int i = 0; i < fileSystemChildren.Length; i++)
  248. {
  249. var child = fileSystemChildren[i];
  250. tasks[i] = GetChild(child.Path, child);
  251. }
  252. return tasks;
  253. }
  254. /// <summary>
  255. /// Get our children from the repo - stubbed for now
  256. /// </summary>
  257. /// <returns></returns>
  258. protected virtual List<BaseItem> GetCachedChildren()
  259. {
  260. return new List<BaseItem>();
  261. }
  262. /// <summary>
  263. /// Gets allowed children of an item
  264. /// </summary>
  265. public IEnumerable<BaseItem> GetChildren(User user)
  266. {
  267. lock(childLock)
  268. return ActualChildren.Where(c => c.IsParentalAllowed(user));
  269. }
  270. /// <summary>
  271. /// Gets allowed recursive children of an item
  272. /// </summary>
  273. public IEnumerable<BaseItem> GetRecursiveChildren(User user)
  274. {
  275. foreach (var item in GetChildren(user))
  276. {
  277. yield return item;
  278. var subFolder = item as Folder;
  279. if (subFolder != null)
  280. {
  281. foreach (var subitem in subFolder.GetRecursiveChildren(user))
  282. {
  283. yield return subitem;
  284. }
  285. }
  286. }
  287. }
  288. /// <summary>
  289. /// Folders need to validate and refresh
  290. /// </summary>
  291. /// <returns></returns>
  292. public override Task ChangedExternally()
  293. {
  294. return Task.Run(() =>
  295. {
  296. if (this.IsRoot)
  297. {
  298. Kernel.Instance.ReloadRoot().ConfigureAwait(false);
  299. }
  300. else
  301. {
  302. RefreshMetadata();
  303. ValidateChildren();
  304. }
  305. });
  306. }
  307. /// <summary>
  308. /// Since it can be slow to make all of these calculations at once, this method will provide a way to get them all back together
  309. /// </summary>
  310. public ItemSpecialCounts GetSpecialCounts(User user)
  311. {
  312. var counts = new ItemSpecialCounts();
  313. IEnumerable<BaseItem> recursiveChildren = GetRecursiveChildren(user);
  314. var recentlyAddedItems = GetRecentlyAddedItems(recursiveChildren, user);
  315. counts.RecentlyAddedItemCount = recentlyAddedItems.Count;
  316. counts.RecentlyAddedUnPlayedItemCount = GetRecentlyAddedUnplayedItems(recentlyAddedItems, user).Count;
  317. counts.InProgressItemCount = GetInProgressItems(recursiveChildren, user).Count;
  318. counts.PlayedPercentage = GetPlayedPercentage(recursiveChildren, user);
  319. return counts;
  320. }
  321. /// <summary>
  322. /// Finds all recursive items within a top-level parent that contain the given genre and are allowed for the current user
  323. /// </summary>
  324. public IEnumerable<BaseItem> GetItemsWithGenre(string genre, User user)
  325. {
  326. return GetRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
  327. }
  328. /// <summary>
  329. /// Finds all recursive items within a top-level parent that contain the given year and are allowed for the current user
  330. /// </summary>
  331. public IEnumerable<BaseItem> GetItemsWithYear(int year, User user)
  332. {
  333. return GetRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
  334. }
  335. /// <summary>
  336. /// Finds all recursive items within a top-level parent that contain the given studio and are allowed for the current user
  337. /// </summary>
  338. public IEnumerable<BaseItem> GetItemsWithStudio(string studio, User user)
  339. {
  340. return GetRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
  341. }
  342. /// <summary>
  343. /// Finds all recursive items within a top-level parent that the user has marked as a favorite
  344. /// </summary>
  345. public IEnumerable<BaseItem> GetFavoriteItems(User user)
  346. {
  347. return GetRecursiveChildren(user).Where(c =>
  348. {
  349. UserItemData data = c.GetUserData(user, false);
  350. if (data != null)
  351. {
  352. return data.IsFavorite;
  353. }
  354. return false;
  355. });
  356. }
  357. /// <summary>
  358. /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
  359. /// </summary>
  360. public IEnumerable<BaseItem> GetItemsWithPerson(string person, User user)
  361. {
  362. return GetRecursiveChildren(user).Where(c =>
  363. {
  364. if (c.People != null)
  365. {
  366. return c.People.ContainsKey(person);
  367. }
  368. return false;
  369. });
  370. }
  371. /// <summary>
  372. /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
  373. /// </summary>
  374. /// <param name="personType">Specify this to limit results to a specific PersonType</param>
  375. public IEnumerable<BaseItem> GetItemsWithPerson(string person, string personType, User user)
  376. {
  377. return GetRecursiveChildren(user).Where(c =>
  378. {
  379. if (c.People != null)
  380. {
  381. return c.People.ContainsKey(person) && c.People[person].Type.Equals(personType, StringComparison.OrdinalIgnoreCase);
  382. }
  383. return false;
  384. });
  385. }
  386. /// <summary>
  387. /// Gets all recently added items (recursive) within a folder, based on configuration and parental settings
  388. /// </summary>
  389. public List<BaseItem> GetRecentlyAddedItems(User user)
  390. {
  391. return GetRecentlyAddedItems(GetRecursiveChildren(user), user);
  392. }
  393. /// <summary>
  394. /// Gets all recently added unplayed items (recursive) within a folder, based on configuration and parental settings
  395. /// </summary>
  396. public List<BaseItem> GetRecentlyAddedUnplayedItems(User user)
  397. {
  398. return GetRecentlyAddedUnplayedItems(GetRecursiveChildren(user), user);
  399. }
  400. /// <summary>
  401. /// Gets all in-progress items (recursive) within a folder
  402. /// </summary>
  403. public List<BaseItem> GetInProgressItems(User user)
  404. {
  405. return GetInProgressItems(GetRecursiveChildren(user), user);
  406. }
  407. /// <summary>
  408. /// Takes a list of items and returns the ones that are recently added
  409. /// </summary>
  410. private static List<BaseItem> GetRecentlyAddedItems(IEnumerable<BaseItem> itemSet, User user)
  411. {
  412. var list = new List<BaseItem>();
  413. foreach (var item in itemSet)
  414. {
  415. if (!item.IsFolder && item.IsRecentlyAdded(user))
  416. {
  417. list.Add(item);
  418. }
  419. }
  420. return list;
  421. }
  422. /// <summary>
  423. /// Takes a list of items and returns the ones that are recently added and unplayed
  424. /// </summary>
  425. private static List<BaseItem> GetRecentlyAddedUnplayedItems(IEnumerable<BaseItem> itemSet, User user)
  426. {
  427. var list = new List<BaseItem>();
  428. foreach (var item in itemSet)
  429. {
  430. if (!item.IsFolder && item.IsRecentlyAdded(user))
  431. {
  432. var userdata = item.GetUserData(user, false);
  433. if (userdata == null || userdata.PlayCount == 0)
  434. {
  435. list.Add(item);
  436. }
  437. }
  438. }
  439. return list;
  440. }
  441. /// <summary>
  442. /// Takes a list of items and returns the ones that are in progress
  443. /// </summary>
  444. private static List<BaseItem> GetInProgressItems(IEnumerable<BaseItem> itemSet, User user)
  445. {
  446. var list = new List<BaseItem>();
  447. foreach (var item in itemSet)
  448. {
  449. if (!item.IsFolder)
  450. {
  451. var userdata = item.GetUserData(user, false);
  452. if (userdata != null && userdata.PlaybackPositionTicks > 0)
  453. {
  454. list.Add(item);
  455. }
  456. }
  457. }
  458. return list;
  459. }
  460. /// <summary>
  461. /// Gets the total played percentage for a set of items
  462. /// </summary>
  463. private static decimal GetPlayedPercentage(IEnumerable<BaseItem> itemSet, User user)
  464. {
  465. itemSet = itemSet.Where(i => !(i.IsFolder));
  466. decimal totalPercent = 0;
  467. int count = 0;
  468. foreach (BaseItem item in itemSet)
  469. {
  470. count++;
  471. UserItemData data = item.GetUserData(user, false);
  472. if (data == null)
  473. {
  474. continue;
  475. }
  476. if (data.PlayCount > 0)
  477. {
  478. totalPercent += 100;
  479. }
  480. else if (data.PlaybackPositionTicks > 0 && item.RunTimeTicks.HasValue)
  481. {
  482. decimal itemPercent = data.PlaybackPositionTicks;
  483. itemPercent /= item.RunTimeTicks.Value;
  484. totalPercent += itemPercent;
  485. }
  486. }
  487. if (count == 0)
  488. {
  489. return 0;
  490. }
  491. return totalPercent / count;
  492. }
  493. /// <summary>
  494. /// Marks the item as either played or unplayed
  495. /// </summary>
  496. public override void SetPlayedStatus(User user, bool wasPlayed)
  497. {
  498. base.SetPlayedStatus(user, wasPlayed);
  499. // Now sweep through recursively and update status
  500. foreach (BaseItem item in GetChildren(user))
  501. {
  502. item.SetPlayedStatus(user, wasPlayed);
  503. }
  504. }
  505. /// <summary>
  506. /// Finds an item by ID, recursively
  507. /// </summary>
  508. public override BaseItem FindItemById(Guid id)
  509. {
  510. var result = base.FindItemById(id);
  511. if (result != null)
  512. {
  513. return result;
  514. }
  515. //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
  516. return RecursiveChildren.FirstOrDefault(i => i.Id == id);
  517. }
  518. /// <summary>
  519. /// Finds an item by path, recursively
  520. /// </summary>
  521. public BaseItem FindByPath(string path)
  522. {
  523. if (PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
  524. {
  525. return this;
  526. }
  527. //this should be functionally equivilent to what was here since it is IEnum and works on a thread-safe copy
  528. return RecursiveChildren.FirstOrDefault(i => i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase));
  529. }
  530. }
  531. }