Folder.cs 21 KB

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