diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-01-28 13:37:01 -0500 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-01-28 13:37:01 -0500 |
| commit | ad82c9f5e95e2b1f94ba7adda047dbfbc38004ea (patch) | |
| tree | 52ab08873cb353faff048edecf8d97d49114b4db /MediaBrowser.Providers/Manager | |
| parent | d748967c5de99ba20a788c2448b066fc3fc4fecb (diff) | |
New provider system. Only for people right now
Diffstat (limited to 'MediaBrowser.Providers/Manager')
| -rw-r--r-- | MediaBrowser.Providers/Manager/ImageSaver.cs | 598 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Manager/ItemImageProvider.cs | 431 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Manager/MetadataService.cs | 367 | ||||
| -rw-r--r-- | MediaBrowser.Providers/Manager/ProviderManager.cs | 487 |
4 files changed, 1883 insertions, 0 deletions
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs new file mode 100644 index 0000000000..a75ce88aeb --- /dev/null +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -0,0 +1,598 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Manager +{ + /// <summary> + /// Class ImageSaver + /// </summary> + public class ImageSaver + { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// <summary> + /// The _config + /// </summary> + private readonly IServerConfigurationManager _config; + + /// <summary> + /// The remote image cache + /// </summary> + private readonly FileSystemRepository _remoteImageCache; + /// <summary> + /// The _directory watchers + /// </summary> + private readonly IDirectoryWatchers _directoryWatchers; + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="ImageSaver"/> class. + /// </summary> + /// <param name="config">The config.</param> + /// <param name="directoryWatchers">The directory watchers.</param> + public ImageSaver(IServerConfigurationManager config, IDirectoryWatchers directoryWatchers, IFileSystem fileSystem, ILogger logger) + { + _config = config; + _directoryWatchers = directoryWatchers; + _fileSystem = fileSystem; + _logger = logger; + _remoteImageCache = new FileSystemRepository(config.ApplicationPaths.DownloadedImagesDataPath); + } + + /// <summary> + /// Saves the image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="source">The source.</param> + /// <param name="mimeType">Type of the MIME.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="sourceUrl">The source URL.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException">mimeType</exception> + public async Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, string sourceUrl, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(mimeType)) + { + throw new ArgumentNullException("mimeType"); + } + + var saveLocally = item.IsSaveLocalMetadataEnabled() && item.Parent != null && !(item is Audio); + + if (item is IItemByName || item is User) + { + saveLocally = true; + } + + if (type != ImageType.Primary && item is Episode) + { + saveLocally = false; + } + + var locationType = item.LocationType; + if (locationType == LocationType.Remote || locationType == LocationType.Virtual) + { + saveLocally = false; + + var season = item as Season; + + // If season is virtual under a physical series, save locally if using compatible convention + if (season != null && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible) + { + var series = season.Series; + + if (series != null) + { + var seriesLocationType = series.LocationType; + if (seriesLocationType == LocationType.FileSystem || seriesLocationType == LocationType.Offline) + { + saveLocally = true; + } + } + } + } + + if (type == ImageType.Backdrop && imageIndex == null) + { + imageIndex = item.BackdropImagePaths.Count; + } + else if (type == ImageType.Screenshot && imageIndex == null) + { + var hasScreenshots = (IHasScreenshots)item; + imageIndex = hasScreenshots.ScreenshotImagePaths.Count; + } + + var index = imageIndex ?? 0; + + var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally); + + // If there are more than one output paths, the stream will need to be seekable + if (paths.Length > 1 && !source.CanSeek) + { + var memoryStream = new MemoryStream(); + using (source) + { + await source.CopyToAsync(memoryStream).ConfigureAwait(false); + } + memoryStream.Position = 0; + source = memoryStream; + } + + var currentPath = GetCurrentImagePath(item, type, index); + + using (source) + { + var isFirst = true; + + foreach (var path in paths) + { + // Seek back to the beginning + if (!isFirst) + { + source.Position = 0; + } + + await SaveImageToLocation(source, path, cancellationToken).ConfigureAwait(false); + + isFirst = false; + } + } + + // Set the path into the item + SetImagePath(item, type, imageIndex, paths[0], sourceUrl); + + // Delete the current path + if (!string.IsNullOrEmpty(currentPath) && !paths.Contains(currentPath, StringComparer.OrdinalIgnoreCase)) + { + _directoryWatchers.TemporarilyIgnore(currentPath); + + try + { + var currentFile = new FileInfo(currentPath); + + // This will fail if the file is hidden + if (currentFile.Exists) + { + if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) + { + currentFile.Attributes &= ~FileAttributes.Hidden; + } + + currentFile.Delete(); + } + } + finally + { + _directoryWatchers.RemoveTempIgnore(currentPath); + } + } + } + + /// <summary> + /// Saves the image to location. + /// </summary> + /// <param name="source">The source.</param> + /// <param name="path">The path.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task SaveImageToLocation(Stream source, string path, CancellationToken cancellationToken) + { + _logger.Debug("Saving image to {0}", path); + + var parentFolder = Path.GetDirectoryName(path); + + _directoryWatchers.TemporarilyIgnore(path); + _directoryWatchers.TemporarilyIgnore(parentFolder); + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + // If the file is currently hidden we'll have to remove that or the save will fail + var file = new FileInfo(path); + + // This will fail if the file is hidden + if (file.Exists) + { + if ((file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) + { + file.Attributes &= ~FileAttributes.Hidden; + } + } + + using (var fs = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true)) + { + await source.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } + finally + { + _directoryWatchers.RemoveTempIgnore(path); + _directoryWatchers.RemoveTempIgnore(parentFolder); + } + } + + /// <summary> + /// Gets the save paths. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="mimeType">Type of the MIME.</param> + /// <param name="saveLocally">if set to <c>true</c> [save locally].</param> + /// <returns>IEnumerable{System.String}.</returns> + private string[] GetSavePaths(BaseItem item, ImageType type, int? imageIndex, string mimeType, bool saveLocally) + { + if (_config.Configuration.ImageSavingConvention == ImageSavingConvention.Legacy || !saveLocally) + { + return new[] { GetStandardSavePath(item, type, imageIndex, mimeType, saveLocally) }; + } + + return GetCompatibleSavePaths(item, type, imageIndex, mimeType); + } + + /// <summary> + /// Gets the current image path. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <returns>System.String.</returns> + /// <exception cref="System.ArgumentNullException"> + /// imageIndex + /// or + /// imageIndex + /// </exception> + private string GetCurrentImagePath(IHasImages item, ImageType type, int imageIndex) + { + return item.GetImagePath(type, imageIndex); + } + + /// <summary> + /// Sets the image path. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="path">The path.</param> + /// <param name="sourceUrl">The source URL.</param> + /// <exception cref="System.ArgumentNullException">imageIndex + /// or + /// imageIndex</exception> + private void SetImagePath(BaseItem item, ImageType type, int? imageIndex, string path, string sourceUrl) + { + switch (type) + { + case ImageType.Screenshot: + + if (!imageIndex.HasValue) + { + throw new ArgumentNullException("imageIndex"); + } + + var hasScreenshots = (IHasScreenshots)item; + if (hasScreenshots.ScreenshotImagePaths.Count > imageIndex.Value) + { + hasScreenshots.ScreenshotImagePaths[imageIndex.Value] = path; + } + else if (!hasScreenshots.ScreenshotImagePaths.Contains(path, StringComparer.OrdinalIgnoreCase)) + { + hasScreenshots.ScreenshotImagePaths.Add(path); + } + break; + case ImageType.Backdrop: + if (!imageIndex.HasValue) + { + throw new ArgumentNullException("imageIndex"); + } + if (item.BackdropImagePaths.Count > imageIndex.Value) + { + item.BackdropImagePaths[imageIndex.Value] = path; + } + else if (!item.BackdropImagePaths.Contains(path, StringComparer.OrdinalIgnoreCase)) + { + item.BackdropImagePaths.Add(path); + } + + if (string.IsNullOrEmpty(sourceUrl)) + { + item.RemoveImageSourceForPath(path); + } + else + { + item.AddImageSource(path, sourceUrl); + } + break; + default: + item.SetImagePath(type, path); + break; + } + } + + /// <summary> + /// Gets the save path. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="mimeType">Type of the MIME.</param> + /// <param name="saveLocally">if set to <c>true</c> [save locally].</param> + /// <returns>System.String.</returns> + /// <exception cref="System.ArgumentNullException"> + /// imageIndex + /// or + /// imageIndex + /// </exception> + private string GetStandardSavePath(BaseItem item, ImageType type, int? imageIndex, string mimeType, bool saveLocally) + { + string filename; + + switch (type) + { + case ImageType.Art: + filename = "clearart"; + break; + case ImageType.Disc: + filename = item is MusicAlbum ? "cdart" : "disc"; + break; + case ImageType.Primary: + filename = item is Episode ? Path.GetFileNameWithoutExtension(item.Path) : "folder"; + break; + case ImageType.Backdrop: + if (!imageIndex.HasValue) + { + throw new ArgumentNullException("imageIndex"); + } + filename = GetBackdropSaveFilename(item.BackdropImagePaths, "backdrop", "backdrop", imageIndex.Value); + break; + case ImageType.Screenshot: + if (!imageIndex.HasValue) + { + throw new ArgumentNullException("imageIndex"); + } + var hasScreenshots = (IHasScreenshots)item; + filename = GetBackdropSaveFilename(hasScreenshots.ScreenshotImagePaths, "screenshot", "screenshot", imageIndex.Value); + break; + default: + filename = type.ToString().ToLower(); + break; + } + + var extension = mimeType.Split('/').Last(); + + if (string.Equals(extension, "jpeg", StringComparison.OrdinalIgnoreCase)) + { + extension = "jpg"; + } + + extension = "." + extension.ToLower(); + + string path = null; + + if (saveLocally) + { + if (item.IsInMixedFolder && !(item is Episode)) + { + path = GetSavePathForItemInMixedFolder(item, type, filename, extension); + } + + if (string.IsNullOrEmpty(path)) + { + path = Path.Combine(item.MetaLocation, filename + extension); + } + } + + // None of the save local conditions passed, so store it in our internal folders + if (string.IsNullOrEmpty(path)) + { + path = _remoteImageCache.GetResourcePath(item.GetType().FullName + item.Id, filename + extension); + } + + return path; + } + + private string GetBackdropSaveFilename(IEnumerable<string> images, string zeroIndexFilename, string numberedIndexPrefix, int index) + { + if (index == 0) + { + return zeroIndexFilename; + } + + var filenames = images.Select(Path.GetFileNameWithoutExtension).ToList(); + + var current = index; + while (filenames.Contains(numberedIndexPrefix + current.ToString(UsCulture), StringComparer.OrdinalIgnoreCase)) + { + current++; + } + + return numberedIndexPrefix + current.ToString(UsCulture); + } + + /// <summary> + /// Gets the compatible save paths. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="mimeType">Type of the MIME.</param> + /// <returns>IEnumerable{System.String}.</returns> + /// <exception cref="System.ArgumentNullException">imageIndex</exception> + private string[] GetCompatibleSavePaths(BaseItem item, ImageType type, int? imageIndex, string mimeType) + { + var season = item as Season; + + var extension = mimeType.Split('/').Last(); + + if (string.Equals(extension, "jpeg", StringComparison.OrdinalIgnoreCase)) + { + extension = "jpg"; + } + extension = "." + extension.ToLower(); + + // Backdrop paths + if (type == ImageType.Backdrop) + { + if (!imageIndex.HasValue) + { + throw new ArgumentNullException("imageIndex"); + } + + if (imageIndex.Value == 0) + { + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart", extension) }; + } + + if (season != null && item.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = item.IndexNumber.Value == 0 + ? "-specials" + : item.IndexNumber.Value.ToString("00", UsCulture); + + var imageFilename = "season" + seasonMarker + "-fanart" + extension; + + return new[] { Path.Combine(seriesFolder, imageFilename) }; + } + + return new[] + { + Path.Combine(item.MetaLocation, "fanart" + extension) + }; + } + + var outputIndex = imageIndex.Value; + + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(UsCulture), extension) }; + } + + var extraFanartFilename = GetBackdropSaveFilename(item.BackdropImagePaths, "fanart", "fanart", outputIndex); + + return new[] + { + Path.Combine(item.MetaLocation, "extrafanart", extraFanartFilename + extension), + Path.Combine(item.MetaLocation, "extrathumbs", "thumb" + outputIndex.ToString(UsCulture) + extension) + }; + } + + if (type == ImageType.Primary) + { + if (season != null && item.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = item.IndexNumber.Value == 0 + ? "-specials" + : item.IndexNumber.Value.ToString("00", UsCulture); + + var imageFilename = "season" + seasonMarker + "-poster" + extension; + + return new[] { Path.Combine(seriesFolder, imageFilename) }; + } + + if (item is Episode) + { + var seasonFolder = Path.GetDirectoryName(item.Path); + + var imageFilename = Path.GetFileNameWithoutExtension(item.Path) + "-thumb" + extension; + + return new[] { Path.Combine(seasonFolder, imageFilename) }; + } + + if (item.IsInMixedFolder || item is MusicVideo) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, string.Empty, extension) }; + } + + if (item is MusicAlbum || item is MusicArtist) + { + return new[] { Path.Combine(item.MetaLocation, "folder" + extension) }; + } + + return new[] { Path.Combine(item.MetaLocation, "poster" + extension) }; + } + + if (type == ImageType.Banner) + { + if (season != null && item.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = item.IndexNumber.Value == 0 + ? "-specials" + : item.IndexNumber.Value.ToString("00", UsCulture); + + var imageFilename = "season" + seasonMarker + "-banner" + extension; + + return new[] { Path.Combine(seriesFolder, imageFilename) }; + } + } + + if (type == ImageType.Thumb) + { + if (season != null && item.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = item.IndexNumber.Value == 0 + ? "-specials" + : item.IndexNumber.Value.ToString("00", UsCulture); + + var imageFilename = "season" + seasonMarker + "-landscape" + extension; + + return new[] { Path.Combine(seriesFolder, imageFilename) }; + } + + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "landscape", extension) }; + } + + return new[] { Path.Combine(item.MetaLocation, "landscape" + extension) }; + } + + // All other paths are the same + return new[] { GetStandardSavePath(item, type, imageIndex, mimeType, true) }; + } + + /// <summary> + /// Gets the save path for item in mixed folder. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="type">The type.</param> + /// <param name="imageFilename">The image filename.</param> + /// <param name="extension">The extension.</param> + /// <returns>System.String.</returns> + private string GetSavePathForItemInMixedFolder(IHasImages item, ImageType type, string imageFilename, string extension) + { + if (type == ImageType.Primary) + { + imageFilename = "poster"; + } + var folder = Path.GetDirectoryName(item.Path); + + return Path.Combine(folder, Path.GetFileNameWithoutExtension(item.Path) + "-" + imageFilename + extension); + } + } +} diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs new file mode 100644 index 0000000000..5358506f0e --- /dev/null +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -0,0 +1,431 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Manager +{ + public class ItemImageProvider + { + private readonly ILogger _logger; + private readonly IProviderManager _providerManager; + private readonly IServerConfigurationManager _config; + + public ItemImageProvider(ILogger logger, IProviderManager providerManager, IServerConfigurationManager config) + { + _logger = logger; + _providerManager = providerManager; + _config = config; + } + + public bool ValidateImages(IHasImages item, IEnumerable<IImageProvider> providers) + { + var hasChanges = item.ValidateImages(); + + foreach (var provider in providers.OfType<IImageFileProvider>()) + { + var images = provider.GetImages(item); + + if (MergeImages(item, images)) + { + hasChanges = true; + } + } + + return hasChanges; + } + + public async Task<RefreshResult> RefreshImages(IHasImages item, IEnumerable<IImageProvider> imageProviders, ImageRefreshOptions options, CancellationToken cancellationToken) + { + var result = new RefreshResult { UpdateType = ItemUpdateType.Unspecified }; + + var providers = GetImageProviders(item, imageProviders).ToList(); + + foreach (var provider in providers.OfType<IRemoteImageProvider>()) + { + await RefreshFromProvider(item, provider, options, result, cancellationToken).ConfigureAwait(false); + } + + foreach (var provider in providers.OfType<IDynamicImageProvider>()) + { + await RefreshFromProvider(item, provider, result, cancellationToken).ConfigureAwait(false); + } + + return result; + } + + /// <summary> + /// Refreshes from provider. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="provider">The provider.</param> + /// <param name="result">The result.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task RefreshFromProvider(IHasImages item, IDynamicImageProvider provider, RefreshResult result, CancellationToken cancellationToken) + { + _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + try + { + var images = provider.GetImageInfos(item); + + foreach (var image in images) + { + if (!item.HasImage(image.Type)) + { + var imageSource = await provider.GetImage(item, image).ConfigureAwait(false); + + // See if the provider returned an image path or a stream + if (!string.IsNullOrEmpty(imageSource.Path)) + { + item.SetImagePath(image.Type, imageSource.Path); + } + else + { + var mimeType = "image/" + imageSource.Format.ToString().ToLower(); + + await _providerManager.SaveImage((BaseItem)item, imageSource.Stream, mimeType, image.Type, null, Guid.NewGuid().ToString(), cancellationToken).ConfigureAwait(false); + } + + result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + } + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + result.ErrorMessage = ex.Message; + result.Status = ProviderRefreshStatus.CompletedWithErrors; + _logger.ErrorException("Error in {0}", ex, provider.Name); + } + } + + /// <summary> + /// Image types that are only one per item + /// </summary> + private readonly ImageType[] _singularImages = + { + ImageType.Primary, + ImageType.Art, + ImageType.Banner, + ImageType.Box, + ImageType.BoxRear, + ImageType.Disc, + ImageType.Logo, + ImageType.Menu, + ImageType.Thumb + }; + + /// <summary> + /// Determines if an item already contains the given images + /// </summary> + /// <param name="item"></param> + /// <param name="images"></param> + /// <returns></returns> + private bool ContainsImages(IHasImages item, List<ImageType> images) + { + if (_singularImages.Any(i => images.Contains(i) && !item.HasImage(i))) + { + return false; + } + + if (images.Contains(ImageType.Backdrop) && item.BackdropImagePaths.Count < GetMaxBackdropCount(item)) + { + return false; + } + + if (images.Contains(ImageType.Screenshot)) + { + var hasScreenshots = item as IHasScreenshots; + if (hasScreenshots != null) + { + if (hasScreenshots.ScreenshotImagePaths.Count < GetMaxBackdropCount(item)) + { + return false; + } + } + } + + return true; + } + + /// <summary> + /// Refreshes from provider. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="provider">The provider.</param> + /// <param name="options">The options.</param> + /// <param name="result">The result.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task RefreshFromProvider(IHasImages item, IRemoteImageProvider provider, ImageRefreshOptions options, RefreshResult result, CancellationToken cancellationToken) + { + try + { + // TODO: Also factor in IsConfiguredToDownloadImage + if (ContainsImages(item, provider.GetSupportedImages(item).ToList())) + { + return; + } + + _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + var images = await provider.GetAllImages(item, cancellationToken).ConfigureAwait(false); + var list = images.ToList(); + + foreach (var type in _singularImages) + { + if (IsConfiguredToDownloadImage(item, type) && !item.HasImage(type)) + { + await DownloadImage(item, provider, result, list, type, cancellationToken).ConfigureAwait(false); + } + } + + await DownloadBackdrops(item, provider, result, list, cancellationToken).ConfigureAwait(false); + + var hasScreenshots = item as IHasScreenshots; + if (hasScreenshots != null) + { + await DownloadScreenshots(hasScreenshots, provider, result, list, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + result.ErrorMessage = ex.Message; + result.Status = ProviderRefreshStatus.CompletedWithErrors; + _logger.ErrorException("Error in {0}", ex, provider.Name); + } + } + + /// <summary> + /// Gets the image providers. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="imageProviders">The image providers.</param> + /// <returns>IEnumerable{IImageProvider}.</returns> + private IEnumerable<IImageProvider> GetImageProviders(IHasImages item, IEnumerable<IImageProvider> imageProviders) + { + var providers = imageProviders.Where(i => + { + try + { + return i.Supports(item); + } + catch (Exception ex) + { + _logger.ErrorException("Error in ImageProvider.Supports", ex, i.Name); + + return false; + } + }); + + if (!_config.Configuration.EnableInternetProviders) + { + providers = providers.Where(i => !(i is IRemoteImageProvider)); + } + + return providers.OrderBy(i => i.Order); + } + + private bool MergeImages(IHasImages item, List<LocalImageInfo> images) + { + var changed = false; + + foreach (var type in _singularImages) + { + var image = images.FirstOrDefault(i => i.Type == type); + + if (image != null) + { + var oldPath = item.GetImagePath(type); + + item.SetImagePath(type, image.Path); + + if (!string.Equals(oldPath, image.Path, StringComparison.OrdinalIgnoreCase)) + { + changed = true; + } + } + } + + // The change reporting will only be accurate at the count level + // Improve this if/when needed + var backdrops = images.Where(i => i.Type == ImageType.Backdrop).ToList(); + if (backdrops.Count > 0) + { + var oldCount = item.BackdropImagePaths.Count; + + item.BackdropImagePaths = item.BackdropImagePaths + .Concat(backdrops.Select(i => i.Path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (oldCount != item.BackdropImagePaths.Count) + { + changed = true; + } + } + + var hasScreenshots = item as IHasScreenshots; + if (hasScreenshots != null) + { + var screenshots = images.Where(i => i.Type == ImageType.Screenshot).ToList(); + + if (screenshots.Count > 0) + { + var oldCount = hasScreenshots.ScreenshotImagePaths.Count; + + hasScreenshots.ScreenshotImagePaths = hasScreenshots.ScreenshotImagePaths + .Concat(screenshots.Select(i => i.Path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (oldCount != hasScreenshots.ScreenshotImagePaths.Count) + { + changed = true; + } + } + } + + return changed; + } + + private async Task DownloadImage(IHasImages item, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken) + { + foreach (var image in images.Where(i => i.Type == type)) + { + var url = image.Url; + + try + { + var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false); + + await _providerManager.SaveImage((BaseItem)item, response.Content, response.ContentType, type, null, url, cancellationToken).ConfigureAwait(false); + + result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + break; + } + catch (HttpException ex) + { + // Sometimes providers send back bad url's. Just move onto the next image + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + continue; + } + break; + } + } + } + + private async Task DownloadBackdrops(IHasImages item, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, CancellationToken cancellationToken) + { + const ImageType imageType = ImageType.Backdrop; + var maxCount = GetMaxBackdropCount(item); + + foreach (var image in images.Where(i => i.Type == imageType)) + { + if (item.BackdropImagePaths.Count >= maxCount) + { + break; + } + + var url = image.Url; + + if (item.ContainsImageWithSourceUrl(url)) + { + continue; + } + + try + { + var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false); + + await _providerManager.SaveImage((BaseItem)item, response.Content, response.ContentType, imageType, null, url, cancellationToken).ConfigureAwait(false); + result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + break; + } + catch (HttpException ex) + { + // Sometimes providers send back bad url's. Just move onto the next image + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + continue; + } + break; + } + } + } + + private async Task DownloadScreenshots(IHasScreenshots item, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, CancellationToken cancellationToken) + { + const ImageType imageType = ImageType.Screenshot; + var maxCount = GetMaxScreenshotCount(item); + + foreach (var image in images.Where(i => i.Type == imageType)) + { + if (item.ScreenshotImagePaths.Count >= maxCount) + { + break; + } + + var url = image.Url; + + if (item.ContainsImageWithSourceUrl(url)) + { + continue; + } + + try + { + var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false); + + await _providerManager.SaveImage((BaseItem)item, response.Content, response.ContentType, imageType, null, url, cancellationToken).ConfigureAwait(false); + result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate; + break; + } + catch (HttpException ex) + { + // Sometimes providers send back bad url's. Just move onto the next image + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + continue; + } + break; + } + } + } + + private bool IsConfiguredToDownloadImage(IHasImages item, ImageType type) + { + return true; + } + + private int GetMaxBackdropCount(IHasImages item) + { + return 1; + } + + private int GetMaxScreenshotCount(IHasScreenshots item) + { + return 1; + } + } +} diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs new file mode 100644 index 0000000000..dbe70b93d7 --- /dev/null +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -0,0 +1,367 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Manager +{ + public abstract class MetadataService<TItemType> : IMetadataService + where TItemType : IHasMetadata, new() + { + protected readonly IServerConfigurationManager ServerConfigurationManager; + protected readonly ILogger Logger; + protected readonly IProviderManager ProviderManager; + + private IMetadataProvider<TItemType>[] _providers = { }; + + private IImageProvider[] _imageProviders = { }; + + protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager) + { + ServerConfigurationManager = serverConfigurationManager; + Logger = logger; + ProviderManager = providerManager; + } + + /// <summary> + /// Adds the parts. + /// </summary> + /// <param name="providers">The providers.</param> + /// <param name="imageProviders">The image providers.</param> + public void AddParts(IEnumerable<IMetadataProvider> providers, IEnumerable<IImageProvider> imageProviders) + { + _providers = providers.OfType<IMetadataProvider<TItemType>>() + .OrderBy(GetSortOrder) + .ToArray(); + + _imageProviders = imageProviders.OrderBy(i => i.Order).ToArray(); + } + + /// <summary> + /// Saves the provider result. + /// </summary> + /// <param name="result">The result.</param> + /// <returns>Task.</returns> + protected Task SaveProviderResult(ProviderResult result) + { + return Task.FromResult(true); + } + + /// <summary> + /// Gets the last result. + /// </summary> + /// <param name="itemId">The item identifier.</param> + /// <returns>ProviderResult.</returns> + protected ProviderResult GetLastResult(Guid itemId) + { + return new ProviderResult + { + ItemId = itemId + }; + } + + public async Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var itemOfType = (TItemType)item; + + var updateType = ItemUpdateType.Unspecified; + var lastResult = GetLastResult(item.Id); + var refreshResult = new ProviderResult { ItemId = item.Id }; + + var imageProviders = GetImageProviders(item).ToList(); + var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager); + var localImagesFailed = false; + + // Start by validating images + try + { + // Always validate images and check for new locally stored ones. + if (itemImageProvider.ValidateImages(item, imageProviders)) + { + updateType = updateType | ItemUpdateType.ImageUpdate; + } + } + catch (Exception ex) + { + localImagesFailed = true; + Logger.ErrorException("Error validating images for {0}", ex, item.Path ?? item.Name); + refreshResult.AddStatus(ProviderRefreshStatus.Failure, ex.Message); + } + + // Next run metadata providers + if (options.MetadataRefreshMode != MetadataRefreshMode.None) + { + var providers = GetProviders(item, lastResult.HasRefreshedMetadata, options).ToList(); + + if (providers.Count > 0) + { + var result = await RefreshWithProviders(itemOfType, options, providers, cancellationToken).ConfigureAwait(false); + + updateType = updateType | result.UpdateType; + refreshResult.AddStatus(result.Status, result.ErrorMessage); + } + + refreshResult.HasRefreshedMetadata = true; + } + + // Next run remote image providers, but only if local image providers didn't throw an exception + if (!localImagesFailed) + { + if ((options.ImageRefreshMode == MetadataRefreshMode.EnsureMetadata && !lastResult.HasRefreshedImages) || + options.ImageRefreshMode == MetadataRefreshMode.FullRefresh) + { + var imagesReult = await itemImageProvider.RefreshImages(itemOfType, imageProviders, options, cancellationToken).ConfigureAwait(false); + + updateType = updateType | imagesReult.UpdateType; + refreshResult.AddStatus(imagesReult.Status, imagesReult.ErrorMessage); + refreshResult.HasRefreshedImages = true; + } + } + + var providersHadChanges = updateType > ItemUpdateType.Unspecified; + + if (options.ForceSave || providersHadChanges) + { + if (string.IsNullOrEmpty(item.Name)) + { + throw new InvalidOperationException("Item has no name"); + } + + // Save to database + await SaveItem(itemOfType, updateType, cancellationToken); + } + + if (providersHadChanges) + { + refreshResult.DateLastRefreshed = DateTime.UtcNow; + await SaveProviderResult(refreshResult).ConfigureAwait(false); + } + } + + /// <summary> + /// Gets the providers. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="hasRefreshedMetadata">if set to <c>true</c> [has refreshed metadata].</param> + /// <param name="options">The options.</param> + /// <returns>IEnumerable{`0}.</returns> + protected virtual IEnumerable<IMetadataProvider> GetProviders(IHasMetadata item, bool hasRefreshedMetadata, MetadataRefreshOptions options) + { + // Get providers to refresh + var providers = _providers.Where(i => CanRefresh(i, item)).ToList(); + + // Run all if either of these flags are true + var runAllProviders = options.ReplaceAllMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || !hasRefreshedMetadata; + + if (!runAllProviders) + { + // Avoid implicitly captured closure + var currentItem = item; + + var providersWithChanges = providers.OfType<IHasChangeMonitor>() + .Where(i => i.HasChanged(currentItem, item.DateLastSaved)) + .ToList(); + + // If local providers are the only ones with changes, then just run those + if (providersWithChanges.All(i => i is ILocalMetadataProvider)) + { + providers = providers.Where(i => i is ILocalMetadataProvider).ToList(); + } + } + + return providers; + } + + /// <summary> + /// Gets the sort order. + /// </summary> + /// <param name="provider">The provider.</param> + /// <returns>System.Int32.</returns> + protected virtual int GetSortOrder(IMetadataProvider<TItemType> provider) + { + if (provider is IRemoteMetadataProvider) + { + return 1; + } + + return 0; + } + + /// <summary> + /// Determines whether this instance can refresh the specified provider. + /// </summary> + /// <param name="provider">The provider.</param> + /// <param name="item">The item.</param> + /// <returns><c>true</c> if this instance can refresh the specified provider; otherwise, <c>false</c>.</returns> + protected bool CanRefresh(IMetadataProvider provider, IHasMetadata item) + { + if (!ServerConfigurationManager.Configuration.EnableInternetProviders && provider is IRemoteMetadataProvider) + { + return false; + } + + if (item.LocationType != LocationType.FileSystem && provider is ILocalMetadataProvider) + { + return false; + } + + return true; + } + + protected abstract Task SaveItem(TItemType item, ItemUpdateType reason, CancellationToken cancellationToken); + + protected virtual ItemId GetId(TItemType item) + { + return new ItemId + { + MetadataCountryCode = item.GetPreferredMetadataCountryCode(), + MetadataLanguage = item.GetPreferredMetadataLanguage(), + Name = item.Name, + ProviderIds = item.ProviderIds + }; + } + + public bool CanRefresh(IHasMetadata item) + { + return item is TItemType; + } + + protected virtual async Task<RefreshResult> RefreshWithProviders(TItemType item, MetadataRefreshOptions options, List<IMetadataProvider> providers, CancellationToken cancellationToken) + { + var refreshResult = new RefreshResult { UpdateType = ItemUpdateType.Unspecified }; + + var temp = new TItemType(); + + // If replacing all metadata, run internet providers first + if (options.ReplaceAllMetadata) + { + await ExecuteRemoteProviders(item, temp, providers.OfType<IRemoteMetadataProvider<TItemType>>(), refreshResult, cancellationToken).ConfigureAwait(false); + } + + var hasLocalMetadata = false; + + foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>()) + { + Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + try + { + var localItem = await provider.GetMetadata(item.Path, cancellationToken).ConfigureAwait(false); + + if (localItem.HasMetadata) + { + MergeData(localItem.Item, temp, new List<MetadataFields>(), false, true); + refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport; + + // Only one local provider allowed per item + hasLocalMetadata = true; + break; + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + // If a local provider fails, consider that a failure + refreshResult.Status = ProviderRefreshStatus.Failure; + refreshResult.ErrorMessage = ex.Message; + Logger.ErrorException("Error in {0}", ex, provider.Name); + + // If the local provider fails don't continue with remote providers because the user's saved metadata could be lost + return refreshResult; + } + } + + if (!options.ReplaceAllMetadata && !hasLocalMetadata) + { + await ExecuteRemoteProviders(item, temp, providers.OfType<IRemoteMetadataProvider<TItemType>>(), refreshResult, cancellationToken).ConfigureAwait(false); + } + + MergeData(temp, item, item.LockedFields, true, true); + + return refreshResult; + } + + private async Task ExecuteRemoteProviders(TItemType item, TItemType temp, IEnumerable<IRemoteMetadataProvider<TItemType>> providers, RefreshResult refreshResult, CancellationToken cancellationToken) + { + var id = GetId(item); + + foreach (var provider in providers) + { + Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); + + try + { + var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false); + + if (result.HasMetadata) + { + MergeData(result.Item, temp, new List<MetadataFields>(), false, false); + + refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload; + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + refreshResult.Status = ProviderRefreshStatus.CompletedWithErrors; + refreshResult.ErrorMessage = ex.Message; + Logger.ErrorException("Error in {0}", ex, provider.Name); + } + } + } + + protected abstract void MergeData(TItemType source, TItemType target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings); + + public virtual int Order + { + get + { + return 0; + } + } + + private IEnumerable<IImageProvider> GetImageProviders(IHasImages item) + { + var providers = _imageProviders.Where(i => + { + try + { + return i.Supports(item); + } + catch (Exception ex) + { + Logger.ErrorException("Error in ImageProvider.Supports", ex, i.Name); + + return false; + } + }); + + if (!ServerConfigurationManager.Configuration.EnableInternetProviders) + { + providers = providers.Where(i => !(i is IRemoteImageProvider)); + } + + return providers.OrderBy(i => i.Order); + } + } + + public class RefreshResult + { + public ItemUpdateType UpdateType { get; set; } + public ProviderRefreshStatus Status { get; set; } + public string ErrorMessage { get; set; } + } +} diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs new file mode 100644 index 0000000000..ebad2e6e05 --- /dev/null +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -0,0 +1,487 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.Manager +{ + /// <summary> + /// Class ProviderManager + /// </summary> + public class ProviderManager : IProviderManager + { + /// <summary> + /// The _logger + /// </summary> + private readonly ILogger _logger; + + /// <summary> + /// The _HTTP client + /// </summary> + private readonly IHttpClient _httpClient; + + /// <summary> + /// The _directory watchers + /// </summary> + private readonly IDirectoryWatchers _directoryWatchers; + + /// <summary> + /// Gets or sets the configuration manager. + /// </summary> + /// <value>The configuration manager.</value> + private IServerConfigurationManager ConfigurationManager { get; set; } + + /// <summary> + /// Gets the list of currently registered metadata prvoiders + /// </summary> + /// <value>The metadata providers enumerable.</value> + private BaseMetadataProvider[] MetadataProviders { get; set; } + + private IRemoteImageProvider[] RemoteImageProviders { get; set; } + private IImageProvider[] ImageProviders { get; set; } + + private readonly IFileSystem _fileSystem; + + private readonly IItemRepository _itemRepo; + + private IMetadataService[] _metadataServices = {}; + + /// <summary> + /// Initializes a new instance of the <see cref="ProviderManager" /> class. + /// </summary> + /// <param name="httpClient">The HTTP client.</param> + /// <param name="configurationManager">The configuration manager.</param> + /// <param name="directoryWatchers">The directory watchers.</param> + /// <param name="logManager">The log manager.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="itemRepo">The item repo.</param> + public ProviderManager(IHttpClient httpClient, IServerConfigurationManager configurationManager, IDirectoryWatchers directoryWatchers, ILogManager logManager, IFileSystem fileSystem, IItemRepository itemRepo) + { + _logger = logManager.GetLogger("ProviderManager"); + _httpClient = httpClient; + ConfigurationManager = configurationManager; + _directoryWatchers = directoryWatchers; + _fileSystem = fileSystem; + _itemRepo = itemRepo; + } + + /// <summary> + /// Adds the metadata providers. + /// </summary> + /// <param name="providers">The providers.</param> + /// <param name="imageProviders">The image providers.</param> + /// <param name="metadataServices">The metadata services.</param> + /// <param name="metadataProviders">The metadata providers.</param> + public void AddParts(IEnumerable<BaseMetadataProvider> providers, IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders) + { + MetadataProviders = providers.OrderBy(e => e.Priority).ToArray(); + + ImageProviders = imageProviders.OrderBy(i => i.Order).ToArray(); + RemoteImageProviders = ImageProviders.OfType<IRemoteImageProvider>().ToArray(); + + _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray(); + + var providerList = metadataProviders.ToList(); + foreach (var service in _metadataServices) + { + service.AddParts(providerList, ImageProviders); + } + } + + public Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var service = _metadataServices.FirstOrDefault(i => i.CanRefresh(item)); + + if (service != null) + { + return service.RefreshMetadata(item, options, cancellationToken); + } + + return ((BaseItem)item).RefreshMetadataDirect(cancellationToken, options.ForceSave, options.ReplaceAllMetadata); + } + + /// <summary> + /// Runs all metadata providers for an entity, and returns true or false indicating if at least one was refreshed and requires persistence + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="force">if set to <c>true</c> [force].</param> + /// <returns>Task{System.Boolean}.</returns> + /// <exception cref="System.ArgumentNullException">item</exception> + public async Task<ItemUpdateType?> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + ItemUpdateType? result = null; + + cancellationToken.ThrowIfCancellationRequested(); + + var enableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders; + + var providerHistories = item.DateLastSaved == default(DateTime) ? + new List<BaseProviderInfo>() : + _itemRepo.GetProviderHistory(item.Id).ToList(); + + // Run the normal providers sequentially in order of priority + foreach (var provider in MetadataProviders) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!ProviderSupportsItem(provider, item)) + { + continue; + } + + // Skip if internet providers are currently disabled + if (provider.RequiresInternet && !enableInternetProviders) + { + continue; + } + + // Put this check below the await because the needs refresh of the next tier of providers may depend on the previous ones running + // This is the case for the fan art provider which depends on the movie and tv providers having run before them + if (provider.RequiresInternet && item.DontFetchMeta && provider.EnforceDontFetchMetadata) + { + continue; + } + + var providerInfo = providerHistories.FirstOrDefault(i => i.ProviderId == provider.Id); + + if (providerInfo == null) + { + providerInfo = new BaseProviderInfo + { + ProviderId = provider.Id + }; + providerHistories.Add(providerInfo); + } + + try + { + if (!force && !provider.NeedsRefresh(item, providerInfo)) + { + continue; + } + } + catch (Exception ex) + { + _logger.Error("Error determining NeedsRefresh for {0}", ex, item.Path); + } + + var updateType = await FetchAsync(provider, item, providerInfo, force, cancellationToken).ConfigureAwait(false); + + if (updateType.HasValue) + { + if (result.HasValue) + { + result = result.Value | updateType.Value; + } + else + { + result = updateType; + } + } + } + + if (result.HasValue || force) + { + await _itemRepo.SaveProviderHistory(item.Id, providerHistories, cancellationToken); + } + + return result; + } + + /// <summary> + /// Providers the supports item. + /// </summary> + /// <param name="provider">The provider.</param> + /// <param name="item">The item.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + private bool ProviderSupportsItem(BaseMetadataProvider provider, BaseItem item) + { + try + { + return provider.Supports(item); + } + catch (Exception ex) + { + _logger.ErrorException("{0} failed in Supports for type {1}", ex, provider.GetType().Name, item.GetType().Name); + return false; + } + } + + /// <summary> + /// Fetches metadata and returns true or false indicating if any work that requires persistence was done + /// </summary> + /// <param name="provider">The provider.</param> + /// <param name="item">The item.</param> + /// <param name="providerInfo">The provider information.</param> + /// <param name="force">if set to <c>true</c> [force].</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{System.Boolean}.</returns> + /// <exception cref="System.ArgumentNullException">item</exception> + private async Task<ItemUpdateType?> FetchAsync(BaseMetadataProvider provider, BaseItem item, BaseProviderInfo providerInfo, bool force, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Don't clog up the log with these providers + if (!(provider is IDynamicInfoProvider)) + { + _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--"); + } + + try + { + var changed = await provider.FetchAsync(item, force, providerInfo, cancellationToken).ConfigureAwait(false); + + if (changed) + { + return provider.ItemUpdateType; + } + + return null; + } + catch (OperationCanceledException ex) + { + _logger.Debug("{0} canceled for {1}", provider.GetType().Name, item.Name); + + // If the outer cancellation token is the one that caused the cancellation, throw it + if (cancellationToken.IsCancellationRequested && ex.CancellationToken == cancellationToken) + { + throw; + } + + return null; + } + catch (Exception ex) + { + _logger.ErrorException("{0} failed refreshing {1} {2}", ex, provider.GetType().Name, item.Name, item.Path ?? string.Empty); + + provider.SetLastRefreshed(item, DateTime.UtcNow, providerInfo, ProviderRefreshStatus.Failure); + + return ItemUpdateType.Unspecified; + } + } + + /// <summary> + /// Saves to library filesystem. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="path">The path.</param> + /// <param name="dataToSave">The data to save.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + public async Task SaveToLibraryFilesystem(BaseItem item, string path, Stream dataToSave, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException(); + } + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + if (dataToSave == null) + { + throw new ArgumentNullException(); + } + + if (cancellationToken.IsCancellationRequested) + { + dataToSave.Dispose(); + cancellationToken.ThrowIfCancellationRequested(); + } + + //Tell the watchers to ignore + _directoryWatchers.TemporarilyIgnore(path); + + if (dataToSave.CanSeek) + { + dataToSave.Position = 0; + } + + try + { + using (dataToSave) + { + using (var fs = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true)) + { + await dataToSave.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } + + // If this is ever used for something other than metadata we can add a file type param + item.ResolveArgs.AddMetadataFile(path); + } + finally + { + //Remove the ignore + _directoryWatchers.RemoveTempIgnore(path); + } + } + + + /// <summary> + /// Saves the image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="url">The URL.</param> + /// <param name="resourcePool">The resource pool.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task SaveImage(BaseItem item, string url, SemaphoreSlim resourcePool, ImageType type, int? imageIndex, CancellationToken cancellationToken) + { + var response = await _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + ResourcePool = resourcePool, + Url = url + + }).ConfigureAwait(false); + + await SaveImage(item, response.Content, response.ContentType, type, imageIndex, url, cancellationToken) + .ConfigureAwait(false); + } + + /// <summary> + /// Saves the image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="source">The source.</param> + /// <param name="mimeType">Type of the MIME.</param> + /// <param name="type">The type.</param> + /// <param name="imageIndex">Index of the image.</param> + /// <param name="sourceUrl">The source URL.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, string sourceUrl, CancellationToken cancellationToken) + { + return new ImageSaver(ConfigurationManager, _directoryWatchers, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, sourceUrl, cancellationToken); + } + + /// <summary> + /// Gets the available remote images. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="providerName">Name of the provider.</param> + /// <param name="type">The type.</param> + /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> + public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, CancellationToken cancellationToken, string providerName = null, ImageType? type = null) + { + var providers = GetRemoteImageProviders(item); + + if (!string.IsNullOrEmpty(providerName)) + { + providers = providers.Where(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)); + } + + var preferredLanguage = item.GetPreferredMetadataLanguage(); + + var tasks = providers.Select(i => GetImages(item, cancellationToken, i, preferredLanguage, type)); + + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + + return results.SelectMany(i => i); + } + + /// <summary> + /// Gets the images. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="i">The i.</param> + /// <param name="preferredLanguage">The preferred language.</param> + /// <param name="type">The type.</param> + /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> + private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken, IRemoteImageProvider i, string preferredLanguage, ImageType? type = null) + { + try + { + if (type.HasValue) + { + var result = await i.GetImages(item, type.Value, cancellationToken).ConfigureAwait(false); + + return FilterImages(result, preferredLanguage); + } + else + { + var result = await i.GetAllImages(item, cancellationToken).ConfigureAwait(false); + return FilterImages(result, preferredLanguage); + } + } + catch (Exception ex) + { + _logger.ErrorException("{0} failed in GetImageInfos for type {1}", ex, i.GetType().Name, item.GetType().Name); + return new List<RemoteImageInfo>(); + } + } + + private IEnumerable<RemoteImageInfo> FilterImages(IEnumerable<RemoteImageInfo> images, string preferredLanguage) + { + if (string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase)) + { + images = images.Where(i => string.IsNullOrEmpty(i.Language) || + string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)); + } + + return images; + } + + private IEnumerable<IRemoteImageProvider> GetRemoteImageProviders(BaseItem item) + { + return RemoteImageProviders.Where(i => + { + try + { + return i.Supports(item); + } + catch (Exception ex) + { + _logger.ErrorException("{0} failed in Supports for type {1}", ex, i.GetType().Name, item.GetType().Name); + return false; + } + + }); + } + + /// <summary> + /// Gets the supported image providers. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>IEnumerable{IImageProvider}.</returns> + public IEnumerable<ImageProviderInfo> GetImageProviderInfo(BaseItem item) + { + return GetRemoteImageProviders(item).Select(i => new ImageProviderInfo + { + Name = i.Name, + Order = i.Order + + }); + } + } +} |
