Folder.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  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. //Logger.LogInfo("New Item Added to Library: ("+child.GetType().Name+")"+ child.Name + "(" + child.Path + ")");
  140. //refresh it
  141. child.RefreshMetadata();
  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. //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. lock (childLock)
  182. ActualChildren = validChildren;
  183. //and save children in repo...
  184. //and fire event
  185. this.OnChildrenChanged(changedArgs);
  186. }
  187. ChildrenValidating = false;
  188. }
  189. /// <summary>
  190. /// Get the children of this folder from the actual file system
  191. /// </summary>
  192. /// <returns></returns>
  193. protected async virtual Task<IEnumerable<BaseItem>> GetNonCachedChildren()
  194. {
  195. ItemResolveEventArgs args = new ItemResolveEventArgs()
  196. {
  197. FileInfo = FileData.GetFileData(this.Path),
  198. Parent = this.Parent,
  199. Cancel = false,
  200. Path = this.Path
  201. };
  202. // Gather child folder and files
  203. if (args.IsDirectory)
  204. {
  205. args.FileSystemChildren = FileData.GetFileSystemEntries(this.Path, "*").ToArray();
  206. bool isVirtualFolder = Parent != null && Parent.IsRoot;
  207. args = FileSystemHelper.FilterChildFileSystemEntries(args, isVirtualFolder);
  208. }
  209. else
  210. {
  211. Logger.LogError("Folder has a path that is not a directory: " + this.Path);
  212. return null;
  213. }
  214. if (!EntityResolutionHelper.ShouldResolvePathContents(args))
  215. {
  216. return null;
  217. }
  218. return (await Task.WhenAll<BaseItem>(GetChildren(args.FileSystemChildren)).ConfigureAwait(false))
  219. .Where(i => i != null).OrderBy(f =>
  220. {
  221. return string.IsNullOrEmpty(f.SortName) ? f.Name : f.SortName;
  222. });
  223. }
  224. /// <summary>
  225. /// Resolves a path into a BaseItem
  226. /// </summary>
  227. protected async Task<BaseItem> GetChild(string path, WIN32_FIND_DATA? fileInfo = null)
  228. {
  229. ItemResolveEventArgs args = new ItemResolveEventArgs()
  230. {
  231. FileInfo = fileInfo ?? FileData.GetFileData(path),
  232. Parent = this,
  233. Cancel = false,
  234. Path = path
  235. };
  236. args.FileSystemChildren = FileData.GetFileSystemEntries(path, "*").ToArray();
  237. args = FileSystemHelper.FilterChildFileSystemEntries(args, false);
  238. return Kernel.Instance.ResolveItem(args);
  239. }
  240. /// <summary>
  241. /// Finds child BaseItems for a given Folder
  242. /// </summary>
  243. protected Task<BaseItem>[] GetChildren(WIN32_FIND_DATA[] fileSystemChildren)
  244. {
  245. Task<BaseItem>[] tasks = new Task<BaseItem>[fileSystemChildren.Length];
  246. for (int i = 0; i < fileSystemChildren.Length; i++)
  247. {
  248. var child = fileSystemChildren[i];
  249. tasks[i] = GetChild(child.Path, child);
  250. }
  251. return tasks;
  252. }
  253. /// <summary>
  254. /// Get our children from the repo - stubbed for now
  255. /// </summary>
  256. /// <returns></returns>
  257. protected virtual List<BaseItem> GetCachedChildren()
  258. {
  259. return new List<BaseItem>();
  260. }
  261. /// <summary>
  262. /// Gets allowed children of an item
  263. /// </summary>
  264. public IEnumerable<BaseItem> GetChildren(User user)
  265. {
  266. return ActualChildren.Where(c => c.IsParentalAllowed(user));
  267. }
  268. /// <summary>
  269. /// Gets allowed recursive children of an item
  270. /// </summary>
  271. public IEnumerable<BaseItem> GetRecursiveChildren(User user)
  272. {
  273. foreach (var item in GetChildren(user))
  274. {
  275. yield return item;
  276. var subFolder = item as Folder;
  277. if (subFolder != null)
  278. {
  279. foreach (var subitem in subFolder.GetRecursiveChildren(user))
  280. {
  281. yield return subitem;
  282. }
  283. }
  284. }
  285. }
  286. /// <summary>
  287. /// 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
  288. /// </summary>
  289. public ItemSpecialCounts GetSpecialCounts(User user)
  290. {
  291. var counts = new ItemSpecialCounts();
  292. IEnumerable<BaseItem> recursiveChildren = GetRecursiveChildren(user);
  293. var recentlyAddedItems = GetRecentlyAddedItems(recursiveChildren, user);
  294. counts.RecentlyAddedItemCount = recentlyAddedItems.Count;
  295. counts.RecentlyAddedUnPlayedItemCount = GetRecentlyAddedUnplayedItems(recentlyAddedItems, user).Count;
  296. counts.InProgressItemCount = GetInProgressItems(recursiveChildren, user).Count;
  297. counts.PlayedPercentage = GetPlayedPercentage(recursiveChildren, user);
  298. return counts;
  299. }
  300. /// <summary>
  301. /// Finds all recursive items within a top-level parent that contain the given genre and are allowed for the current user
  302. /// </summary>
  303. public IEnumerable<BaseItem> GetItemsWithGenre(string genre, User user)
  304. {
  305. return GetRecursiveChildren(user).Where(f => f.Genres != null && f.Genres.Any(s => s.Equals(genre, StringComparison.OrdinalIgnoreCase)));
  306. }
  307. /// <summary>
  308. /// Finds all recursive items within a top-level parent that contain the given year and are allowed for the current user
  309. /// </summary>
  310. public IEnumerable<BaseItem> GetItemsWithYear(int year, User user)
  311. {
  312. return GetRecursiveChildren(user).Where(f => f.ProductionYear.HasValue && f.ProductionYear == year);
  313. }
  314. /// <summary>
  315. /// Finds all recursive items within a top-level parent that contain the given studio and are allowed for the current user
  316. /// </summary>
  317. public IEnumerable<BaseItem> GetItemsWithStudio(string studio, User user)
  318. {
  319. return GetRecursiveChildren(user).Where(f => f.Studios != null && f.Studios.Any(s => s.Equals(studio, StringComparison.OrdinalIgnoreCase)));
  320. }
  321. /// <summary>
  322. /// Finds all recursive items within a top-level parent that the user has marked as a favorite
  323. /// </summary>
  324. public IEnumerable<BaseItem> GetFavoriteItems(User user)
  325. {
  326. return GetRecursiveChildren(user).Where(c =>
  327. {
  328. UserItemData data = c.GetUserData(user, false);
  329. if (data != null)
  330. {
  331. return data.IsFavorite;
  332. }
  333. return false;
  334. });
  335. }
  336. /// <summary>
  337. /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
  338. /// </summary>
  339. public IEnumerable<BaseItem> GetItemsWithPerson(string person, User user)
  340. {
  341. return GetRecursiveChildren(user).Where(c =>
  342. {
  343. if (c.People != null)
  344. {
  345. return c.People.ContainsKey(person);
  346. }
  347. return false;
  348. });
  349. }
  350. /// <summary>
  351. /// Finds all recursive items within a top-level parent that contain the given person and are allowed for the current user
  352. /// </summary>
  353. /// <param name="personType">Specify this to limit results to a specific PersonType</param>
  354. public IEnumerable<BaseItem> GetItemsWithPerson(string person, string personType, User user)
  355. {
  356. return GetRecursiveChildren(user).Where(c =>
  357. {
  358. if (c.People != null)
  359. {
  360. return c.People.ContainsKey(person) && c.People[person].Type.Equals(personType, StringComparison.OrdinalIgnoreCase);
  361. }
  362. return false;
  363. });
  364. }
  365. /// <summary>
  366. /// Gets all recently added items (recursive) within a folder, based on configuration and parental settings
  367. /// </summary>
  368. public List<BaseItem> GetRecentlyAddedItems(User user)
  369. {
  370. return GetRecentlyAddedItems(GetRecursiveChildren(user), user);
  371. }
  372. /// <summary>
  373. /// Gets all recently added unplayed items (recursive) within a folder, based on configuration and parental settings
  374. /// </summary>
  375. public List<BaseItem> GetRecentlyAddedUnplayedItems(User user)
  376. {
  377. return GetRecentlyAddedUnplayedItems(GetRecursiveChildren(user), user);
  378. }
  379. /// <summary>
  380. /// Gets all in-progress items (recursive) within a folder
  381. /// </summary>
  382. public List<BaseItem> GetInProgressItems(User user)
  383. {
  384. return GetInProgressItems(GetRecursiveChildren(user), user);
  385. }
  386. /// <summary>
  387. /// Takes a list of items and returns the ones that are recently added
  388. /// </summary>
  389. private static List<BaseItem> GetRecentlyAddedItems(IEnumerable<BaseItem> itemSet, User user)
  390. {
  391. var list = new List<BaseItem>();
  392. foreach (var item in itemSet)
  393. {
  394. if (!item.IsFolder && item.IsRecentlyAdded(user))
  395. {
  396. list.Add(item);
  397. }
  398. }
  399. return list;
  400. }
  401. /// <summary>
  402. /// Takes a list of items and returns the ones that are recently added and unplayed
  403. /// </summary>
  404. private static List<BaseItem> GetRecentlyAddedUnplayedItems(IEnumerable<BaseItem> itemSet, User user)
  405. {
  406. var list = new List<BaseItem>();
  407. foreach (var item in itemSet)
  408. {
  409. if (!item.IsFolder && item.IsRecentlyAdded(user))
  410. {
  411. var userdata = item.GetUserData(user, false);
  412. if (userdata == null || userdata.PlayCount == 0)
  413. {
  414. list.Add(item);
  415. }
  416. }
  417. }
  418. return list;
  419. }
  420. /// <summary>
  421. /// Takes a list of items and returns the ones that are in progress
  422. /// </summary>
  423. private static List<BaseItem> GetInProgressItems(IEnumerable<BaseItem> itemSet, User user)
  424. {
  425. var list = new List<BaseItem>();
  426. foreach (var item in itemSet)
  427. {
  428. if (!item.IsFolder)
  429. {
  430. var userdata = item.GetUserData(user, false);
  431. if (userdata != null && userdata.PlaybackPositionTicks > 0)
  432. {
  433. list.Add(item);
  434. }
  435. }
  436. }
  437. return list;
  438. }
  439. /// <summary>
  440. /// Gets the total played percentage for a set of items
  441. /// </summary>
  442. private static decimal GetPlayedPercentage(IEnumerable<BaseItem> itemSet, User user)
  443. {
  444. itemSet = itemSet.Where(i => !(i.IsFolder));
  445. decimal totalPercent = 0;
  446. int count = 0;
  447. foreach (BaseItem item in itemSet)
  448. {
  449. count++;
  450. UserItemData data = item.GetUserData(user, false);
  451. if (data == null)
  452. {
  453. continue;
  454. }
  455. if (data.PlayCount > 0)
  456. {
  457. totalPercent += 100;
  458. }
  459. else if (data.PlaybackPositionTicks > 0 && item.RunTimeTicks.HasValue)
  460. {
  461. decimal itemPercent = data.PlaybackPositionTicks;
  462. itemPercent /= item.RunTimeTicks.Value;
  463. totalPercent += itemPercent;
  464. }
  465. }
  466. if (count == 0)
  467. {
  468. return 0;
  469. }
  470. return totalPercent / count;
  471. }
  472. /// <summary>
  473. /// Marks the item as either played or unplayed
  474. /// </summary>
  475. public override void SetPlayedStatus(User user, bool wasPlayed)
  476. {
  477. base.SetPlayedStatus(user, wasPlayed);
  478. // Now sweep through recursively and update status
  479. foreach (BaseItem item in GetChildren(user))
  480. {
  481. item.SetPlayedStatus(user, wasPlayed);
  482. }
  483. }
  484. /// <summary>
  485. /// Finds an item by ID, recursively
  486. /// </summary>
  487. public override BaseItem FindItemById(Guid id)
  488. {
  489. var result = base.FindItemById(id);
  490. if (result != null)
  491. {
  492. return result;
  493. }
  494. foreach (BaseItem item in ActualChildren)
  495. {
  496. result = item.FindItemById(id);
  497. if (result != null)
  498. {
  499. return result;
  500. }
  501. }
  502. return null;
  503. }
  504. /// <summary>
  505. /// Finds an item by path, recursively
  506. /// </summary>
  507. public BaseItem FindByPath(string path)
  508. {
  509. if (Path.Equals(path, StringComparison.OrdinalIgnoreCase))
  510. {
  511. return this;
  512. }
  513. foreach (BaseItem item in ActualChildren)
  514. {
  515. var folder = item as Folder;
  516. if (folder != null)
  517. {
  518. var foundItem = folder.FindByPath(path);
  519. if (foundItem != null)
  520. {
  521. return foundItem;
  522. }
  523. }
  524. else if (item.Path.Equals(path, StringComparison.OrdinalIgnoreCase))
  525. {
  526. return item;
  527. }
  528. }
  529. return null;
  530. }
  531. }
  532. }