Folder.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  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 IEnumerable<string> PhysicalLocations { get; set; }
  33. public override bool IsFolder
  34. {
  35. get
  36. {
  37. return true;
  38. }
  39. }
  40. public bool IsRoot { get; set; }
  41. public bool IsVirtualFolder
  42. {
  43. get
  44. {
  45. return Parent != null && Parent.IsRoot;
  46. }
  47. }
  48. protected object childLock = new object();
  49. protected List<BaseItem> children;
  50. protected virtual List<BaseItem> ActualChildren
  51. {
  52. get
  53. {
  54. if (children == null)
  55. {
  56. LoadChildren();
  57. }
  58. return children;
  59. }
  60. set
  61. {
  62. children = value;
  63. }
  64. }
  65. /// <summary>
  66. /// thread-safe access to the actual children of this folder - without regard to user
  67. /// </summary>
  68. public IEnumerable<BaseItem> Children
  69. {
  70. get
  71. {
  72. lock (childLock)
  73. return ActualChildren.ToList();
  74. }
  75. }
  76. /// <summary>
  77. /// thread-safe access to all recursive children of this folder - without regard to user
  78. /// </summary>
  79. public IEnumerable<BaseItem> RecursiveChildren
  80. {
  81. get
  82. {
  83. foreach (var item in Children)
  84. {
  85. yield return item;
  86. var subFolder = item as Folder;
  87. if (subFolder != null)
  88. {
  89. foreach (var subitem in subFolder.RecursiveChildren)
  90. {
  91. yield return subitem;
  92. }
  93. }
  94. }
  95. }
  96. }
  97. /// <summary>
  98. /// Loads and validates our children
  99. /// </summary>
  100. protected virtual void LoadChildren()
  101. {
  102. //first - load our children from the repo
  103. lock (childLock)
  104. children = GetCachedChildren();
  105. //then kick off a validation against the actual file system
  106. Task.Run(() => ValidateChildren());
  107. }
  108. protected bool ChildrenValidating = false;
  109. /// <summary>
  110. /// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
  111. /// ***Currently does not contain logic to maintain items that are unavailable in the file system***
  112. /// </summary>
  113. /// <returns></returns>
  114. protected async virtual void ValidateChildren()
  115. {
  116. 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
  117. ChildrenValidating = true;
  118. bool changed = false; //this will save us a little time at the end if nothing changes
  119. var changedArgs = new ChildrenChangedEventArgs(this);
  120. //get the current valid children from filesystem (or wherever)
  121. var nonCachedChildren = await GetNonCachedChildren();
  122. if (nonCachedChildren == null) return; //nothing to validate
  123. //build a dictionary of the current children we have now by Id so we can compare quickly and easily
  124. Dictionary<Guid, BaseItem> currentChildren;
  125. lock (childLock)
  126. currentChildren = ActualChildren.ToDictionary(i => i.Id);
  127. //create a list for our validated children
  128. var validChildren = new List<BaseItem>();
  129. //now traverse the valid children and find any changed or new items
  130. foreach (var child in nonCachedChildren)
  131. {
  132. BaseItem currentChild;
  133. currentChildren.TryGetValue(child.Id, out currentChild);
  134. if (currentChild == null)
  135. {
  136. //brand new item - needs to be added
  137. changed = true;
  138. changedArgs.ItemsAdded.Add(child);
  139. //refresh it
  140. child.RefreshMetadata();
  141. //Logger.LogInfo("New Item Added to Library: ("+child.GetType().Name+") "+ child.Name + " (" + child.Path + ")");
  142. //save it in repo...
  143. //and add it to our valid children
  144. validChildren.Add(child);
  145. //fire an added event...?
  146. //if it is a folder we need to validate its children as well
  147. Folder folder = child as Folder;
  148. if (folder != null)
  149. {
  150. folder.ValidateChildren();
  151. //probably need to refresh too...
  152. }
  153. }
  154. else
  155. {
  156. //existing item - check if it has changed
  157. if (currentChild.IsChanged(child))
  158. {
  159. changed = true;
  160. //update resolve args and refresh meta
  161. // Note - we are refreshing the existing child instead of the newly found one so the "Except" operation below
  162. // will identify this item as the same one
  163. currentChild.ResolveArgs = child.ResolveArgs;
  164. currentChild.RefreshMetadata();
  165. Logger.LogInfo("Item Changed: ("+currentChild.GetType().Name+") "+ currentChild.Name + " (" + currentChild.Path + ")");
  166. //save it in repo...
  167. validChildren.Add(currentChild);
  168. }
  169. else
  170. {
  171. //current child that didn't change - just put it in the valid children
  172. validChildren.Add(currentChild);
  173. }
  174. }
  175. }
  176. //that's all the new and changed ones - now see if there are any that are missing
  177. changedArgs.ItemsRemoved = currentChildren.Values.Except(validChildren);
  178. changed |= changedArgs.ItemsRemoved != null;
  179. //now, if anything changed - replace our children
  180. if (changed)
  181. {
  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 a given Folder
  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. /// 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
  290. /// </summary>
  291. public ItemSpecialCounts GetSpecialCounts(User user)
  292. {
  293. var counts = new ItemSpecialCounts();
  294. IEnumerable<BaseItem> recursiveChildren = GetRecursiveChildren(user);
  295. var recentlyAddedItems = GetRecentlyAddedItems(recursiveChildren, user);
  296. counts.RecentlyAddedItemCount = recentlyAddedItems.Count;
  297. counts.RecentlyAddedUnPlayedItemCount = GetRecentlyAddedUnplayedItems(recentlyAddedItems, user).Count;
  298. counts.InProgressItemCount = GetInProgressItems(recursiveChildren, user).Count;
  299. counts.PlayedPercentage = GetPlayedPercentage(recursiveChildren, user);
  300. return counts;
  301. }
  302. /// <summary>
  303. /// Finds all recursive items within a top-level parent that contain the given genre and are allowed for the current user
  304. /// </summary>
  305. public IEnumerable<BaseItem> GetItemsWithGenre(string genre, User user)
  306. {
  307. return GetRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
  308. }
  309. /// <summary>
  310. /// Finds all recursive items within a top-level parent that contain the given year and are allowed for the current user
  311. /// </summary>
  312. public IEnumerable<BaseItem> GetItemsWithYear(int year, User user)
  313. {
  314. return GetRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
  315. }
  316. /// <summary>
  317. /// Finds all recursive items within a top-level parent that contain the given studio and are allowed for the current user
  318. /// </summary>
  319. public IEnumerable<BaseItem> GetItemsWithStudio(string studio, User user)
  320. {
  321. return GetRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
  322. }
  323. /// <summary>
  324. /// Finds all recursive items within a top-level parent that the user has marked as a favorite
  325. /// </summary>
  326. public IEnumerable<BaseItem> GetFavoriteItems(User user)
  327. {
  328. return GetRecursiveChildren(user).Where(c =>
  329. {
  330. UserItemData data = c.GetUserData(user, false);
  331. if (data != null)
  332. {
  333. return data.IsFavorite;
  334. }
  335. return false;
  336. });
  337. }
  338. /// <summary>
  339. /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
  340. /// </summary>
  341. public IEnumerable<BaseItem> GetItemsWithPerson(string person, User user)
  342. {
  343. return GetRecursiveChildren(user).Where(c =>
  344. {
  345. if (c.People != null)
  346. {
  347. return c.People.ContainsKey(person);
  348. }
  349. return false;
  350. });
  351. }
  352. /// <summary>
  353. /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
  354. /// </summary>
  355. /// <param name="personType">Specify this to limit results to a specific PersonType</param>
  356. public IEnumerable<BaseItem> GetItemsWithPerson(string person, string personType, User user)
  357. {
  358. return GetRecursiveChildren(user).Where(c =>
  359. {
  360. if (c.People != null)
  361. {
  362. return c.People.ContainsKey(person) && c.People[person].Type.Equals(personType, StringComparison.OrdinalIgnoreCase);
  363. }
  364. return false;
  365. });
  366. }
  367. /// <summary>
  368. /// Gets all recently added items (recursive) within a folder, based on configuration and parental settings
  369. /// </summary>
  370. public List<BaseItem> GetRecentlyAddedItems(User user)
  371. {
  372. return GetRecentlyAddedItems(GetRecursiveChildren(user), user);
  373. }
  374. /// <summary>
  375. /// Gets all recently added unplayed items (recursive) within a folder, based on configuration and parental settings
  376. /// </summary>
  377. public List<BaseItem> GetRecentlyAddedUnplayedItems(User user)
  378. {
  379. return GetRecentlyAddedUnplayedItems(GetRecursiveChildren(user), user);
  380. }
  381. /// <summary>
  382. /// Gets all in-progress items (recursive) within a folder
  383. /// </summary>
  384. public List<BaseItem> GetInProgressItems(User user)
  385. {
  386. return GetInProgressItems(GetRecursiveChildren(user), user);
  387. }
  388. /// <summary>
  389. /// Takes a list of items and returns the ones that are recently added
  390. /// </summary>
  391. private static List<BaseItem> GetRecentlyAddedItems(IEnumerable<BaseItem> itemSet, User user)
  392. {
  393. var list = new List<BaseItem>();
  394. foreach (var item in itemSet)
  395. {
  396. if (!item.IsFolder && item.IsRecentlyAdded(user))
  397. {
  398. list.Add(item);
  399. }
  400. }
  401. return list;
  402. }
  403. /// <summary>
  404. /// Takes a list of items and returns the ones that are recently added and unplayed
  405. /// </summary>
  406. private static List<BaseItem> GetRecentlyAddedUnplayedItems(IEnumerable<BaseItem> itemSet, User user)
  407. {
  408. var list = new List<BaseItem>();
  409. foreach (var item in itemSet)
  410. {
  411. if (!item.IsFolder && item.IsRecentlyAdded(user))
  412. {
  413. var userdata = item.GetUserData(user, false);
  414. if (userdata == null || userdata.PlayCount == 0)
  415. {
  416. list.Add(item);
  417. }
  418. }
  419. }
  420. return list;
  421. }
  422. /// <summary>
  423. /// Takes a list of items and returns the ones that are in progress
  424. /// </summary>
  425. private static List<BaseItem> GetInProgressItems(IEnumerable<BaseItem> itemSet, User user)
  426. {
  427. var list = new List<BaseItem>();
  428. foreach (var item in itemSet)
  429. {
  430. if (!item.IsFolder)
  431. {
  432. var userdata = item.GetUserData(user, false);
  433. if (userdata != null && userdata.PlaybackPositionTicks > 0)
  434. {
  435. list.Add(item);
  436. }
  437. }
  438. }
  439. return list;
  440. }
  441. /// <summary>
  442. /// Gets the total played percentage for a set of items
  443. /// </summary>
  444. private static decimal GetPlayedPercentage(IEnumerable<BaseItem> itemSet, User user)
  445. {
  446. itemSet = itemSet.Where(i => !(i.IsFolder));
  447. decimal totalPercent = 0;
  448. int count = 0;
  449. foreach (BaseItem item in itemSet)
  450. {
  451. count++;
  452. UserItemData data = item.GetUserData(user, false);
  453. if (data == null)
  454. {
  455. continue;
  456. }
  457. if (data.PlayCount > 0)
  458. {
  459. totalPercent += 100;
  460. }
  461. else if (data.PlaybackPositionTicks > 0 && item.RunTimeTicks.HasValue)
  462. {
  463. decimal itemPercent = data.PlaybackPositionTicks;
  464. itemPercent /= item.RunTimeTicks.Value;
  465. totalPercent += itemPercent;
  466. }
  467. }
  468. if (count == 0)
  469. {
  470. return 0;
  471. }
  472. return totalPercent / count;
  473. }
  474. /// <summary>
  475. /// Marks the item as either played or unplayed
  476. /// </summary>
  477. public override void SetPlayedStatus(User user, bool wasPlayed)
  478. {
  479. base.SetPlayedStatus(user, wasPlayed);
  480. // Now sweep through recursively and update status
  481. foreach (BaseItem item in GetChildren(user))
  482. {
  483. item.SetPlayedStatus(user, wasPlayed);
  484. }
  485. }
  486. /// <summary>
  487. /// Finds an item by ID, recursively
  488. /// </summary>
  489. public override BaseItem FindItemById(Guid id)
  490. {
  491. var result = base.FindItemById(id);
  492. if (result != null)
  493. {
  494. return result;
  495. }
  496. foreach (BaseItem item in ActualChildren)
  497. {
  498. result = item.FindItemById(id);
  499. if (result != null)
  500. {
  501. return result;
  502. }
  503. }
  504. return null;
  505. }
  506. /// <summary>
  507. /// Finds an item by path, recursively
  508. /// </summary>
  509. public BaseItem FindByPath(string path)
  510. {
  511. if (Path.Equals(path, StringComparison.OrdinalIgnoreCase))
  512. {
  513. return this;
  514. }
  515. foreach (BaseItem item in ActualChildren)
  516. {
  517. var folder = item as Folder;
  518. if (folder != null)
  519. {
  520. var foundItem = folder.FindByPath(path);
  521. if (foundItem != null)
  522. {
  523. return foundItem;
  524. }
  525. }
  526. else if (item.Path.Equals(path, StringComparison.OrdinalIgnoreCase))
  527. {
  528. return item;
  529. }
  530. }
  531. return null;
  532. }
  533. }
  534. }