ProviderManager.cs 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163
  1. using MediaBrowser.Common.Net;
  2. using MediaBrowser.Controller;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Entities.Audio;
  6. using MediaBrowser.Controller.Entities.Movies;
  7. using MediaBrowser.Controller.Entities.TV;
  8. using MediaBrowser.Controller.Library;
  9. using MediaBrowser.Controller.LiveTv;
  10. using MediaBrowser.Controller.Providers;
  11. using MediaBrowser.Model.Configuration;
  12. using MediaBrowser.Model.Entities;
  13. using MediaBrowser.Model.Logging;
  14. using MediaBrowser.Model.Providers;
  15. using System;
  16. using System.Collections.Generic;
  17. using System.IO;
  18. using System.Linq;
  19. using System.Threading;
  20. using System.Threading.Tasks;
  21. using MediaBrowser.Common.Progress;
  22. using MediaBrowser.Model.IO;
  23. using MediaBrowser.Controller.Dto;
  24. using MediaBrowser.Model.Events;
  25. using MediaBrowser.Model.Serialization;
  26. using Priority_Queue;
  27. using MediaBrowser.Model.Extensions;
  28. using MediaBrowser.Controller.Subtitles;
  29. namespace MediaBrowser.Providers.Manager
  30. {
  31. /// <summary>
  32. /// Class ProviderManager
  33. /// </summary>
  34. public class ProviderManager : IProviderManager, IDisposable
  35. {
  36. /// <summary>
  37. /// The _logger
  38. /// </summary>
  39. private readonly ILogger _logger;
  40. /// <summary>
  41. /// The _HTTP client
  42. /// </summary>
  43. private readonly IHttpClient _httpClient;
  44. /// <summary>
  45. /// The _directory watchers
  46. /// </summary>
  47. private readonly ILibraryMonitor _libraryMonitor;
  48. /// <summary>
  49. /// Gets or sets the configuration manager.
  50. /// </summary>
  51. /// <value>The configuration manager.</value>
  52. private IServerConfigurationManager ConfigurationManager { get; set; }
  53. private IImageProvider[] ImageProviders { get; set; }
  54. private readonly IFileSystem _fileSystem;
  55. private IMetadataService[] _metadataServices = { };
  56. private IMetadataProvider[] _metadataProviders = { };
  57. private IEnumerable<IMetadataSaver> _savers;
  58. private readonly IServerApplicationPaths _appPaths;
  59. private readonly IJsonSerializer _json;
  60. private IExternalId[] _externalIds;
  61. private readonly Func<ILibraryManager> _libraryManagerFactory;
  62. private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
  63. public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted;
  64. public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted;
  65. public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress;
  66. private ISubtitleManager _subtitleManager;
  67. /// <summary>
  68. /// Initializes a new instance of the <see cref="ProviderManager" /> class.
  69. /// </summary>
  70. public ProviderManager(IHttpClient httpClient, ISubtitleManager subtitleManager, IServerConfigurationManager configurationManager, ILibraryMonitor libraryMonitor, ILogManager logManager, IFileSystem fileSystem, IServerApplicationPaths appPaths, Func<ILibraryManager> libraryManagerFactory, IJsonSerializer json)
  71. {
  72. _logger = logManager.GetLogger("ProviderManager");
  73. _httpClient = httpClient;
  74. ConfigurationManager = configurationManager;
  75. _libraryMonitor = libraryMonitor;
  76. _fileSystem = fileSystem;
  77. _appPaths = appPaths;
  78. _libraryManagerFactory = libraryManagerFactory;
  79. _json = json;
  80. _subtitleManager = subtitleManager;
  81. }
  82. /// <summary>
  83. /// Adds the metadata providers.
  84. /// </summary>
  85. public void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices,
  86. IEnumerable<IMetadataProvider> metadataProviders, IEnumerable<IMetadataSaver> metadataSavers,
  87. IEnumerable<IExternalId> externalIds)
  88. {
  89. ImageProviders = imageProviders.ToArray();
  90. _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray();
  91. _metadataProviders = metadataProviders.ToArray();
  92. _externalIds = externalIds.OrderBy(i => i.Name).ToArray();
  93. _savers = metadataSavers.Where(i =>
  94. {
  95. var configurable = i as IConfigurableProvider;
  96. return configurable == null || configurable.IsEnabled;
  97. }).ToArray();
  98. }
  99. public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
  100. {
  101. IMetadataService service = null;
  102. var type = item.GetType();
  103. foreach (var current in _metadataServices)
  104. {
  105. if (current.CanRefreshPrimary(type))
  106. {
  107. service = current;
  108. break;
  109. }
  110. }
  111. if (service == null)
  112. {
  113. foreach (var current in _metadataServices)
  114. {
  115. if (current.CanRefresh(item))
  116. {
  117. service = current;
  118. break;
  119. }
  120. }
  121. }
  122. if (service != null)
  123. {
  124. return service.RefreshMetadata(item, options, cancellationToken);
  125. }
  126. _logger.Error("Unable to find a metadata service for item of type " + item.GetType().Name);
  127. return Task.FromResult(ItemUpdateType.None);
  128. }
  129. public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken)
  130. {
  131. using (var response = await _httpClient.GetResponse(new HttpRequestOptions
  132. {
  133. CancellationToken = cancellationToken,
  134. Url = url,
  135. BufferContent = false
  136. }).ConfigureAwait(false))
  137. {
  138. // Workaround for tvheadend channel icons
  139. // TODO: Isolate this hack into the tvh plugin
  140. if (string.IsNullOrEmpty(response.ContentType))
  141. {
  142. if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1)
  143. {
  144. response.ContentType = "image/png";
  145. }
  146. }
  147. await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false);
  148. }
  149. }
  150. public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
  151. {
  152. return new ImageSaver(ConfigurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken);
  153. }
  154. public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
  155. {
  156. if (string.IsNullOrWhiteSpace(source))
  157. {
  158. throw new ArgumentNullException("source");
  159. }
  160. var fileStream = _fileSystem.GetFileStream(source, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, true);
  161. return new ImageSaver(ConfigurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
  162. }
  163. public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken)
  164. {
  165. var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
  166. if (!string.IsNullOrEmpty(query.ProviderName))
  167. {
  168. var providerName = query.ProviderName;
  169. providers = providers.Where(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
  170. }
  171. var preferredLanguage = item.GetPreferredMetadataLanguage();
  172. var languages = new List<string>();
  173. if (!query.IncludeAllLanguages && !string.IsNullOrWhiteSpace(preferredLanguage))
  174. {
  175. languages.Add(preferredLanguage);
  176. }
  177. var tasks = providers.Select(i => GetImages(item, cancellationToken, i, languages, query.ImageType));
  178. var results = await Task.WhenAll(tasks).ConfigureAwait(false);
  179. return results.SelectMany(i => i.ToList());
  180. }
  181. /// <summary>
  182. /// Gets the images.
  183. /// </summary>
  184. /// <param name="item">The item.</param>
  185. /// <param name="cancellationToken">The cancellation token.</param>
  186. /// <param name="provider">The provider.</param>
  187. /// <param name="preferredLanguages">The preferred languages.</param>
  188. /// <param name="type">The type.</param>
  189. /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
  190. private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken, IRemoteImageProvider provider, List<string> preferredLanguages, ImageType? type = null)
  191. {
  192. try
  193. {
  194. var result = await provider.GetImages(item, cancellationToken).ConfigureAwait(false);
  195. if (type.HasValue)
  196. {
  197. result = result.Where(i => i.Type == type.Value);
  198. }
  199. if (preferredLanguages.Count > 0)
  200. {
  201. result = result.Where(i => string.IsNullOrEmpty(i.Language) ||
  202. preferredLanguages.Contains(i.Language, StringComparer.OrdinalIgnoreCase) ||
  203. string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase));
  204. }
  205. return result;
  206. }
  207. catch (OperationCanceledException)
  208. {
  209. return new List<RemoteImageInfo>();
  210. }
  211. catch (Exception ex)
  212. {
  213. _logger.ErrorException("{0} failed in GetImageInfos for type {1}", ex, provider.GetType().Name, item.GetType().Name);
  214. return new List<RemoteImageInfo>();
  215. }
  216. }
  217. /// <summary>
  218. /// Gets the supported image providers.
  219. /// </summary>
  220. /// <param name="item">The item.</param>
  221. /// <returns>IEnumerable{IImageProvider}.</returns>
  222. public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item)
  223. {
  224. return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo
  225. {
  226. Name = i.Name,
  227. SupportedImages = i.GetSupportedImages(item).ToArray()
  228. });
  229. }
  230. public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions)
  231. {
  232. return GetImageProviders(item, _libraryManagerFactory().GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false);
  233. }
  234. private IEnumerable<IImageProvider> GetImageProviders(BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, ImageRefreshOptions refreshOptions, bool includeDisabled)
  235. {
  236. // Avoid implicitly captured closure
  237. var currentOptions = options;
  238. var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
  239. var typeFetcherOrder = typeOptions == null ? null : typeOptions.ImageFetcherOrder;
  240. return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, options, refreshOptions, includeDisabled))
  241. .OrderBy(i =>
  242. {
  243. // See if there's a user-defined order
  244. if (!(i is ILocalImageProvider))
  245. {
  246. var fetcherOrder = typeFetcherOrder ?? currentOptions.ImageFetcherOrder;
  247. var index = Array.IndexOf(fetcherOrder, i.Name);
  248. if (index != -1)
  249. {
  250. return index;
  251. }
  252. }
  253. // Not configured. Just return some high number to put it at the end.
  254. return 100;
  255. })
  256. .ThenBy(GetOrder);
  257. }
  258. public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
  259. where T : BaseItem
  260. {
  261. var globalMetadataOptions = GetMetadataOptions(item);
  262. return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
  263. }
  264. private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
  265. where T : BaseItem
  266. {
  267. // Avoid implicitly captured closure
  268. var currentOptions = globalMetadataOptions;
  269. return _metadataProviders.OfType<IMetadataProvider<T>>()
  270. .Where(i => CanRefresh(i, item, libraryOptions, currentOptions, includeDisabled, forceEnableInternetMetadata))
  271. .OrderBy(i => GetConfiguredOrder(item, i, libraryOptions, globalMetadataOptions))
  272. .ThenBy(GetDefaultOrder);
  273. }
  274. private IEnumerable<IRemoteImageProvider> GetRemoteImageProviders(BaseItem item, bool includeDisabled)
  275. {
  276. var options = GetMetadataOptions(item);
  277. var libraryOptions = _libraryManagerFactory().GetLibraryOptions(item);
  278. return GetImageProviders(item, libraryOptions, options, new ImageRefreshOptions(new DirectoryService(_logger, _fileSystem)), includeDisabled).OfType<IRemoteImageProvider>();
  279. }
  280. private bool CanRefresh(IMetadataProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, bool includeDisabled, bool forceEnableInternetMetadata)
  281. {
  282. if (!includeDisabled)
  283. {
  284. // If locked only allow local providers
  285. if (item.IsLocked && !(provider is ILocalMetadataProvider) && !(provider is IForcedProvider))
  286. {
  287. return false;
  288. }
  289. if (provider is IRemoteMetadataProvider)
  290. {
  291. if (!forceEnableInternetMetadata && !item.IsMetadataFetcherEnabled(libraryOptions, provider.Name))
  292. {
  293. return false;
  294. }
  295. }
  296. }
  297. if (!item.SupportsLocalMetadata && provider is ILocalMetadataProvider)
  298. {
  299. return false;
  300. }
  301. // If this restriction is ever lifted, movie xml providers will have to be updated to prevent owned items like trailers from reading those files
  302. if (!item.OwnerId.Equals(Guid.Empty))
  303. {
  304. if (provider is ILocalMetadataProvider || provider is IRemoteMetadataProvider)
  305. {
  306. return false;
  307. }
  308. }
  309. return true;
  310. }
  311. private bool CanRefresh(IImageProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, ImageRefreshOptions refreshOptions, bool includeDisabled)
  312. {
  313. if (!includeDisabled)
  314. {
  315. // If locked only allow local providers
  316. if (item.IsLocked && !(provider is ILocalImageProvider))
  317. {
  318. if (refreshOptions.ImageRefreshMode != MetadataRefreshMode.FullRefresh)
  319. {
  320. return false;
  321. }
  322. }
  323. if (provider is IRemoteImageProvider || provider is IDynamicImageProvider)
  324. {
  325. if (!item.IsImageFetcherEnabled(libraryOptions, provider.Name))
  326. {
  327. return false;
  328. }
  329. }
  330. }
  331. try
  332. {
  333. return provider.Supports(item);
  334. }
  335. catch (Exception ex)
  336. {
  337. _logger.ErrorException("{0} failed in Supports for type {1}", ex, provider.GetType().Name, item.GetType().Name);
  338. return false;
  339. }
  340. }
  341. /// <summary>
  342. /// Gets the order.
  343. /// </summary>
  344. /// <param name="provider">The provider.</param>
  345. /// <returns>System.Int32.</returns>
  346. private int GetOrder(IImageProvider provider)
  347. {
  348. var hasOrder = provider as IHasOrder;
  349. if (hasOrder == null)
  350. {
  351. return 0;
  352. }
  353. return hasOrder.Order;
  354. }
  355. private int GetConfiguredOrder(BaseItem item, IMetadataProvider provider, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions)
  356. {
  357. // See if there's a user-defined order
  358. if (provider is ILocalMetadataProvider)
  359. {
  360. var configuredOrder = libraryOptions.LocalMetadataReaderOrder ?? globalMetadataOptions.LocalMetadataReaderOrder;
  361. var index = Array.IndexOf(configuredOrder, provider.Name);
  362. if (index != -1)
  363. {
  364. return index;
  365. }
  366. }
  367. // See if there's a user-defined order
  368. if (provider is IRemoteMetadataProvider)
  369. {
  370. var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
  371. var typeFetcherOrder = typeOptions == null ? null : typeOptions.MetadataFetcherOrder;
  372. var fetcherOrder = typeFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder;
  373. var index = Array.IndexOf(fetcherOrder, provider.Name);
  374. if (index != -1)
  375. {
  376. return index;
  377. }
  378. }
  379. // Not configured. Just return some high number to put it at the end.
  380. return 100;
  381. }
  382. private int GetDefaultOrder(IMetadataProvider provider)
  383. {
  384. var hasOrder = provider as IHasOrder;
  385. if (hasOrder != null)
  386. {
  387. return hasOrder.Order;
  388. }
  389. return 0;
  390. }
  391. public MetadataPluginSummary[] GetAllMetadataPlugins()
  392. {
  393. return new MetadataPluginSummary[]
  394. {
  395. GetPluginSummary<Game>(),
  396. GetPluginSummary<GameSystem>(),
  397. GetPluginSummary<Movie>(),
  398. GetPluginSummary<BoxSet>(),
  399. GetPluginSummary<Book>(),
  400. GetPluginSummary<Series>(),
  401. GetPluginSummary<Season>(),
  402. GetPluginSummary<Episode>(),
  403. GetPluginSummary<MusicAlbum>(),
  404. GetPluginSummary<MusicArtist>(),
  405. GetPluginSummary<Audio>(),
  406. GetPluginSummary<AudioBook>(),
  407. GetPluginSummary<Studio>(),
  408. GetPluginSummary<MusicVideo>(),
  409. GetPluginSummary<Video>()
  410. };
  411. }
  412. private MetadataPluginSummary GetPluginSummary<T>()
  413. where T : BaseItem, new()
  414. {
  415. // Give it a dummy path just so that it looks like a file system item
  416. var dummy = new T()
  417. {
  418. Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
  419. ParentId = Guid.NewGuid()
  420. };
  421. var options = GetMetadataOptions(dummy);
  422. var summary = new MetadataPluginSummary
  423. {
  424. ItemType = typeof(T).Name
  425. };
  426. var libraryOptions = new LibraryOptions();
  427. var imageProviders = GetImageProviders(dummy, libraryOptions, options, new ImageRefreshOptions(new DirectoryService(_logger, _fileSystem)), true).ToList();
  428. var pluginList = summary.Plugins.ToList();
  429. AddMetadataPlugins(pluginList, dummy, libraryOptions, options);
  430. AddImagePlugins(pluginList, dummy, imageProviders);
  431. var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy);
  432. // Subtitle fetchers
  433. pluginList.AddRange(subtitleProviders.Select(i => new MetadataPlugin
  434. {
  435. Name = i.Name,
  436. Type = MetadataPluginType.SubtitleFetcher
  437. }));
  438. summary.Plugins = pluginList.ToArray();
  439. var supportedImageTypes = imageProviders.OfType<IRemoteImageProvider>()
  440. .SelectMany(i => i.GetSupportedImages(dummy))
  441. .ToList();
  442. supportedImageTypes.AddRange(imageProviders.OfType<IDynamicImageProvider>()
  443. .SelectMany(i => i.GetSupportedImages(dummy)));
  444. summary.SupportedImageTypes = supportedImageTypes.Distinct().ToArray();
  445. return summary;
  446. }
  447. private void AddMetadataPlugins<T>(List<MetadataPlugin> list, T item, LibraryOptions libraryOptions, MetadataOptions options)
  448. where T : BaseItem
  449. {
  450. var providers = GetMetadataProvidersInternal<T>(item, libraryOptions, options, true, true).ToList();
  451. // Locals
  452. list.AddRange(providers.Where(i => (i is ILocalMetadataProvider)).Select(i => new MetadataPlugin
  453. {
  454. Name = i.Name,
  455. Type = MetadataPluginType.LocalMetadataProvider
  456. }));
  457. // Fetchers
  458. list.AddRange(providers.Where(i => (i is IRemoteMetadataProvider)).Select(i => new MetadataPlugin
  459. {
  460. Name = i.Name,
  461. Type = MetadataPluginType.MetadataFetcher
  462. }));
  463. // Savers
  464. list.AddRange(_savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, true)).OrderBy(i => i.Name).Select(i => new MetadataPlugin
  465. {
  466. Name = i.Name,
  467. Type = MetadataPluginType.MetadataSaver
  468. }));
  469. }
  470. private void AddImagePlugins<T>(List<MetadataPlugin> list, T item, List<IImageProvider> imageProviders)
  471. where T : BaseItem
  472. {
  473. // Locals
  474. list.AddRange(imageProviders.Where(i => (i is ILocalImageProvider)).Select(i => new MetadataPlugin
  475. {
  476. Name = i.Name,
  477. Type = MetadataPluginType.LocalImageProvider
  478. }));
  479. // Fetchers
  480. list.AddRange(imageProviders.Where(i => i is IDynamicImageProvider || (i is IRemoteImageProvider)).Select(i => new MetadataPlugin
  481. {
  482. Name = i.Name,
  483. Type = MetadataPluginType.ImageFetcher
  484. }));
  485. }
  486. public MetadataOptions GetMetadataOptions(BaseItem item)
  487. {
  488. var type = item.GetType().Name;
  489. return ConfigurationManager.Configuration.MetadataOptions
  490. .FirstOrDefault(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) ??
  491. new MetadataOptions();
  492. }
  493. /// <summary>
  494. /// Saves the metadata.
  495. /// </summary>
  496. public void SaveMetadata(BaseItem item, ItemUpdateType updateType)
  497. {
  498. SaveMetadata(item, updateType, _savers);
  499. }
  500. /// <summary>
  501. /// Saves the metadata.
  502. /// </summary>
  503. public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
  504. {
  505. SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)));
  506. }
  507. /// <summary>
  508. /// Saves the metadata.
  509. /// </summary>
  510. /// <param name="item">The item.</param>
  511. /// <param name="updateType">Type of the update.</param>
  512. /// <param name="savers">The savers.</param>
  513. /// <returns>Task.</returns>
  514. private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
  515. {
  516. var libraryOptions = _libraryManagerFactory().GetLibraryOptions(item);
  517. foreach (var saver in savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false)))
  518. {
  519. _logger.Debug("Saving {0} to {1}.", item.Path ?? item.Name, saver.Name);
  520. var fileSaver = saver as IMetadataFileSaver;
  521. if (fileSaver != null)
  522. {
  523. string path = null;
  524. try
  525. {
  526. path = fileSaver.GetSavePath(item);
  527. }
  528. catch (Exception ex)
  529. {
  530. _logger.ErrorException("Error in {0} GetSavePath", ex, saver.Name);
  531. continue;
  532. }
  533. try
  534. {
  535. _libraryMonitor.ReportFileSystemChangeBeginning(path);
  536. saver.Save(item, CancellationToken.None);
  537. }
  538. catch (Exception ex)
  539. {
  540. _logger.ErrorException("Error in metadata saver", ex);
  541. }
  542. finally
  543. {
  544. _libraryMonitor.ReportFileSystemChangeComplete(path, false);
  545. }
  546. }
  547. else
  548. {
  549. try
  550. {
  551. saver.Save(item, CancellationToken.None);
  552. }
  553. catch (Exception ex)
  554. {
  555. _logger.ErrorException("Error in metadata saver", ex);
  556. }
  557. }
  558. }
  559. }
  560. /// <summary>
  561. /// Determines whether [is saver enabled for item] [the specified saver].
  562. /// </summary>
  563. private bool IsSaverEnabledForItem(IMetadataSaver saver, BaseItem item, LibraryOptions libraryOptions, ItemUpdateType updateType, bool includeDisabled)
  564. {
  565. var options = GetMetadataOptions(item);
  566. try
  567. {
  568. if (!saver.IsEnabledFor(item, updateType))
  569. {
  570. return false;
  571. }
  572. if (!includeDisabled)
  573. {
  574. if (libraryOptions.MetadataSavers == null)
  575. {
  576. if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparer.OrdinalIgnoreCase))
  577. {
  578. return false;
  579. }
  580. if (!item.IsSaveLocalMetadataEnabled())
  581. {
  582. if (updateType >= ItemUpdateType.MetadataEdit)
  583. {
  584. var fileSaver = saver as IMetadataFileSaver;
  585. // Manual edit occurred
  586. // Even if save local is off, save locally anyway if the metadata file already exists
  587. if (fileSaver == null || !_fileSystem.FileExists(fileSaver.GetSavePath(item)))
  588. {
  589. return false;
  590. }
  591. }
  592. else
  593. {
  594. // Manual edit did not occur
  595. // Since local metadata saving is disabled, consider it disabled
  596. return false;
  597. }
  598. }
  599. }
  600. else
  601. {
  602. if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparer.OrdinalIgnoreCase))
  603. {
  604. return false;
  605. }
  606. }
  607. }
  608. return true;
  609. }
  610. catch (Exception ex)
  611. {
  612. _logger.ErrorException("Error in {0}.IsEnabledFor", ex, saver.Name);
  613. return false;
  614. }
  615. }
  616. public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken)
  617. where TItemType : BaseItem, new()
  618. where TLookupType : ItemLookupInfo
  619. {
  620. BaseItem referenceItem = null;
  621. if (!searchInfo.ItemId.Equals(Guid.Empty))
  622. {
  623. referenceItem = _libraryManagerFactory().GetItemById(searchInfo.ItemId);
  624. }
  625. return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken);
  626. }
  627. public async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken)
  628. where TItemType : BaseItem, new()
  629. where TLookupType : ItemLookupInfo
  630. {
  631. LibraryOptions libraryOptions;
  632. if (referenceItem == null)
  633. {
  634. // Give it a dummy path just so that it looks like a file system item
  635. var dummy = new TItemType
  636. {
  637. Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
  638. ParentId = Guid.NewGuid()
  639. };
  640. dummy.SetParent(new Folder());
  641. referenceItem = dummy;
  642. libraryOptions = new LibraryOptions();
  643. }
  644. else
  645. {
  646. libraryOptions = _libraryManagerFactory().GetLibraryOptions(referenceItem);
  647. }
  648. var options = GetMetadataOptions(referenceItem);
  649. var providers = GetMetadataProvidersInternal<TItemType>(referenceItem, libraryOptions, options, searchInfo.IncludeDisabledProviders, false)
  650. .OfType<IRemoteSearchProvider<TLookupType>>();
  651. if (!string.IsNullOrEmpty(searchInfo.SearchProviderName))
  652. {
  653. providers = providers.Where(i => string.Equals(i.Name, searchInfo.SearchProviderName, StringComparison.OrdinalIgnoreCase));
  654. }
  655. if (string.IsNullOrWhiteSpace(searchInfo.SearchInfo.MetadataLanguage))
  656. {
  657. searchInfo.SearchInfo.MetadataLanguage = ConfigurationManager.Configuration.PreferredMetadataLanguage;
  658. }
  659. if (string.IsNullOrWhiteSpace(searchInfo.SearchInfo.MetadataCountryCode))
  660. {
  661. searchInfo.SearchInfo.MetadataCountryCode = ConfigurationManager.Configuration.MetadataCountryCode;
  662. }
  663. var resultList = new List<RemoteSearchResult>();
  664. foreach (var provider in providers)
  665. {
  666. try
  667. {
  668. var results = await GetSearchResults(provider, searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false);
  669. foreach (var result in results)
  670. {
  671. var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.GetProviderId(p.Key), p.Value, StringComparison.OrdinalIgnoreCase)));
  672. if (existingMatch == null)
  673. {
  674. resultList.Add(result);
  675. }
  676. else
  677. {
  678. foreach (var providerId in result.ProviderIds)
  679. {
  680. if (!existingMatch.ProviderIds.ContainsKey(providerId.Key))
  681. {
  682. existingMatch.ProviderIds.Add(providerId.Key, providerId.Value);
  683. }
  684. }
  685. if (string.IsNullOrWhiteSpace(existingMatch.ImageUrl))
  686. {
  687. existingMatch.ImageUrl = result.ImageUrl;
  688. }
  689. }
  690. }
  691. }
  692. catch (Exception ex)
  693. {
  694. // Logged at lower levels
  695. }
  696. }
  697. //_logger.Debug("Returning search results {0}", _json.SerializeToString(resultList));
  698. return resultList;
  699. }
  700. private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(IRemoteSearchProvider<TLookupType> provider, TLookupType searchInfo,
  701. CancellationToken cancellationToken)
  702. where TLookupType : ItemLookupInfo
  703. {
  704. var results = await provider.GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
  705. var list = results.ToList();
  706. foreach (var item in list)
  707. {
  708. item.SearchProviderName = provider.Name;
  709. }
  710. return list;
  711. }
  712. public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken)
  713. {
  714. var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
  715. if (provider == null)
  716. {
  717. throw new ArgumentException("Search provider not found.");
  718. }
  719. return provider.GetImageResponse(url, cancellationToken);
  720. }
  721. public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
  722. {
  723. return _externalIds.Where(i =>
  724. {
  725. try
  726. {
  727. return i.Supports(item);
  728. }
  729. catch (Exception ex)
  730. {
  731. _logger.ErrorException("Error in {0}.Suports", ex, i.GetType().Name);
  732. return false;
  733. }
  734. });
  735. }
  736. public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
  737. {
  738. return GetExternalIds(item)
  739. .Select(i =>
  740. {
  741. if (string.IsNullOrEmpty(i.UrlFormatString))
  742. {
  743. return null;
  744. }
  745. var value = item.GetProviderId(i.Key);
  746. if (string.IsNullOrEmpty(value))
  747. {
  748. return null;
  749. }
  750. return new ExternalUrl
  751. {
  752. Name = i.Name,
  753. Url = string.Format(i.UrlFormatString, value)
  754. };
  755. }).Where(i => i != null).Concat(item.GetRelatedUrls());
  756. }
  757. public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
  758. {
  759. return GetExternalIds(item)
  760. .Select(i => new ExternalIdInfo
  761. {
  762. Name = i.Name,
  763. Key = i.Key,
  764. UrlFormatString = i.UrlFormatString
  765. });
  766. }
  767. private Dictionary<Guid, double> _activeRefreshes = new Dictionary<Guid, double>();
  768. public Dictionary<Guid, Guid> GetRefreshQueue()
  769. {
  770. lock (_refreshQueueLock)
  771. {
  772. var dict = new Dictionary<Guid, Guid>();
  773. foreach (var item in _refreshQueue)
  774. {
  775. dict[item.Item1] = item.Item1;
  776. }
  777. return dict;
  778. }
  779. }
  780. public void OnRefreshStart(BaseItem item)
  781. {
  782. //_logger.Info("OnRefreshStart {0}", item.Id.ToString("N"));
  783. var id = item.Id;
  784. lock (_activeRefreshes)
  785. {
  786. _activeRefreshes[id] = 0;
  787. }
  788. if (RefreshStarted != null)
  789. {
  790. RefreshStarted(this, new GenericEventArgs<BaseItem>(item));
  791. }
  792. }
  793. public void OnRefreshComplete(BaseItem item)
  794. {
  795. //_logger.Info("OnRefreshComplete {0}", item.Id.ToString("N"));
  796. lock (_activeRefreshes)
  797. {
  798. _activeRefreshes.Remove(item.Id);
  799. }
  800. if (RefreshCompleted != null)
  801. {
  802. RefreshCompleted(this, new GenericEventArgs<BaseItem>(item));
  803. }
  804. }
  805. public double? GetRefreshProgress(Guid id)
  806. {
  807. lock (_activeRefreshes)
  808. {
  809. double value;
  810. if (_activeRefreshes.TryGetValue(id, out value))
  811. {
  812. return value;
  813. }
  814. return null;
  815. }
  816. }
  817. public void OnRefreshProgress(BaseItem item, double progress)
  818. {
  819. //_logger.Info("OnRefreshProgress {0} {1}", item.Id.ToString("N"), progress);
  820. var id = item.Id;
  821. lock (_activeRefreshes)
  822. {
  823. if (_activeRefreshes.ContainsKey(id))
  824. {
  825. _activeRefreshes[id] = progress;
  826. if (RefreshProgress != null)
  827. {
  828. RefreshProgress(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress)));
  829. }
  830. }
  831. else
  832. {
  833. // TODO: Need to hunt down the conditions for this happening
  834. //throw new Exception(string.Format("Refresh for item {0} {1} is not in progress", item.GetType().Name, item.Id.ToString("N")));
  835. }
  836. }
  837. }
  838. private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue =
  839. new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>();
  840. private readonly object _refreshQueueLock = new object();
  841. private bool _isProcessingRefreshQueue;
  842. public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority)
  843. {
  844. if (_disposed)
  845. {
  846. return;
  847. }
  848. _refreshQueue.Enqueue(new Tuple<Guid, MetadataRefreshOptions>(id, options), (int)priority);
  849. lock (_refreshQueueLock)
  850. {
  851. if (!_isProcessingRefreshQueue)
  852. {
  853. _isProcessingRefreshQueue = true;
  854. Task.Run(StartProcessingRefreshQueue);
  855. }
  856. }
  857. }
  858. private async Task StartProcessingRefreshQueue()
  859. {
  860. Tuple<Guid, MetadataRefreshOptions> refreshItem;
  861. var libraryManager = _libraryManagerFactory();
  862. if (_disposed)
  863. {
  864. return;
  865. }
  866. var cancellationToken = _disposeCancellationTokenSource.Token;
  867. while (_refreshQueue.TryDequeue(out refreshItem))
  868. {
  869. if (_disposed)
  870. {
  871. return;
  872. }
  873. try
  874. {
  875. var item = libraryManager.GetItemById(refreshItem.Item1);
  876. if (item != null)
  877. {
  878. // Try to throttle this a little bit.
  879. await Task.Delay(100).ConfigureAwait(false);
  880. var artist = item as MusicArtist;
  881. var task = artist == null
  882. ? RefreshItem(item, refreshItem.Item2, cancellationToken)
  883. : RefreshArtist(artist, refreshItem.Item2, cancellationToken);
  884. await task.ConfigureAwait(false);
  885. }
  886. }
  887. catch (OperationCanceledException)
  888. {
  889. break;
  890. }
  891. catch (Exception ex)
  892. {
  893. _logger.ErrorException("Error refreshing item", ex);
  894. }
  895. }
  896. lock (_refreshQueueLock)
  897. {
  898. _isProcessingRefreshQueue = false;
  899. }
  900. }
  901. private async Task RefreshItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
  902. {
  903. await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
  904. // Collection folders don't validate their children so we'll have to simulate that here
  905. var collectionFolder = item as CollectionFolder;
  906. if (collectionFolder != null)
  907. {
  908. await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false);
  909. }
  910. else
  911. {
  912. var folder = item as Folder;
  913. if (folder != null)
  914. {
  915. await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false);
  916. }
  917. }
  918. }
  919. private async Task RefreshCollectionFolderChildren(MetadataRefreshOptions options, CollectionFolder collectionFolder, CancellationToken cancellationToken)
  920. {
  921. foreach (var child in collectionFolder.GetPhysicalFolders().ToList())
  922. {
  923. await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
  924. if (child.IsFolder)
  925. {
  926. var folder = (Folder)child;
  927. await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true).ConfigureAwait(false);
  928. }
  929. }
  930. }
  931. private async Task RefreshArtist(MusicArtist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
  932. {
  933. var albums = _libraryManagerFactory()
  934. .GetItemList(new InternalItemsQuery
  935. {
  936. IncludeItemTypes = new[] { typeof(MusicAlbum).Name },
  937. ArtistIds = new[] { item.Id },
  938. DtoOptions = new DtoOptions(false)
  939. {
  940. EnableImages = false
  941. }
  942. })
  943. .OfType<MusicAlbum>()
  944. .ToList();
  945. var musicArtists = albums
  946. .Select(i => i.MusicArtist)
  947. .Where(i => i != null)
  948. .ToList();
  949. var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true));
  950. await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
  951. try
  952. {
  953. await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
  954. }
  955. catch (Exception ex)
  956. {
  957. _logger.ErrorException("Error refreshing library", ex);
  958. }
  959. }
  960. public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options,
  961. CancellationToken cancellationToken)
  962. {
  963. return RefreshItem(item, options, cancellationToken);
  964. }
  965. private bool _disposed;
  966. public void Dispose()
  967. {
  968. _disposed = true;
  969. if (!_disposeCancellationTokenSource.IsCancellationRequested)
  970. {
  971. _disposeCancellationTokenSource.Cancel();
  972. }
  973. }
  974. }
  975. }