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 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 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.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
  529. }
  530. }
  531. }