Folder.cs 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Common.Progress;
  3. using MediaBrowser.Controller.Entities.TV;
  4. using MediaBrowser.Controller.Library;
  5. using MediaBrowser.Controller.Localization;
  6. using MediaBrowser.Controller.Providers;
  7. using MediaBrowser.Model.Dto;
  8. using MediaBrowser.Model.Entities;
  9. using MoreLinq;
  10. using System;
  11. using System.Collections;
  12. using System.Collections.Generic;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Runtime.Serialization;
  16. using System.Threading;
  17. using System.Threading.Tasks;
  18. namespace MediaBrowser.Controller.Entities
  19. {
  20. /// <summary>
  21. /// Class Folder
  22. /// </summary>
  23. public class Folder : BaseItem, IHasThemeMedia, IHasTags
  24. {
  25. public static IUserManager UserManager { get; set; }
  26. public List<Guid> ThemeSongIds { get; set; }
  27. public List<Guid> ThemeVideoIds { get; set; }
  28. public List<string> Tags { get; set; }
  29. public Folder()
  30. {
  31. LinkedChildren = new List<LinkedChild>();
  32. ThemeSongIds = new List<Guid>();
  33. ThemeVideoIds = new List<Guid>();
  34. Tags = new List<string>();
  35. }
  36. /// <summary>
  37. /// Gets a value indicating whether this instance is folder.
  38. /// </summary>
  39. /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
  40. [IgnoreDataMember]
  41. public override bool IsFolder
  42. {
  43. get
  44. {
  45. return true;
  46. }
  47. }
  48. /// <summary>
  49. /// Gets or sets a value indicating whether this instance is physical root.
  50. /// </summary>
  51. /// <value><c>true</c> if this instance is physical root; otherwise, <c>false</c>.</value>
  52. public bool IsPhysicalRoot { get; set; }
  53. /// <summary>
  54. /// Gets or sets a value indicating whether this instance is root.
  55. /// </summary>
  56. /// <value><c>true</c> if this instance is root; otherwise, <c>false</c>.</value>
  57. public bool IsRoot { get; set; }
  58. /// <summary>
  59. /// Gets a value indicating whether this instance is virtual folder.
  60. /// </summary>
  61. /// <value><c>true</c> if this instance is virtual folder; otherwise, <c>false</c>.</value>
  62. [IgnoreDataMember]
  63. public virtual bool IsVirtualFolder
  64. {
  65. get
  66. {
  67. return false;
  68. }
  69. }
  70. public virtual List<LinkedChild> LinkedChildren { get; set; }
  71. protected virtual bool SupportsShortcutChildren
  72. {
  73. get { return true; }
  74. }
  75. /// <summary>
  76. /// Adds the child.
  77. /// </summary>
  78. /// <param name="item">The item.</param>
  79. /// <param name="cancellationToken">The cancellation token.</param>
  80. /// <returns>Task.</returns>
  81. /// <exception cref="System.InvalidOperationException">Unable to add + item.Name</exception>
  82. public async Task AddChild(BaseItem item, CancellationToken cancellationToken)
  83. {
  84. item.Parent = this;
  85. if (item.Id == Guid.Empty)
  86. {
  87. item.Id = item.Path.GetMBId(item.GetType());
  88. }
  89. if (ActualChildren.Any(i => i.Id == item.Id))
  90. {
  91. throw new ArgumentException(string.Format("A child with the Id {0} already exists.", item.Id));
  92. }
  93. if (item.DateCreated == DateTime.MinValue)
  94. {
  95. item.DateCreated = DateTime.UtcNow;
  96. }
  97. if (item.DateModified == DateTime.MinValue)
  98. {
  99. item.DateModified = DateTime.UtcNow;
  100. }
  101. AddChildInternal(item);
  102. await LibraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
  103. await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false);
  104. }
  105. protected void AddChildrenInternal(IEnumerable<BaseItem> children)
  106. {
  107. lock (_childrenSyncLock)
  108. {
  109. var newChildren = ActualChildren.ToList();
  110. newChildren.AddRange(children);
  111. _children = newChildren;
  112. }
  113. }
  114. protected void AddChildInternal(BaseItem child)
  115. {
  116. lock (_childrenSyncLock)
  117. {
  118. var newChildren = ActualChildren.ToList();
  119. newChildren.Add(child);
  120. _children = newChildren;
  121. }
  122. }
  123. protected void RemoveChildrenInternal(IEnumerable<BaseItem> children)
  124. {
  125. var ids = children.Select(i => i.Id).ToList();
  126. lock (_childrenSyncLock)
  127. {
  128. _children = ActualChildren.Where(i => !ids.Contains(i.Id)).ToList();
  129. }
  130. }
  131. protected void ClearChildrenInternal()
  132. {
  133. lock (_childrenSyncLock)
  134. {
  135. _children = new List<BaseItem>();
  136. }
  137. }
  138. /// <summary>
  139. /// Never want folders to be blocked by "BlockNotRated"
  140. /// </summary>
  141. [IgnoreDataMember]
  142. public override string OfficialRatingForComparison
  143. {
  144. get
  145. {
  146. if (this is Series)
  147. {
  148. return base.OfficialRatingForComparison;
  149. }
  150. return !string.IsNullOrEmpty(base.OfficialRatingForComparison) ? base.OfficialRatingForComparison : "None";
  151. }
  152. }
  153. /// <summary>
  154. /// Removes the child.
  155. /// </summary>
  156. /// <param name="item">The item.</param>
  157. /// <param name="cancellationToken">The cancellation token.</param>
  158. /// <returns>Task.</returns>
  159. /// <exception cref="System.InvalidOperationException">Unable to remove + item.Name</exception>
  160. public Task RemoveChild(BaseItem item, CancellationToken cancellationToken)
  161. {
  162. RemoveChildrenInternal(new[] { item });
  163. item.Parent = null;
  164. return ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken);
  165. }
  166. /// <summary>
  167. /// Clears the children.
  168. /// </summary>
  169. /// <param name="cancellationToken">The cancellation token.</param>
  170. /// <returns>Task.</returns>
  171. public Task ClearChildren(CancellationToken cancellationToken)
  172. {
  173. var items = ActualChildren.ToList();
  174. ClearChildrenInternal();
  175. foreach (var item in items)
  176. {
  177. LibraryManager.ReportItemRemoved(item);
  178. }
  179. return ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken);
  180. }
  181. #region Indexing
  182. /// <summary>
  183. /// Returns the valid set of index by options for this folder type.
  184. /// Override or extend to modify.
  185. /// </summary>
  186. /// <returns>Dictionary{System.StringFunc{UserIEnumerable{BaseItem}}}.</returns>
  187. protected virtual IEnumerable<string> GetIndexByOptions()
  188. {
  189. return new List<string> {
  190. {LocalizedStrings.Instance.GetString("NoneDispPref")},
  191. {LocalizedStrings.Instance.GetString("PerformerDispPref")},
  192. {LocalizedStrings.Instance.GetString("GenreDispPref")},
  193. {LocalizedStrings.Instance.GetString("DirectorDispPref")},
  194. {LocalizedStrings.Instance.GetString("YearDispPref")},
  195. {LocalizedStrings.Instance.GetString("StudioDispPref")}
  196. };
  197. }
  198. /// <summary>
  199. /// Get the list of indexy by choices for this folder (localized).
  200. /// </summary>
  201. /// <value>The index by option strings.</value>
  202. [IgnoreDataMember]
  203. public IEnumerable<string> IndexByOptionStrings
  204. {
  205. get { return GetIndexByOptions(); }
  206. }
  207. #endregion
  208. /// <summary>
  209. /// The children
  210. /// </summary>
  211. private IReadOnlyList<BaseItem> _children;
  212. /// <summary>
  213. /// The _children sync lock
  214. /// </summary>
  215. private readonly object _childrenSyncLock = new object();
  216. /// <summary>
  217. /// Gets or sets the actual children.
  218. /// </summary>
  219. /// <value>The actual children.</value>
  220. protected virtual IEnumerable<BaseItem> ActualChildren
  221. {
  222. get
  223. {
  224. return _children ?? (_children = LoadChildrenInternal());
  225. }
  226. }
  227. /// <summary>
  228. /// thread-safe access to the actual children of this folder - without regard to user
  229. /// </summary>
  230. /// <value>The children.</value>
  231. [IgnoreDataMember]
  232. public IEnumerable<BaseItem> Children
  233. {
  234. get { return ActualChildren; }
  235. }
  236. /// <summary>
  237. /// thread-safe access to all recursive children of this folder - without regard to user
  238. /// </summary>
  239. /// <value>The recursive children.</value>
  240. [IgnoreDataMember]
  241. public IEnumerable<BaseItem> RecursiveChildren
  242. {
  243. get { return GetRecursiveChildren(); }
  244. }
  245. public override bool IsVisible(User user)
  246. {
  247. if (this is ICollectionFolder)
  248. {
  249. if (user.Configuration.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
  250. // Backwards compatibility
  251. user.Configuration.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
  252. {
  253. return false;
  254. }
  255. }
  256. return base.IsVisible(user);
  257. }
  258. private List<BaseItem> LoadChildrenInternal()
  259. {
  260. return LoadChildren().ToList();
  261. }
  262. /// <summary>
  263. /// Loads our children. Validation will occur externally.
  264. /// We want this sychronous.
  265. /// </summary>
  266. protected virtual IEnumerable<BaseItem> LoadChildren()
  267. {
  268. //just load our children from the repo - the library will be validated and maintained in other processes
  269. return GetCachedChildren();
  270. }
  271. public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
  272. {
  273. return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions());
  274. }
  275. /// <summary>
  276. /// Validates that the children of the folder still exist
  277. /// </summary>
  278. /// <param name="progress">The progress.</param>
  279. /// <param name="cancellationToken">The cancellation token.</param>
  280. /// <param name="metadataRefreshOptions">The metadata refresh options.</param>
  281. /// <param name="recursive">if set to <c>true</c> [recursive].</param>
  282. /// <returns>Task.</returns>
  283. public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken, MetadataRefreshOptions metadataRefreshOptions, bool recursive = true)
  284. {
  285. metadataRefreshOptions.DirectoryService = metadataRefreshOptions.DirectoryService ?? new DirectoryService(Logger);
  286. return ValidateChildrenWithCancellationSupport(progress, cancellationToken, recursive, true, metadataRefreshOptions, metadataRefreshOptions.DirectoryService);
  287. }
  288. private Task ValidateChildrenWithCancellationSupport(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
  289. {
  290. return ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService);
  291. }
  292. private Dictionary<Guid, BaseItem> GetActualChildrenDictionary()
  293. {
  294. var dictionary = new Dictionary<Guid, BaseItem>();
  295. foreach (var child in ActualChildren)
  296. {
  297. var id = child.Id;
  298. if (dictionary.ContainsKey(id))
  299. {
  300. Logger.Error("Found folder containing items with duplicate id. Path: {0}, Child Name: {1}",
  301. Path ?? Name,
  302. child.Path ?? child.Name);
  303. }
  304. else
  305. {
  306. dictionary[id] = child;
  307. }
  308. }
  309. return dictionary;
  310. }
  311. private bool IsValidFromResolver(BaseItem current, BaseItem newItem)
  312. {
  313. var currentAsVideo = current as Video;
  314. if (currentAsVideo != null)
  315. {
  316. var newAsVideo = newItem as Video;
  317. if (newAsVideo != null)
  318. {
  319. if (currentAsVideo.IsPlaceHolder != newAsVideo.IsPlaceHolder)
  320. {
  321. return false;
  322. }
  323. if (currentAsVideo.IsMultiPart != newAsVideo.IsMultiPart)
  324. {
  325. return false;
  326. }
  327. if (currentAsVideo.HasLocalAlternateVersions != newAsVideo.HasLocalAlternateVersions)
  328. {
  329. return false;
  330. }
  331. }
  332. }
  333. else
  334. {
  335. var currentAsPlaceHolder = current as ISupportsPlaceHolders;
  336. if (currentAsPlaceHolder != null)
  337. {
  338. var newHasPlaceHolder = newItem as ISupportsPlaceHolders;
  339. if (newHasPlaceHolder != null)
  340. {
  341. if (currentAsPlaceHolder.IsPlaceHolder != newHasPlaceHolder.IsPlaceHolder)
  342. {
  343. return false;
  344. }
  345. }
  346. }
  347. }
  348. return current.IsInMixedFolder == newItem.IsInMixedFolder;
  349. }
  350. /// <summary>
  351. /// Validates the children internal.
  352. /// </summary>
  353. /// <param name="progress">The progress.</param>
  354. /// <param name="cancellationToken">The cancellation token.</param>
  355. /// <param name="recursive">if set to <c>true</c> [recursive].</param>
  356. /// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
  357. /// <param name="refreshOptions">The refresh options.</param>
  358. /// <param name="directoryService">The directory service.</param>
  359. /// <returns>Task.</returns>
  360. protected async virtual Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService)
  361. {
  362. var locationType = LocationType;
  363. cancellationToken.ThrowIfCancellationRequested();
  364. var validChildren = new List<BaseItem>();
  365. if (locationType != LocationType.Remote && locationType != LocationType.Virtual)
  366. {
  367. IEnumerable<BaseItem> nonCachedChildren;
  368. try
  369. {
  370. nonCachedChildren = GetNonCachedChildren(directoryService);
  371. }
  372. catch (IOException ex)
  373. {
  374. nonCachedChildren = new BaseItem[] { };
  375. Logger.ErrorException("Error getting file system entries for {0}", ex, Path);
  376. }
  377. if (nonCachedChildren == null) return; //nothing to validate
  378. progress.Report(5);
  379. //build a dictionary of the current children we have now by Id so we can compare quickly and easily
  380. var currentChildren = GetActualChildrenDictionary();
  381. //create a list for our validated children
  382. var newItems = new List<BaseItem>();
  383. cancellationToken.ThrowIfCancellationRequested();
  384. foreach (var child in nonCachedChildren)
  385. {
  386. BaseItem currentChild;
  387. if (currentChildren.TryGetValue(child.Id, out currentChild))
  388. {
  389. if (IsValidFromResolver(currentChild, child))
  390. {
  391. var currentChildLocationType = currentChild.LocationType;
  392. if (currentChildLocationType != LocationType.Remote &&
  393. currentChildLocationType != LocationType.Virtual)
  394. {
  395. currentChild.DateModified = child.DateModified;
  396. }
  397. currentChild.IsOffline = false;
  398. validChildren.Add(currentChild);
  399. }
  400. else
  401. {
  402. validChildren.Add(child);
  403. }
  404. }
  405. else
  406. {
  407. // Brand new item - needs to be added
  408. newItems.Add(child);
  409. validChildren.Add(child);
  410. }
  411. }
  412. // If any items were added or removed....
  413. if (newItems.Count > 0 || currentChildren.Count != validChildren.Count)
  414. {
  415. // That's all the new and changed ones - now see if there are any that are missing
  416. var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
  417. var actualRemovals = new List<BaseItem>();
  418. foreach (var item in itemsRemoved)
  419. {
  420. if (item.LocationType == LocationType.Virtual ||
  421. item.LocationType == LocationType.Remote)
  422. {
  423. // Don't remove these because there's no way to accurately validate them.
  424. validChildren.Add(item);
  425. }
  426. else if (!string.IsNullOrEmpty(item.Path) && IsPathOffline(item.Path))
  427. {
  428. item.IsOffline = true;
  429. validChildren.Add(item);
  430. }
  431. else
  432. {
  433. item.IsOffline = false;
  434. actualRemovals.Add(item);
  435. }
  436. }
  437. if (actualRemovals.Count > 0)
  438. {
  439. RemoveChildrenInternal(actualRemovals);
  440. foreach (var item in actualRemovals)
  441. {
  442. LibraryManager.ReportItemRemoved(item);
  443. }
  444. }
  445. await LibraryManager.CreateItems(newItems, cancellationToken).ConfigureAwait(false);
  446. AddChildrenInternal(newItems);
  447. await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false);
  448. }
  449. }
  450. progress.Report(10);
  451. cancellationToken.ThrowIfCancellationRequested();
  452. if (recursive)
  453. {
  454. await ValidateSubFolders(ActualChildren.OfType<Folder>().ToList(), directoryService, progress, cancellationToken).ConfigureAwait(false);
  455. }
  456. progress.Report(20);
  457. if (refreshChildMetadata)
  458. {
  459. var container = this as IMetadataContainer;
  460. var innerProgress = new ActionableProgress<double>();
  461. innerProgress.RegisterAction(p => progress.Report((.80 * p) + 20));
  462. if (container != null)
  463. {
  464. await container.RefreshAllMetadata(refreshOptions, innerProgress, cancellationToken).ConfigureAwait(false);
  465. }
  466. else
  467. {
  468. await RefreshMetadataRecursive(refreshOptions, recursive, innerProgress, cancellationToken);
  469. }
  470. }
  471. progress.Report(100);
  472. }
  473. private async Task RefreshMetadataRecursive(MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
  474. {
  475. var children = ActualChildren.ToList();
  476. var percentages = new Dictionary<Guid, double>(children.Count);
  477. var tasks = new List<Task>();
  478. foreach (var child in children)
  479. {
  480. if (tasks.Count >= 3)
  481. {
  482. await Task.WhenAll(tasks).ConfigureAwait(false);
  483. tasks.Clear();
  484. }
  485. cancellationToken.ThrowIfCancellationRequested();
  486. var innerProgress = new ActionableProgress<double>();
  487. // Avoid implicitly captured closure
  488. var currentChild = child;
  489. innerProgress.RegisterAction(p =>
  490. {
  491. lock (percentages)
  492. {
  493. percentages[currentChild.Id] = p / 100;
  494. var percent = percentages.Values.Sum();
  495. percent /= children.Count;
  496. percent *= 100;
  497. progress.Report(percent);
  498. }
  499. });
  500. if (child.IsFolder)
  501. {
  502. await RefreshChildMetadata(child, refreshOptions, recursive, innerProgress, cancellationToken)
  503. .ConfigureAwait(false);
  504. }
  505. else
  506. {
  507. // Avoid implicitly captured closure
  508. var taskChild = child;
  509. tasks.Add(Task.Run(async () => await RefreshChildMetadata(taskChild, refreshOptions, false, innerProgress, cancellationToken).ConfigureAwait(false), cancellationToken));
  510. }
  511. }
  512. await Task.WhenAll(tasks).ConfigureAwait(false);
  513. progress.Report(100);
  514. }
  515. private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
  516. {
  517. var container = child as IMetadataContainer;
  518. if (container != null)
  519. {
  520. await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
  521. }
  522. else
  523. {
  524. await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
  525. if (recursive)
  526. {
  527. var folder = child as Folder;
  528. if (folder != null)
  529. {
  530. await folder.RefreshMetadataRecursive(refreshOptions, true, progress, cancellationToken);
  531. }
  532. }
  533. }
  534. progress.Report(100);
  535. }
  536. /// <summary>
  537. /// Refreshes the children.
  538. /// </summary>
  539. /// <param name="children">The children.</param>
  540. /// <param name="directoryService">The directory service.</param>
  541. /// <param name="progress">The progress.</param>
  542. /// <param name="cancellationToken">The cancellation token.</param>
  543. /// <returns>Task.</returns>
  544. private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken)
  545. {
  546. var list = children;
  547. var childCount = list.Count;
  548. var percentages = new Dictionary<Guid, double>(list.Count);
  549. foreach (var item in list)
  550. {
  551. cancellationToken.ThrowIfCancellationRequested();
  552. var child = item;
  553. var innerProgress = new ActionableProgress<double>();
  554. innerProgress.RegisterAction(p =>
  555. {
  556. lock (percentages)
  557. {
  558. percentages[child.Id] = p / 100;
  559. var percent = percentages.Values.Sum();
  560. percent /= childCount;
  561. progress.Report((10 * percent) + 10);
  562. }
  563. });
  564. await child.ValidateChildrenWithCancellationSupport(innerProgress, cancellationToken, true, false, null, directoryService)
  565. .ConfigureAwait(false);
  566. }
  567. }
  568. /// <summary>
  569. /// Determines whether the specified path is offline.
  570. /// </summary>
  571. /// <param name="path">The path.</param>
  572. /// <returns><c>true</c> if the specified path is offline; otherwise, <c>false</c>.</returns>
  573. private bool IsPathOffline(string path)
  574. {
  575. if (File.Exists(path))
  576. {
  577. return false;
  578. }
  579. var originalPath = path;
  580. // Depending on whether the path is local or unc, it may return either null or '\' at the top
  581. while (!string.IsNullOrEmpty(path) && path.Length > 1)
  582. {
  583. if (Directory.Exists(path))
  584. {
  585. return false;
  586. }
  587. path = System.IO.Path.GetDirectoryName(path);
  588. }
  589. if (ContainsPath(LibraryManager.GetDefaultVirtualFolders(), originalPath))
  590. {
  591. return true;
  592. }
  593. return UserManager.Users.Any(user => ContainsPath(LibraryManager.GetVirtualFolders(user), originalPath));
  594. }
  595. /// <summary>
  596. /// Determines whether the specified folders contains path.
  597. /// </summary>
  598. /// <param name="folders">The folders.</param>
  599. /// <param name="path">The path.</param>
  600. /// <returns><c>true</c> if the specified folders contains path; otherwise, <c>false</c>.</returns>
  601. private bool ContainsPath(IEnumerable<VirtualFolderInfo> folders, string path)
  602. {
  603. return folders.SelectMany(i => i.Locations).Any(i => ContainsPath(i, path));
  604. }
  605. private bool ContainsPath(string parent, string path)
  606. {
  607. return string.Equals(parent, path, StringComparison.OrdinalIgnoreCase) || FileSystem.ContainsSubPath(parent, path);
  608. }
  609. /// <summary>
  610. /// Get the children of this folder from the actual file system
  611. /// </summary>
  612. /// <returns>IEnumerable{BaseItem}.</returns>
  613. protected virtual IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
  614. {
  615. return LibraryManager.ResolvePaths<BaseItem>(GetFileSystemChildren(directoryService), directoryService, this);
  616. }
  617. /// <summary>
  618. /// Get our children from the repo - stubbed for now
  619. /// </summary>
  620. /// <returns>IEnumerable{BaseItem}.</returns>
  621. protected IEnumerable<BaseItem> GetCachedChildren()
  622. {
  623. return ItemRepository.GetChildren(Id).Select(RetrieveChild).Where(i => i != null);
  624. }
  625. /// <summary>
  626. /// Retrieves the child.
  627. /// </summary>
  628. /// <param name="child">The child.</param>
  629. /// <returns>BaseItem.</returns>
  630. private BaseItem RetrieveChild(Guid child)
  631. {
  632. var item = LibraryManager.GetItemById(child);
  633. if (item != null)
  634. {
  635. if (item is IByReferenceItem)
  636. {
  637. return LibraryManager.GetOrAddByReferenceItem(item);
  638. }
  639. item.Parent = this;
  640. }
  641. return item;
  642. }
  643. /// <summary>
  644. /// Gets allowed children of an item
  645. /// </summary>
  646. /// <param name="user">The user.</param>
  647. /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
  648. /// <returns>IEnumerable{BaseItem}.</returns>
  649. /// <exception cref="System.ArgumentNullException"></exception>
  650. public virtual IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren)
  651. {
  652. return GetChildren(user, includeLinkedChildren, false);
  653. }
  654. internal IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren, bool includeHidden)
  655. {
  656. if (user == null)
  657. {
  658. throw new ArgumentNullException();
  659. }
  660. //the true root should return our users root folder children
  661. if (IsPhysicalRoot) return user.RootFolder.GetChildren(user, includeLinkedChildren);
  662. var list = new List<BaseItem>();
  663. var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, includeHidden, false);
  664. return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
  665. }
  666. protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
  667. {
  668. return Children;
  669. }
  670. /// <summary>
  671. /// Adds the children to list.
  672. /// </summary>
  673. /// <param name="user">The user.</param>
  674. /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
  675. /// <param name="list">The list.</param>
  676. /// <param name="includeHidden">if set to <c>true</c> [include hidden].</param>
  677. /// <param name="recursive">if set to <c>true</c> [recursive].</param>
  678. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  679. private bool AddChildrenToList(User user, bool includeLinkedChildren, List<BaseItem> list, bool includeHidden, bool recursive)
  680. {
  681. var hasLinkedChildren = false;
  682. foreach (var child in GetEligibleChildrenForRecursiveChildren(user))
  683. {
  684. if (child.IsVisible(user))
  685. {
  686. if (includeHidden || !child.IsHiddenFromUser(user))
  687. {
  688. list.Add(child);
  689. }
  690. if (recursive && child.IsFolder)
  691. {
  692. var folder = (Folder)child;
  693. if (folder.AddChildrenToList(user, includeLinkedChildren, list, includeHidden, true))
  694. {
  695. hasLinkedChildren = true;
  696. }
  697. }
  698. }
  699. }
  700. if (includeLinkedChildren)
  701. {
  702. foreach (var child in GetLinkedChildren())
  703. {
  704. if (child.IsVisible(user))
  705. {
  706. hasLinkedChildren = true;
  707. list.Add(child);
  708. }
  709. }
  710. }
  711. return hasLinkedChildren;
  712. }
  713. /// <summary>
  714. /// Gets allowed recursive children of an item
  715. /// </summary>
  716. /// <param name="user">The user.</param>
  717. /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
  718. /// <returns>IEnumerable{BaseItem}.</returns>
  719. /// <exception cref="System.ArgumentNullException"></exception>
  720. public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
  721. {
  722. if (user == null)
  723. {
  724. throw new ArgumentNullException("user");
  725. }
  726. var list = new List<BaseItem>();
  727. var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, false, true);
  728. return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
  729. }
  730. /// <summary>
  731. /// Gets the recursive children.
  732. /// </summary>
  733. /// <returns>IList{BaseItem}.</returns>
  734. public IList<BaseItem> GetRecursiveChildren()
  735. {
  736. var list = new List<BaseItem>();
  737. AddChildrenToList(list, true, null);
  738. return list;
  739. }
  740. /// <summary>
  741. /// Adds the children to list.
  742. /// </summary>
  743. /// <param name="list">The list.</param>
  744. /// <param name="recursive">if set to <c>true</c> [recursive].</param>
  745. /// <param name="filter">The filter.</param>
  746. private void AddChildrenToList(List<BaseItem> list, bool recursive, Func<BaseItem, bool> filter)
  747. {
  748. foreach (var child in Children)
  749. {
  750. if (filter == null || filter(child))
  751. {
  752. list.Add(child);
  753. }
  754. if (recursive && child.IsFolder)
  755. {
  756. var folder = (Folder)child;
  757. folder.AddChildrenToList(list, true, filter);
  758. }
  759. }
  760. }
  761. /// <summary>
  762. /// Gets the linked children.
  763. /// </summary>
  764. /// <returns>IEnumerable{BaseItem}.</returns>
  765. public IEnumerable<BaseItem> GetLinkedChildren()
  766. {
  767. return LinkedChildren
  768. .Select(GetLinkedChild)
  769. .Where(i => i != null);
  770. }
  771. protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemInfo> fileSystemChildren, CancellationToken cancellationToken)
  772. {
  773. var changesFound = false;
  774. if (SupportsShortcutChildren && LocationType == LocationType.FileSystem)
  775. {
  776. if (RefreshLinkedChildren(fileSystemChildren))
  777. {
  778. changesFound = true;
  779. }
  780. }
  781. var baseHasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
  782. return baseHasChanges || changesFound;
  783. }
  784. /// <summary>
  785. /// Refreshes the linked children.
  786. /// </summary>
  787. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  788. private bool RefreshLinkedChildren(IEnumerable<FileSystemInfo> fileSystemChildren)
  789. {
  790. var currentManualLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Manual).ToList();
  791. var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList();
  792. var newShortcutLinks = fileSystemChildren
  793. .Where(i => (i.Attributes & FileAttributes.Directory) != FileAttributes.Directory && FileSystem.IsShortcut(i.FullName))
  794. .Select(i =>
  795. {
  796. try
  797. {
  798. Logger.Debug("Found shortcut at {0}", i.FullName);
  799. var resolvedPath = FileSystem.ResolveShortcut(i.FullName);
  800. if (!string.IsNullOrEmpty(resolvedPath))
  801. {
  802. return new LinkedChild
  803. {
  804. Path = resolvedPath,
  805. Type = LinkedChildType.Shortcut
  806. };
  807. }
  808. Logger.Error("Error resolving shortcut {0}", i.FullName);
  809. return null;
  810. }
  811. catch (IOException ex)
  812. {
  813. Logger.ErrorException("Error resolving shortcut {0}", ex, i.FullName);
  814. return null;
  815. }
  816. })
  817. .Where(i => i != null)
  818. .ToList();
  819. if (!newShortcutLinks.SequenceEqual(currentShortcutLinks, new LinkedChildComparer()))
  820. {
  821. Logger.Info("Shortcut links have changed for {0}", Path);
  822. newShortcutLinks.AddRange(currentManualLinks);
  823. LinkedChildren = newShortcutLinks;
  824. return true;
  825. }
  826. foreach (var child in LinkedChildren)
  827. {
  828. // Reset the cached value
  829. if (child.ItemId.HasValue && child.ItemId.Value == Guid.Empty)
  830. {
  831. child.ItemId = null;
  832. }
  833. }
  834. return false;
  835. }
  836. /// <summary>
  837. /// Folders need to validate and refresh
  838. /// </summary>
  839. /// <returns>Task.</returns>
  840. public override async Task ChangedExternally()
  841. {
  842. var progress = new Progress<double>();
  843. await ValidateChildren(progress, CancellationToken.None).ConfigureAwait(false);
  844. await base.ChangedExternally().ConfigureAwait(false);
  845. }
  846. /// <summary>
  847. /// Marks the played.
  848. /// </summary>
  849. /// <param name="user">The user.</param>
  850. /// <param name="datePlayed">The date played.</param>
  851. /// <param name="userManager">The user manager.</param>
  852. /// <returns>Task.</returns>
  853. public override async Task MarkPlayed(User user, DateTime? datePlayed, IUserDataManager userManager)
  854. {
  855. // Sweep through recursively and update status
  856. var tasks = GetRecursiveChildren(user, true).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual).Select(c => c.MarkPlayed(user, datePlayed, userManager));
  857. await Task.WhenAll(tasks).ConfigureAwait(false);
  858. }
  859. /// <summary>
  860. /// Marks the unplayed.
  861. /// </summary>
  862. /// <param name="user">The user.</param>
  863. /// <param name="userManager">The user manager.</param>
  864. /// <returns>Task.</returns>
  865. public override async Task MarkUnplayed(User user, IUserDataManager userManager)
  866. {
  867. // Sweep through recursively and update status
  868. var tasks = GetRecursiveChildren(user, true).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual).Select(c => c.MarkUnplayed(user, userManager));
  869. await Task.WhenAll(tasks).ConfigureAwait(false);
  870. }
  871. /// <summary>
  872. /// Finds an item by path, recursively
  873. /// </summary>
  874. /// <param name="path">The path.</param>
  875. /// <returns>BaseItem.</returns>
  876. /// <exception cref="System.ArgumentNullException"></exception>
  877. public BaseItem FindByPath(string path)
  878. {
  879. if (string.IsNullOrEmpty(path))
  880. {
  881. throw new ArgumentNullException();
  882. }
  883. if (string.Equals(Path, path, StringComparison.OrdinalIgnoreCase))
  884. {
  885. return this;
  886. }
  887. if (PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase))
  888. {
  889. return this;
  890. }
  891. return RecursiveChildren.FirstOrDefault(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) ||
  892. (!i.IsFolder && !i.IsInMixedFolder && string.Equals(i.ContainingFolderPath, path, StringComparison.OrdinalIgnoreCase)) ||
  893. i.PhysicalLocations.Contains(path, StringComparer.OrdinalIgnoreCase));
  894. }
  895. public override bool IsPlayed(User user)
  896. {
  897. return GetRecursiveChildren(user).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
  898. .All(i => i.IsPlayed(user));
  899. }
  900. public override bool IsUnplayed(User user)
  901. {
  902. return GetRecursiveChildren(user).Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual)
  903. .All(i => i.IsUnplayed(user));
  904. }
  905. public override void FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, User user)
  906. {
  907. var recursiveItemCount = 0;
  908. var unplayed = 0;
  909. double totalPercentPlayed = 0;
  910. IEnumerable<BaseItem> children;
  911. var folder = this;
  912. var season = folder as Season;
  913. if (season != null)
  914. {
  915. children = season.GetEpisodes(user).Where(i => i.LocationType != LocationType.Virtual);
  916. }
  917. else
  918. {
  919. children = folder.GetRecursiveChildren(user)
  920. .Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual);
  921. }
  922. // Loop through each recursive child
  923. foreach (var child in children)
  924. {
  925. recursiveItemCount++;
  926. var isUnplayed = true;
  927. var itemUserData = UserDataManager.GetUserData(user.Id, child.GetUserDataKey());
  928. // Incrememt totalPercentPlayed
  929. if (itemUserData != null)
  930. {
  931. if (itemUserData.Played)
  932. {
  933. totalPercentPlayed += 100;
  934. isUnplayed = false;
  935. }
  936. else if (itemUserData.PlaybackPositionTicks > 0 && child.RunTimeTicks.HasValue && child.RunTimeTicks.Value > 0)
  937. {
  938. double itemPercent = itemUserData.PlaybackPositionTicks;
  939. itemPercent /= child.RunTimeTicks.Value;
  940. totalPercentPlayed += itemPercent;
  941. }
  942. }
  943. if (isUnplayed)
  944. {
  945. unplayed++;
  946. }
  947. }
  948. dto.UnplayedItemCount = unplayed;
  949. if (recursiveItemCount > 0)
  950. {
  951. dto.PlayedPercentage = totalPercentPlayed / recursiveItemCount;
  952. dto.Played = dto.PlayedPercentage.Value >= 100;
  953. }
  954. }
  955. }
  956. }