LibraryManager.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140
  1. using MediaBrowser.Common.Events;
  2. using MediaBrowser.Common.Extensions;
  3. using MediaBrowser.Common.Progress;
  4. using MediaBrowser.Common.ScheduledTasks;
  5. using MediaBrowser.Controller.Configuration;
  6. using MediaBrowser.Controller.Entities;
  7. using MediaBrowser.Controller.Entities.Audio;
  8. using MediaBrowser.Controller.Entities.Movies;
  9. using MediaBrowser.Controller.IO;
  10. using MediaBrowser.Controller.Library;
  11. using MediaBrowser.Controller.Persistence;
  12. using MediaBrowser.Controller.Resolvers;
  13. using MediaBrowser.Controller.Sorting;
  14. using MediaBrowser.Model.Configuration;
  15. using MediaBrowser.Model.Entities;
  16. using MediaBrowser.Model.Logging;
  17. using MediaBrowser.Server.Implementations.ScheduledTasks;
  18. using MoreLinq;
  19. using System;
  20. using System.Collections.Concurrent;
  21. using System.Collections.Generic;
  22. using System.Globalization;
  23. using System.IO;
  24. using System.Linq;
  25. using System.Threading;
  26. using System.Threading.Tasks;
  27. using SortOrder = MediaBrowser.Model.Entities.SortOrder;
  28. namespace MediaBrowser.Server.Implementations.Library
  29. {
  30. /// <summary>
  31. /// Class LibraryManager
  32. /// </summary>
  33. public class LibraryManager : ILibraryManager
  34. {
  35. /// <summary>
  36. /// Gets the intro providers.
  37. /// </summary>
  38. /// <value>The intro providers.</value>
  39. private IEnumerable<IIntroProvider> IntroProviders { get; set; }
  40. /// <summary>
  41. /// Gets the list of entity resolution ignore rules
  42. /// </summary>
  43. /// <value>The entity resolution ignore rules.</value>
  44. private IEnumerable<IResolverIgnoreRule> EntityResolutionIgnoreRules { get; set; }
  45. /// <summary>
  46. /// Gets the list of BasePluginFolders added by plugins
  47. /// </summary>
  48. /// <value>The plugin folders.</value>
  49. private IEnumerable<IVirtualFolderCreator> PluginFolderCreators { get; set; }
  50. /// <summary>
  51. /// Gets the list of currently registered entity resolvers
  52. /// </summary>
  53. /// <value>The entity resolvers enumerable.</value>
  54. private IEnumerable<IItemResolver> EntityResolvers { get; set; }
  55. /// <summary>
  56. /// Gets or sets the comparers.
  57. /// </summary>
  58. /// <value>The comparers.</value>
  59. private IEnumerable<IBaseItemComparer> Comparers { get; set; }
  60. /// <summary>
  61. /// Gets the active item repository
  62. /// </summary>
  63. /// <value>The item repository.</value>
  64. public IItemRepository ItemRepository { get; set; }
  65. /// <summary>
  66. /// Occurs when [item added].
  67. /// </summary>
  68. public event EventHandler<ItemChangeEventArgs> ItemAdded;
  69. /// <summary>
  70. /// Occurs when [item updated].
  71. /// </summary>
  72. public event EventHandler<ItemChangeEventArgs> ItemUpdated;
  73. /// <summary>
  74. /// Occurs when [item removed].
  75. /// </summary>
  76. public event EventHandler<ItemChangeEventArgs> ItemRemoved;
  77. /// <summary>
  78. /// The _logger
  79. /// </summary>
  80. private readonly ILogger _logger;
  81. /// <summary>
  82. /// The _task manager
  83. /// </summary>
  84. private readonly ITaskManager _taskManager;
  85. /// <summary>
  86. /// The _user manager
  87. /// </summary>
  88. private readonly IUserManager _userManager;
  89. private readonly IUserDataRepository _userDataRepository;
  90. /// <summary>
  91. /// Gets or sets the configuration manager.
  92. /// </summary>
  93. /// <value>The configuration manager.</value>
  94. private IServerConfigurationManager ConfigurationManager { get; set; }
  95. /// <summary>
  96. /// A collection of items that may be referenced from multiple physical places in the library
  97. /// (typically, multiple user roots). We store them here and be sure they all reference a
  98. /// single instance.
  99. /// </summary>
  100. private ConcurrentDictionary<Guid, BaseItem> ByReferenceItems { get; set; }
  101. private ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
  102. private object _libraryItemsCacheSyncLock = new object();
  103. private bool _libraryItemsCacheInitialized;
  104. private ConcurrentDictionary<Guid, BaseItem> LibraryItemsCache
  105. {
  106. get
  107. {
  108. LazyInitializer.EnsureInitialized(ref _libraryItemsCache, ref _libraryItemsCacheInitialized, ref _libraryItemsCacheSyncLock, CreateLibraryItemsCache);
  109. return _libraryItemsCache;
  110. }
  111. }
  112. private readonly ConcurrentDictionary<string, UserRootFolder> _userRootFolders =
  113. new ConcurrentDictionary<string, UserRootFolder>();
  114. /// <summary>
  115. /// Initializes a new instance of the <see cref="LibraryManager" /> class.
  116. /// </summary>
  117. /// <param name="logger">The logger.</param>
  118. /// <param name="taskManager">The task manager.</param>
  119. /// <param name="userManager">The user manager.</param>
  120. /// <param name="configurationManager">The configuration manager.</param>
  121. /// <param name="userDataRepository">The user data repository.</param>
  122. public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataRepository userDataRepository)
  123. {
  124. _logger = logger;
  125. _taskManager = taskManager;
  126. _userManager = userManager;
  127. ConfigurationManager = configurationManager;
  128. _userDataRepository = userDataRepository;
  129. ByReferenceItems = new ConcurrentDictionary<Guid, BaseItem>();
  130. ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated;
  131. RecordConfigurationValues(configurationManager.Configuration);
  132. }
  133. /// <summary>
  134. /// Adds the parts.
  135. /// </summary>
  136. /// <param name="rules">The rules.</param>
  137. /// <param name="pluginFolders">The plugin folders.</param>
  138. /// <param name="resolvers">The resolvers.</param>
  139. /// <param name="introProviders">The intro providers.</param>
  140. /// <param name="itemComparers">The item comparers.</param>
  141. public void AddParts(IEnumerable<IResolverIgnoreRule> rules,
  142. IEnumerable<IVirtualFolderCreator> pluginFolders,
  143. IEnumerable<IItemResolver> resolvers,
  144. IEnumerable<IIntroProvider> introProviders,
  145. IEnumerable<IBaseItemComparer> itemComparers)
  146. {
  147. EntityResolutionIgnoreRules = rules;
  148. PluginFolderCreators = pluginFolders;
  149. EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
  150. IntroProviders = introProviders;
  151. Comparers = itemComparers;
  152. }
  153. /// <summary>
  154. /// The _root folder
  155. /// </summary>
  156. private AggregateFolder _rootFolder;
  157. /// <summary>
  158. /// The _root folder sync lock
  159. /// </summary>
  160. private object _rootFolderSyncLock = new object();
  161. /// <summary>
  162. /// The _root folder initialized
  163. /// </summary>
  164. private bool _rootFolderInitialized;
  165. /// <summary>
  166. /// Gets the root folder.
  167. /// </summary>
  168. /// <value>The root folder.</value>
  169. public AggregateFolder RootFolder
  170. {
  171. get
  172. {
  173. LazyInitializer.EnsureInitialized(ref _rootFolder, ref _rootFolderInitialized, ref _rootFolderSyncLock, CreateRootFolder);
  174. return _rootFolder;
  175. }
  176. private set
  177. {
  178. _rootFolder = value;
  179. if (value == null)
  180. {
  181. _rootFolderInitialized = false;
  182. }
  183. }
  184. }
  185. private bool _internetProvidersEnabled;
  186. private bool _peopleImageFetchingEnabled;
  187. private string _itemsByNamePath;
  188. private void RecordConfigurationValues(ServerConfiguration configuration)
  189. {
  190. _itemsByNamePath = ConfigurationManager.ApplicationPaths.ItemsByNamePath;
  191. _internetProvidersEnabled = configuration.EnableInternetProviders;
  192. _peopleImageFetchingEnabled = configuration.InternetProviderExcludeTypes == null || !configuration.InternetProviderExcludeTypes.Contains(typeof(Person).Name, StringComparer.OrdinalIgnoreCase);
  193. }
  194. /// <summary>
  195. /// Configurations the updated.
  196. /// </summary>
  197. /// <param name="sender">The sender.</param>
  198. /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
  199. void ConfigurationUpdated(object sender, EventArgs e)
  200. {
  201. var config = ConfigurationManager.Configuration;
  202. // Figure out whether or not we should refresh people after the update is finished
  203. var refreshPeopleAfterUpdate = !_internetProvidersEnabled && config.EnableInternetProviders;
  204. // This is true if internet providers has just been turned on, or if People have just been removed from InternetProviderExcludeTypes
  205. if (!refreshPeopleAfterUpdate)
  206. {
  207. var newConfigurationFetchesPeopleImages = config.InternetProviderExcludeTypes == null || !config.InternetProviderExcludeTypes.Contains(typeof(Person).Name, StringComparer.OrdinalIgnoreCase);
  208. refreshPeopleAfterUpdate = newConfigurationFetchesPeopleImages && !_peopleImageFetchingEnabled;
  209. }
  210. var ibnPathChanged = !string.Equals(_itemsByNamePath, ConfigurationManager.ApplicationPaths.ItemsByNamePath);
  211. if (ibnPathChanged)
  212. {
  213. _itemsByName.Clear();
  214. }
  215. RecordConfigurationValues(config);
  216. Task.Run(() =>
  217. {
  218. // Any number of configuration settings could change the way the library is refreshed, so do that now
  219. _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
  220. if (refreshPeopleAfterUpdate)
  221. {
  222. _taskManager.CancelIfRunningAndQueue<PeopleValidationTask>();
  223. }
  224. });
  225. }
  226. /// <summary>
  227. /// Creates the library items cache.
  228. /// </summary>
  229. /// <returns>ConcurrentDictionary{GuidBaseItem}.</returns>
  230. private ConcurrentDictionary<Guid, BaseItem> CreateLibraryItemsCache()
  231. {
  232. var items = RootFolder.RecursiveChildren.ToList();
  233. items.Add(RootFolder);
  234. var specialFeatures = items.OfType<Movie>().SelectMany(i => i.SpecialFeatures).ToList();
  235. var localTrailers = items.SelectMany(i => i.LocalTrailers).ToList();
  236. var themeSongs = items.SelectMany(i => i.ThemeSongs).ToList();
  237. var themeVideos = items.SelectMany(i => i.ThemeVideos).ToList();
  238. items.AddRange(specialFeatures);
  239. items.AddRange(localTrailers);
  240. items.AddRange(themeSongs);
  241. items.AddRange(themeVideos);
  242. // Need to use DistinctBy Id because there could be multiple instances with the same id
  243. // due to sharing the default library
  244. var userRootFolders = _userManager.Users.Select(i => i.RootFolder)
  245. .DistinctBy(i => i.Id)
  246. .ToList();
  247. items.AddRange(userRootFolders);
  248. // Get all user collection folders
  249. var userFolders =
  250. _userManager.Users.SelectMany(i => i.RootFolder.Children)
  251. .Where(i => !(i is BasePluginFolder))
  252. .DistinctBy(i => i.Id)
  253. .ToList();
  254. items.AddRange(userFolders);
  255. return new ConcurrentDictionary<Guid, BaseItem>(items.ToDictionary(i => i.Id));
  256. }
  257. /// <summary>
  258. /// Updates the item in library cache.
  259. /// </summary>
  260. /// <param name="item">The item.</param>
  261. private void UpdateItemInLibraryCache(BaseItem item)
  262. {
  263. LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
  264. foreach (var subItem in item.LocalTrailers)
  265. {
  266. // Prevent access to foreach variable in closure
  267. var copy = subItem;
  268. LibraryItemsCache.AddOrUpdate(subItem.Id, subItem, delegate { return copy; });
  269. }
  270. foreach (var subItem in item.ThemeSongs)
  271. {
  272. // Prevent access to foreach variable in closure
  273. var copy = subItem;
  274. LibraryItemsCache.AddOrUpdate(subItem.Id, subItem, delegate { return copy; });
  275. }
  276. foreach (var subItem in item.ThemeVideos)
  277. {
  278. // Prevent access to foreach variable in closure
  279. var copy = subItem;
  280. LibraryItemsCache.AddOrUpdate(subItem.Id, subItem, delegate { return copy; });
  281. }
  282. var movie = item as Movie;
  283. if (movie != null)
  284. {
  285. foreach (var subItem in movie.SpecialFeatures)
  286. {
  287. // Prevent access to foreach variable in closure
  288. var special1 = subItem;
  289. LibraryItemsCache.AddOrUpdate(subItem.Id, subItem, delegate { return special1; });
  290. }
  291. }
  292. }
  293. /// <summary>
  294. /// Resolves the item.
  295. /// </summary>
  296. /// <param name="args">The args.</param>
  297. /// <returns>BaseItem.</returns>
  298. public BaseItem ResolveItem(ItemResolveArgs args)
  299. {
  300. var item = EntityResolvers.Select(r => r.ResolvePath(args)).FirstOrDefault(i => i != null);
  301. if (item != null)
  302. {
  303. ResolverHelper.SetInitialItemValues(item, args);
  304. // Now handle the issue with posibly having the same item referenced from multiple physical
  305. // places within the library. Be sure we always end up with just one instance.
  306. if (item is IByReferenceItem)
  307. {
  308. item = GetOrAddByReferenceItem(item);
  309. }
  310. }
  311. return item;
  312. }
  313. /// <summary>
  314. /// Ensure supplied item has only one instance throughout
  315. /// </summary>
  316. /// <param name="item"></param>
  317. /// <returns>The proper instance to the item</returns>
  318. public BaseItem GetOrAddByReferenceItem(BaseItem item)
  319. {
  320. // Add this item to our list if not there already
  321. if (!ByReferenceItems.TryAdd(item.Id, item))
  322. {
  323. // Already there - return the existing reference
  324. item = ByReferenceItems[item.Id];
  325. }
  326. return item;
  327. }
  328. /// <summary>
  329. /// Resolves a path into a BaseItem
  330. /// </summary>
  331. /// <param name="path">The path.</param>
  332. /// <param name="parent">The parent.</param>
  333. /// <param name="fileInfo">The file info.</param>
  334. /// <returns>BaseItem.</returns>
  335. /// <exception cref="System.ArgumentNullException"></exception>
  336. public BaseItem ResolvePath(string path, Folder parent = null, FileSystemInfo fileInfo = null)
  337. {
  338. if (string.IsNullOrEmpty(path))
  339. {
  340. throw new ArgumentNullException();
  341. }
  342. fileInfo = fileInfo ?? FileSystem.GetFileSystemInfo(path);
  343. if (!fileInfo.Exists)
  344. {
  345. return null;
  346. }
  347. var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths)
  348. {
  349. Parent = parent,
  350. Path = path,
  351. FileInfo = fileInfo
  352. };
  353. // Return null if ignore rules deem that we should do so
  354. if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(args)))
  355. {
  356. return null;
  357. }
  358. // Gather child folder and files
  359. if (args.IsDirectory)
  360. {
  361. var isPhysicalRoot = args.IsPhysicalRoot;
  362. // When resolving the root, we need it's grandchildren (children of user views)
  363. var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
  364. args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, _logger, flattenFolderDepth: flattenFolderDepth, args: args, resolveShortcuts: isPhysicalRoot || args.IsVf);
  365. }
  366. // Check to see if we should resolve based on our contents
  367. if (args.IsDirectory && !ShouldResolvePathContents(args))
  368. {
  369. return null;
  370. }
  371. return ResolveItem(args);
  372. }
  373. /// <summary>
  374. /// Determines whether a path should be ignored based on its contents - called after the contents have been read
  375. /// </summary>
  376. /// <param name="args">The args.</param>
  377. /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
  378. private static bool ShouldResolvePathContents(ItemResolveArgs args)
  379. {
  380. // Ignore any folders containing a file called .ignore
  381. return !args.ContainsFileSystemEntryByName(".ignore");
  382. }
  383. /// <summary>
  384. /// Resolves a set of files into a list of BaseItem
  385. /// </summary>
  386. /// <typeparam name="T"></typeparam>
  387. /// <param name="files">The files.</param>
  388. /// <param name="parent">The parent.</param>
  389. /// <returns>List{``0}.</returns>
  390. public List<T> ResolvePaths<T>(IEnumerable<FileSystemInfo> files, Folder parent)
  391. where T : BaseItem
  392. {
  393. var list = new List<T>();
  394. Parallel.ForEach(files, f =>
  395. {
  396. try
  397. {
  398. var item = ResolvePath(f.FullName, parent, f) as T;
  399. if (item != null)
  400. {
  401. lock (list)
  402. {
  403. list.Add(item);
  404. }
  405. }
  406. }
  407. catch (Exception ex)
  408. {
  409. _logger.ErrorException("Error resolving path {0}", ex, f.FullName);
  410. }
  411. });
  412. return list;
  413. }
  414. /// <summary>
  415. /// Creates the root media folder
  416. /// </summary>
  417. /// <returns>AggregateFolder.</returns>
  418. /// <exception cref="System.InvalidOperationException">Cannot create the root folder until plugins have loaded</exception>
  419. public AggregateFolder CreateRootFolder()
  420. {
  421. var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath;
  422. var rootFolder = RetrieveItem(rootFolderPath.GetMBId(typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(rootFolderPath);
  423. // Add in the plug-in folders
  424. foreach (var child in PluginFolderCreators)
  425. {
  426. var folder = child.GetFolder();
  427. rootFolder.AddVirtualChild(child.GetFolder());
  428. }
  429. return rootFolder;
  430. }
  431. /// <summary>
  432. /// Gets the user root folder.
  433. /// </summary>
  434. /// <param name="userRootPath">The user root path.</param>
  435. /// <returns>UserRootFolder.</returns>
  436. public UserRootFolder GetUserRootFolder(string userRootPath)
  437. {
  438. return _userRootFolders.GetOrAdd(userRootPath, key => RetrieveItem(userRootPath.GetMBId(typeof(UserRootFolder))) as UserRootFolder ?? (UserRootFolder)ResolvePath(userRootPath));
  439. }
  440. /// <summary>
  441. /// Gets a Person
  442. /// </summary>
  443. /// <param name="name">The name.</param>
  444. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  445. /// <returns>Task{Person}.</returns>
  446. public Task<Person> GetPerson(string name, bool allowSlowProviders = false)
  447. {
  448. return GetPerson(name, CancellationToken.None, allowSlowProviders);
  449. }
  450. /// <summary>
  451. /// Gets a Person
  452. /// </summary>
  453. /// <param name="name">The name.</param>
  454. /// <param name="cancellationToken">The cancellation token.</param>
  455. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  456. /// <param name="forceCreation">if set to <c>true</c> [force creation].</param>
  457. /// <returns>Task{Person}.</returns>
  458. private Task<Person> GetPerson(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool forceCreation = false)
  459. {
  460. return GetItemByName<Person>(ConfigurationManager.ApplicationPaths.PeoplePath, name, cancellationToken, allowSlowProviders, forceCreation);
  461. }
  462. /// <summary>
  463. /// Gets a Studio
  464. /// </summary>
  465. /// <param name="name">The name.</param>
  466. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  467. /// <returns>Task{Studio}.</returns>
  468. public Task<Studio> GetStudio(string name, bool allowSlowProviders = false)
  469. {
  470. return GetItemByName<Studio>(ConfigurationManager.ApplicationPaths.StudioPath, name, CancellationToken.None, allowSlowProviders);
  471. }
  472. /// <summary>
  473. /// Gets a Genre
  474. /// </summary>
  475. /// <param name="name">The name.</param>
  476. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  477. /// <returns>Task{Genre}.</returns>
  478. public Task<Genre> GetGenre(string name, bool allowSlowProviders = false)
  479. {
  480. return GetItemByName<Genre>(ConfigurationManager.ApplicationPaths.GenrePath, name, CancellationToken.None, allowSlowProviders);
  481. }
  482. /// <summary>
  483. /// Gets a Genre
  484. /// </summary>
  485. /// <param name="name">The name.</param>
  486. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  487. /// <returns>Task{Genre}.</returns>
  488. public Task<Artist> GetArtist(string name, bool allowSlowProviders = false)
  489. {
  490. return GetArtist(name, CancellationToken.None, allowSlowProviders);
  491. }
  492. /// <summary>
  493. /// Gets the artist.
  494. /// </summary>
  495. /// <param name="name">The name.</param>
  496. /// <param name="cancellationToken">The cancellation token.</param>
  497. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  498. /// <param name="forceCreation">if set to <c>true</c> [force creation].</param>
  499. /// <returns>Task{Artist}.</returns>
  500. private Task<Artist> GetArtist(string name, CancellationToken cancellationToken, bool allowSlowProviders = false, bool forceCreation = false)
  501. {
  502. return GetItemByName<Artist>(ConfigurationManager.ApplicationPaths.ArtistsPath, name, cancellationToken, allowSlowProviders, forceCreation);
  503. }
  504. /// <summary>
  505. /// The us culture
  506. /// </summary>
  507. private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
  508. /// <summary>
  509. /// Gets a Year
  510. /// </summary>
  511. /// <param name="value">The value.</param>
  512. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  513. /// <returns>Task{Year}.</returns>
  514. /// <exception cref="System.ArgumentOutOfRangeException"></exception>
  515. public Task<Year> GetYear(int value, bool allowSlowProviders = false)
  516. {
  517. if (value <= 0)
  518. {
  519. throw new ArgumentOutOfRangeException();
  520. }
  521. return GetItemByName<Year>(ConfigurationManager.ApplicationPaths.YearPath, value.ToString(UsCulture), CancellationToken.None, allowSlowProviders);
  522. }
  523. /// <summary>
  524. /// The images by name item cache
  525. /// </summary>
  526. private readonly ConcurrentDictionary<string, BaseItem> _itemsByName = new ConcurrentDictionary<string, BaseItem>(StringComparer.OrdinalIgnoreCase);
  527. /// <summary>
  528. /// Generically retrieves an IBN item
  529. /// </summary>
  530. /// <typeparam name="T"></typeparam>
  531. /// <param name="path">The path.</param>
  532. /// <param name="name">The name.</param>
  533. /// <param name="cancellationToken">The cancellation token.</param>
  534. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  535. /// <param name="forceCreation">if set to <c>true</c> [force creation].</param>
  536. /// <returns>Task{``0}.</returns>
  537. /// <exception cref="System.ArgumentNullException">
  538. /// </exception>
  539. private async Task<T> GetItemByName<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true, bool forceCreation = false)
  540. where T : BaseItem, new()
  541. {
  542. if (string.IsNullOrEmpty(path))
  543. {
  544. throw new ArgumentNullException();
  545. }
  546. if (string.IsNullOrEmpty(name))
  547. {
  548. throw new ArgumentNullException();
  549. }
  550. var key = Path.Combine(path, FileSystem.GetValidFilename(name));
  551. BaseItem obj;
  552. if (forceCreation || !_itemsByName.TryGetValue(key, out obj))
  553. {
  554. obj = await CreateItemByName<T>(path, name, cancellationToken, allowSlowProviders).ConfigureAwait(false);
  555. _itemsByName.AddOrUpdate(key, obj, (keyName, oldValue) => obj);
  556. }
  557. return obj as T;
  558. }
  559. /// <summary>
  560. /// Creates an IBN item based on a given path
  561. /// </summary>
  562. /// <typeparam name="T"></typeparam>
  563. /// <param name="path">The path.</param>
  564. /// <param name="name">The name.</param>
  565. /// <param name="cancellationToken">The cancellation token.</param>
  566. /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
  567. /// <returns>Task{``0}.</returns>
  568. /// <exception cref="System.IO.IOException">Path not created: + path</exception>
  569. private async Task<T> CreateItemByName<T>(string path, string name, CancellationToken cancellationToken, bool allowSlowProviders = true)
  570. where T : BaseItem, new()
  571. {
  572. cancellationToken.ThrowIfCancellationRequested();
  573. _logger.Debug("Getting {0}: {1}", typeof(T).Name, name);
  574. path = Path.Combine(path, FileSystem.GetValidFilename(name));
  575. var fileInfo = new DirectoryInfo(path);
  576. var isNew = false;
  577. if (!fileInfo.Exists)
  578. {
  579. Directory.CreateDirectory(path);
  580. fileInfo = new DirectoryInfo(path);
  581. if (!fileInfo.Exists)
  582. {
  583. throw new IOException("Path not created: " + path);
  584. }
  585. isNew = true;
  586. }
  587. cancellationToken.ThrowIfCancellationRequested();
  588. var id = path.GetMBId(typeof(T));
  589. var item = RetrieveItem(id) as T;
  590. if (item == null)
  591. {
  592. item = new T
  593. {
  594. Name = name,
  595. Id = id,
  596. DateCreated = fileInfo.CreationTimeUtc,
  597. DateModified = fileInfo.LastWriteTimeUtc,
  598. Path = path
  599. };
  600. isNew = true;
  601. }
  602. cancellationToken.ThrowIfCancellationRequested();
  603. // Set this now so we don't cause additional file system access during provider executions
  604. item.ResetResolveArgs(fileInfo);
  605. await item.RefreshMetadata(cancellationToken, isNew, allowSlowProviders: allowSlowProviders).ConfigureAwait(false);
  606. cancellationToken.ThrowIfCancellationRequested();
  607. return item;
  608. }
  609. /// <summary>
  610. /// Validate and refresh the People sub-set of the IBN.
  611. /// The items are stored in the db but not loaded into memory until actually requested by an operation.
  612. /// </summary>
  613. /// <param name="cancellationToken">The cancellation token.</param>
  614. /// <param name="progress">The progress.</param>
  615. /// <returns>Task.</returns>
  616. public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
  617. {
  618. const int maxTasks = 25;
  619. var tasks = new List<Task>();
  620. var includedPersonTypes = new[] { PersonType.Actor, PersonType.Director, PersonType.GuestStar, PersonType.Writer, PersonType.Director, PersonType.Producer };
  621. var people = RootFolder.RecursiveChildren
  622. .Where(c => c.People != null)
  623. .SelectMany(c => c.People.Where(p => includedPersonTypes.Contains(p.Type)))
  624. .DistinctBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
  625. .ToList();
  626. var numComplete = 0;
  627. foreach (var person in people)
  628. {
  629. if (tasks.Count > maxTasks)
  630. {
  631. await Task.WhenAll(tasks).ConfigureAwait(false);
  632. tasks.Clear();
  633. // Safe cancellation point, when there are no pending tasks
  634. cancellationToken.ThrowIfCancellationRequested();
  635. }
  636. // Avoid accessing the foreach variable within the closure
  637. var currentPerson = person;
  638. tasks.Add(Task.Run(async () =>
  639. {
  640. cancellationToken.ThrowIfCancellationRequested();
  641. try
  642. {
  643. await GetPerson(currentPerson.Name, cancellationToken, true, true).ConfigureAwait(false);
  644. }
  645. catch (IOException ex)
  646. {
  647. _logger.ErrorException("Error validating IBN entry {0}", ex, currentPerson.Name);
  648. }
  649. // Update progress
  650. lock (progress)
  651. {
  652. numComplete++;
  653. double percent = numComplete;
  654. percent /= people.Count;
  655. progress.Report(100 * percent);
  656. }
  657. }));
  658. }
  659. await Task.WhenAll(tasks).ConfigureAwait(false);
  660. progress.Report(100);
  661. _logger.Info("People validation complete");
  662. }
  663. public async Task ValidateArtists(CancellationToken cancellationToken, IProgress<double> progress)
  664. {
  665. const int maxTasks = 25;
  666. var tasks = new List<Task>();
  667. var artists = RootFolder.RecursiveChildren
  668. .OfType<Audio>()
  669. .SelectMany(c =>
  670. {
  671. var list = new List<string>();
  672. if (!string.IsNullOrEmpty(c.AlbumArtist))
  673. {
  674. list.Add(c.AlbumArtist);
  675. }
  676. if (!string.IsNullOrEmpty(c.Artist))
  677. {
  678. list.Add(c.Artist);
  679. }
  680. return list;
  681. })
  682. .Distinct(StringComparer.OrdinalIgnoreCase)
  683. .ToList();
  684. var numComplete = 0;
  685. foreach (var artist in artists)
  686. {
  687. if (tasks.Count > maxTasks)
  688. {
  689. await Task.WhenAll(tasks).ConfigureAwait(false);
  690. tasks.Clear();
  691. // Safe cancellation point, when there are no pending tasks
  692. cancellationToken.ThrowIfCancellationRequested();
  693. }
  694. // Avoid accessing the foreach variable within the closure
  695. var currentArtist = artist;
  696. tasks.Add(Task.Run(async () =>
  697. {
  698. cancellationToken.ThrowIfCancellationRequested();
  699. try
  700. {
  701. await GetArtist(currentArtist, cancellationToken, true, true).ConfigureAwait(false);
  702. }
  703. catch (IOException ex)
  704. {
  705. _logger.ErrorException("Error validating Artist {0}", ex, currentArtist);
  706. }
  707. // Update progress
  708. lock (progress)
  709. {
  710. numComplete++;
  711. double percent = numComplete;
  712. percent /= artists.Count;
  713. progress.Report(100 * percent);
  714. }
  715. }));
  716. }
  717. await Task.WhenAll(tasks).ConfigureAwait(false);
  718. progress.Report(100);
  719. _logger.Info("Artist validation complete");
  720. }
  721. /// <summary>
  722. /// Reloads the root media folder
  723. /// </summary>
  724. /// <param name="progress">The progress.</param>
  725. /// <param name="cancellationToken">The cancellation token.</param>
  726. /// <returns>Task.</returns>
  727. public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
  728. {
  729. // Just run the scheduled task so that the user can see it
  730. return Task.Run(() => _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>());
  731. }
  732. /// <summary>
  733. /// Validates the media library internal.
  734. /// </summary>
  735. /// <param name="progress">The progress.</param>
  736. /// <param name="cancellationToken">The cancellation token.</param>
  737. /// <returns>Task.</returns>
  738. public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
  739. {
  740. _logger.Info("Validating media library");
  741. await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
  742. // Start by just validating the children of the root, but go no further
  743. await RootFolder.ValidateChildren(new Progress<double>(), cancellationToken, recursive: false);
  744. foreach (var folder in _userManager.Users.Select(u => u.RootFolder).Distinct())
  745. {
  746. await ValidateCollectionFolders(folder, cancellationToken).ConfigureAwait(false);
  747. }
  748. var innerProgress = new ActionableProgress<double>();
  749. innerProgress.RegisterAction(pct => progress.Report(pct * .8));
  750. // Now validate the entire media library
  751. await RootFolder.ValidateChildren(innerProgress, cancellationToken, recursive: true).ConfigureAwait(false);
  752. innerProgress = new ActionableProgress<double>();
  753. innerProgress.RegisterAction(pct => progress.Report(80 + pct * .2));
  754. await ValidateArtists(cancellationToken, innerProgress);
  755. progress.Report(100);
  756. }
  757. /// <summary>
  758. /// Validates only the collection folders for a User and goes no further
  759. /// </summary>
  760. /// <param name="userRootFolder">The user root folder.</param>
  761. /// <param name="cancellationToken">The cancellation token.</param>
  762. /// <returns>Task.</returns>
  763. private async Task ValidateCollectionFolders(UserRootFolder userRootFolder, CancellationToken cancellationToken)
  764. {
  765. _logger.Info("Validating collection folders within {0}", userRootFolder.Path);
  766. await userRootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
  767. cancellationToken.ThrowIfCancellationRequested();
  768. await userRootFolder.ValidateChildren(new Progress<double>(), cancellationToken, recursive: false).ConfigureAwait(false);
  769. }
  770. /// <summary>
  771. /// Gets the default view.
  772. /// </summary>
  773. /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
  774. public IEnumerable<VirtualFolderInfo> GetDefaultVirtualFolders()
  775. {
  776. return GetView(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath);
  777. }
  778. /// <summary>
  779. /// Gets the view.
  780. /// </summary>
  781. /// <param name="user">The user.</param>
  782. /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
  783. public IEnumerable<VirtualFolderInfo> GetVirtualFolders(User user)
  784. {
  785. return GetView(user.RootFolderPath);
  786. }
  787. /// <summary>
  788. /// Gets the view.
  789. /// </summary>
  790. /// <param name="path">The path.</param>
  791. /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
  792. private IEnumerable<VirtualFolderInfo> GetView(string path)
  793. {
  794. return Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)
  795. .Select(dir => new VirtualFolderInfo
  796. {
  797. Name = Path.GetFileName(dir),
  798. Locations = Directory.EnumerateFiles(dir, "*.lnk", SearchOption.TopDirectoryOnly).Select(FileSystem.ResolveShortcut).OrderBy(i => i).ToList()
  799. });
  800. }
  801. /// <summary>
  802. /// Gets the item by id.
  803. /// </summary>
  804. /// <param name="id">The id.</param>
  805. /// <returns>BaseItem.</returns>
  806. /// <exception cref="System.ArgumentNullException">id</exception>
  807. public BaseItem GetItemById(Guid id)
  808. {
  809. if (id == Guid.Empty)
  810. {
  811. throw new ArgumentNullException("id");
  812. }
  813. BaseItem item;
  814. LibraryItemsCache.TryGetValue(id, out item);
  815. return item;
  816. }
  817. /// <summary>
  818. /// Gets the intros.
  819. /// </summary>
  820. /// <param name="item">The item.</param>
  821. /// <param name="user">The user.</param>
  822. /// <returns>IEnumerable{System.String}.</returns>
  823. public IEnumerable<string> GetIntros(BaseItem item, User user)
  824. {
  825. return IntroProviders.SelectMany(i => i.GetIntros(item, user));
  826. }
  827. /// <summary>
  828. /// Sorts the specified sort by.
  829. /// </summary>
  830. /// <param name="items">The items.</param>
  831. /// <param name="user">The user.</param>
  832. /// <param name="sortBy">The sort by.</param>
  833. /// <param name="sortOrder">The sort order.</param>
  834. /// <returns>IEnumerable{BaseItem}.</returns>
  835. public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder)
  836. {
  837. var isFirst = true;
  838. IOrderedEnumerable<BaseItem> orderedItems = null;
  839. foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c != null))
  840. {
  841. if (isFirst)
  842. {
  843. orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, orderBy) : items.OrderBy(i => i, orderBy);
  844. }
  845. else
  846. {
  847. orderedItems = sortOrder == SortOrder.Descending ? orderedItems.ThenByDescending(i => i, orderBy) : orderedItems.ThenBy(i => i, orderBy);
  848. }
  849. isFirst = false;
  850. }
  851. return orderedItems ?? items;
  852. }
  853. /// <summary>
  854. /// Gets the comparer.
  855. /// </summary>
  856. /// <param name="name">The name.</param>
  857. /// <param name="user">The user.</param>
  858. /// <returns>IBaseItemComparer.</returns>
  859. private IBaseItemComparer GetComparer(string name, User user)
  860. {
  861. var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase));
  862. if (comparer != null)
  863. {
  864. // If it requires a user, create a new one, and assign the user
  865. if (comparer is IUserBaseItemComparer)
  866. {
  867. var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType());
  868. userComparer.User = user;
  869. userComparer.UserManager = _userManager;
  870. userComparer.UserDataRepository = _userDataRepository;
  871. return userComparer;
  872. }
  873. }
  874. return comparer;
  875. }
  876. /// <summary>
  877. /// Creates the item.
  878. /// </summary>
  879. /// <param name="item">The item.</param>
  880. /// <param name="cancellationToken">The cancellation token.</param>
  881. /// <returns>Task.</returns>
  882. public async Task CreateItem(BaseItem item, CancellationToken cancellationToken)
  883. {
  884. await SaveItem(item, cancellationToken).ConfigureAwait(false);
  885. UpdateItemInLibraryCache(item);
  886. if (ItemAdded != null)
  887. {
  888. ItemAdded(this, new ItemChangeEventArgs { Item = item });
  889. }
  890. }
  891. /// <summary>
  892. /// Updates the item.
  893. /// </summary>
  894. /// <param name="item">The item.</param>
  895. /// <param name="cancellationToken">The cancellation token.</param>
  896. /// <returns>Task.</returns>
  897. public async Task UpdateItem(BaseItem item, CancellationToken cancellationToken)
  898. {
  899. await SaveItem(item, cancellationToken).ConfigureAwait(false);
  900. UpdateItemInLibraryCache(item);
  901. if (ItemUpdated != null)
  902. {
  903. ItemUpdated(this, new ItemChangeEventArgs { Item = item });
  904. }
  905. }
  906. /// <summary>
  907. /// Reports the item removed.
  908. /// </summary>
  909. /// <param name="item">The item.</param>
  910. public void ReportItemRemoved(BaseItem item)
  911. {
  912. if (ItemRemoved != null)
  913. {
  914. ItemRemoved(this, new ItemChangeEventArgs { Item = item });
  915. }
  916. }
  917. /// <summary>
  918. /// Saves the item.
  919. /// </summary>
  920. /// <param name="item">The item.</param>
  921. /// <param name="cancellationToken">The cancellation token.</param>
  922. /// <returns>Task.</returns>
  923. private Task SaveItem(BaseItem item, CancellationToken cancellationToken)
  924. {
  925. return ItemRepository.SaveItem(item, cancellationToken);
  926. }
  927. /// <summary>
  928. /// Retrieves the item.
  929. /// </summary>
  930. /// <param name="id">The id.</param>
  931. /// <returns>Task{BaseItem}.</returns>
  932. public BaseItem RetrieveItem(Guid id)
  933. {
  934. return ItemRepository.RetrieveItem(id);
  935. }
  936. /// <summary>
  937. /// Saves the children.
  938. /// </summary>
  939. /// <param name="id">The id.</param>
  940. /// <param name="children">The children.</param>
  941. /// <param name="cancellationToken">The cancellation token.</param>
  942. /// <returns>Task.</returns>
  943. public Task SaveChildren(Guid id, IEnumerable<BaseItem> children, CancellationToken cancellationToken)
  944. {
  945. return ItemRepository.SaveChildren(id, children, cancellationToken);
  946. }
  947. /// <summary>
  948. /// Retrieves the children.
  949. /// </summary>
  950. /// <param name="parent">The parent.</param>
  951. /// <returns>IEnumerable{BaseItem}.</returns>
  952. public IEnumerable<BaseItem> RetrieveChildren(Folder parent)
  953. {
  954. return ItemRepository.RetrieveChildren(parent);
  955. }
  956. }
  957. }