Browse Source

New provider system. Only for people right now

Luke Pulverenti 11 năm trước cách đây
mục cha
commit
ad82c9f5e9
83 tập tin đã thay đổi với 3094 bổ sung1746 xóa
  1. 7 2
      MediaBrowser.Api/Images/ImageService.cs
  2. 12 10
      MediaBrowser.Api/Images/RemoteImageService.cs
  3. 41 8
      MediaBrowser.Api/ItemRefreshService.cs
  4. 4 4
      MediaBrowser.Controller/Drawing/ImageExtensions.cs
  5. 11 0
      MediaBrowser.Controller/Drawing/ImageFormat.cs
  6. 92 23
      MediaBrowser.Controller/Entities/BaseItem.cs
  7. 10 3
      MediaBrowser.Controller/Entities/Folder.cs
  8. 26 1
      MediaBrowser.Controller/Entities/IHasImages.cs
  9. 4 2
      MediaBrowser.Controller/Entities/IHasScreenshots.cs
  10. 12 7
      MediaBrowser.Controller/Entities/Movies/Movie.cs
  11. 11 9
      MediaBrowser.Controller/Entities/User.cs
  12. 10 6
      MediaBrowser.Controller/Entities/Video.cs
  13. 2 3
      MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs
  14. 3 2
      MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs
  15. 8 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  16. 31 0
      MediaBrowser.Controller/Providers/IHasMetadata.cs
  17. 3 25
      MediaBrowser.Controller/Providers/IImageProvider.cs
  18. 58 0
      MediaBrowser.Controller/Providers/ILocalImageProvider.cs
  19. 90 0
      MediaBrowser.Controller/Providers/IMetadataProvider.cs
  20. 38 0
      MediaBrowser.Controller/Providers/IMetadataService.cs
  21. 15 5
      MediaBrowser.Controller/Providers/IProviderManager.cs
  22. 48 0
      MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
  23. 49 0
      MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
  24. 60 0
      MediaBrowser.Controller/Providers/ProviderResult.cs
  25. 6 0
      MediaBrowser.Model/Dto/BaseItemDto.cs
  26. 11 0
      MediaBrowser.Model/Entities/IHasProviderIds.cs
  27. 3 3
      MediaBrowser.Model/Providers/ImageProviderInfo.cs
  28. 5 0
      MediaBrowser.Model/Querying/ItemFields.cs
  29. 327 0
      MediaBrowser.Providers/All/LocalImageProvider.cs
  30. 28 0
      MediaBrowser.Providers/BaseXmlProvider.cs
  31. 2 2
      MediaBrowser.Providers/CollectionFolderImageProvider.cs
  32. 25 4
      MediaBrowser.Providers/GameGenres/GameGenreImageProvider.cs
  33. 42 0
      MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs
  34. 26 4
      MediaBrowser.Providers/Genres/GenreImageProvider.cs
  35. 42 0
      MediaBrowser.Providers/Genres/GenreMetadataService.cs
  36. 0 11
      MediaBrowser.Providers/ImageFromMediaLocationProvider.cs
  37. 0 160
      MediaBrowser.Providers/ImagesByName/GameGenreImageProvider.cs
  38. 0 160
      MediaBrowser.Providers/ImagesByName/GenreImageProvider.cs
  39. 0 161
      MediaBrowser.Providers/ImagesByName/MusicGenreImageProvider.cs
  40. 0 160
      MediaBrowser.Providers/ImagesByName/StudioImageProvider.cs
  41. 1 1
      MediaBrowser.Providers/Manager/ImageSaver.cs
  42. 431 0
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  43. 367 0
      MediaBrowser.Providers/Manager/MetadataService.cs
  44. 55 22
      MediaBrowser.Providers/Manager/ProviderManager.cs
  45. 21 14
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  46. 31 4
      MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs
  47. 27 5
      MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs
  48. 0 207
      MediaBrowser.Providers/Movies/MovieDbPersonImageProvider.cs
  49. 0 440
      MediaBrowser.Providers/Movies/MovieDbPersonProvider.cs
  50. 0 89
      MediaBrowser.Providers/Movies/PersonProviderFromXml.cs
  51. 27 5
      MediaBrowser.Providers/Music/ManualFanartAlbumProvider.cs
  52. 30 5
      MediaBrowser.Providers/Music/ManualFanartArtistProvider.cs
  53. 32 5
      MediaBrowser.Providers/Music/ManualLastFmImageProvider.cs
  54. 25 4
      MediaBrowser.Providers/MusicGenres/MusicGenreImageProvider.cs
  55. 42 0
      MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
  56. 27 5
      MediaBrowser.Providers/People/MovieDbPersonImageProvider.cs
  57. 289 0
      MediaBrowser.Providers/People/MovieDbPersonProvider.cs
  58. 47 0
      MediaBrowser.Providers/People/PersonMetadataService.cs
  59. 64 0
      MediaBrowser.Providers/People/PersonXmlProvider.cs
  60. 28 6
      MediaBrowser.Providers/People/TvdbPersonImageProvider.cs
  61. 126 0
      MediaBrowser.Providers/ProviderUtils.cs
  62. 41 0
      MediaBrowser.Providers/Studios/StudioMetadataService.cs
  63. 25 4
      MediaBrowser.Providers/Studios/StudiosImageProvider.cs
  64. 27 5
      MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs
  65. 31 5
      MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs
  66. 25 4
      MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs
  67. 28 5
      MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs
  68. 28 5
      MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs
  69. 0 98
      MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs
  70. 0 8
      MediaBrowser.Providers/VirtualItemImageValidator.cs
  71. 6 6
      MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs
  72. 5 0
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  73. 3 3
      MediaBrowser.Server.Implementations/Library/ResolverHelper.cs
  74. 6 1
      MediaBrowser.Server.Implementations/Library/UserManager.cs
  75. 0 1
      MediaBrowser.Server.Implementations/Library/Validators/GenresPostScanTask.cs
  76. 9 1
      MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs
  77. 1 1
      MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
  78. 19 3
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  79. 1 1
      MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs
  80. 1 1
      MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs
  81. 0 2
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  82. 2 2
      MediaBrowser.ServerApplication/ApplicationHost.cs
  83. 4 3
      MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs

+ 7 - 2
MediaBrowser.Api/Images/ImageService.cs

@@ -66,7 +66,7 @@ namespace MediaBrowser.Api.Images
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
     }
-    
+
     /// <summary>
     /// Class UpdateItemImageIndex
     /// </summary>
@@ -799,7 +799,12 @@ namespace MediaBrowser.Api.Images
 
                 await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, null, CancellationToken.None).ConfigureAwait(false);
 
-                await entity.RefreshMetadata(CancellationToken.None, forceRefresh: true, forceSave: true, allowSlowProviders: false).ConfigureAwait(false);
+                await entity.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ImageRefreshMode = MetadataRefreshMode.None,
+                    ForceSave = true
+
+                }, CancellationToken.None).ConfigureAwait(false);
             }
         }
     }

+ 12 - 10
MediaBrowser.Api/Images/RemoteImageService.cs

@@ -9,13 +9,13 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using ServiceStack;
+using ServiceStack.Text.Controller;
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using ServiceStack.Text.Controller;
 
 namespace MediaBrowser.Api.Images
 {
@@ -193,12 +193,7 @@ namespace MediaBrowser.Api.Images
 
         private List<ImageProviderInfo> GetImageProviders(BaseItem item)
         {
-            return _providerManager.GetImageProviders(item).Select(i => new ImageProviderInfo
-            {
-                Name = i.Name,
-                Priority = i.Priority
-
-            }).ToList();
+            return _providerManager.GetImageProviderInfo(item).ToList();
         }
 
         public object Get(GetRemoteImages request)
@@ -229,7 +224,9 @@ namespace MediaBrowser.Api.Images
             var result = new RemoteImageResult
             {
                 TotalRecordCount = imagesList.Count,
-                Providers = _providerManager.GetImageProviders(item).Select(i => i.Name).ToList()
+                Providers = images.Select(i => i.ProviderName)
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .ToList()
             };
 
             if (request.StartIndex.HasValue)
@@ -284,8 +281,13 @@ namespace MediaBrowser.Api.Images
         {
             await _providerManager.SaveImage(item, request.ImageUrl, null, request.Type, null, CancellationToken.None).ConfigureAwait(false);
 
-            await item.RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false)
-                    .ConfigureAwait(false);
+            await item.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = true,
+                ImageRefreshMode = MetadataRefreshMode.None,
+                MetadataRefreshMode = MetadataRefreshMode.None
+
+            }, CancellationToken.None).ConfigureAwait(false);
         }
 
         /// <summary>

+ 41 - 8
MediaBrowser.Api/ItemRefreshService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using ServiceStack;
 using System;
 using System.Linq;
@@ -131,7 +132,11 @@ namespace MediaBrowser.Api
 
             try
             {
-                await item.RefreshMetadata(cancellationToken, forceRefresh: request.Forced).ConfigureAwait(false);
+                await item.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ReplaceAllMetadata = request.Forced,
+
+                }, CancellationToken.None).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -152,7 +157,11 @@ namespace MediaBrowser.Api
 
             try
             {
-                await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
+                await item.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ReplaceAllMetadata = request.Forced,
+
+                }, CancellationToken.None).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -173,7 +182,11 @@ namespace MediaBrowser.Api
 
             try
             {
-                await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
+                await item.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ReplaceAllMetadata = request.Forced,
+
+                }, CancellationToken.None).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -194,7 +207,11 @@ namespace MediaBrowser.Api
 
             try
             {
-                await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
+                await item.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ReplaceAllMetadata = request.Forced,
+
+                }, CancellationToken.None).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -215,7 +232,11 @@ namespace MediaBrowser.Api
 
             try
             {
-                await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
+                await item.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ReplaceAllMetadata = request.Forced,
+
+                }, CancellationToken.None).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -236,7 +257,11 @@ namespace MediaBrowser.Api
 
             try
             {
-                await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
+                await item.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ReplaceAllMetadata = request.Forced,
+
+                }, CancellationToken.None).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
@@ -266,7 +291,11 @@ namespace MediaBrowser.Api
 
             try
             {
-                await item.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
+                await item.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ReplaceAllMetadata = request.Forced,
+
+                }, CancellationToken.None).ConfigureAwait(false);
 
                 if (item.IsFolder)
                 {
@@ -301,7 +330,11 @@ namespace MediaBrowser.Api
         {
             foreach (var child in collectionFolder.Children.ToList())
             {
-                await child.RefreshMetadata(CancellationToken.None, forceRefresh: request.Forced).ConfigureAwait(false);
+                await child.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ReplaceAllMetadata = request.Forced,
+
+                }, CancellationToken.None).ConfigureAwait(false);
 
                 if (child.IsFolder)
                 {

+ 4 - 4
MediaBrowser.Controller/Drawing/ImageExtensions.cs

@@ -18,17 +18,17 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="image">The image.</param>
         /// <param name="toStream">To stream.</param>
         /// <param name="quality">The quality.</param>
-        public static void Save(this Image image, ImageFormat outputFormat, Stream toStream, int quality)
+        public static void Save(this Image image, System.Drawing.Imaging.ImageFormat outputFormat, Stream toStream, int quality)
         {
             // Use special save methods for jpeg and png that will result in a much higher quality image
             // All other formats use the generic Image.Save
-            if (ImageFormat.Jpeg.Equals(outputFormat))
+            if (System.Drawing.Imaging.ImageFormat.Jpeg.Equals(outputFormat))
             {
                 SaveAsJpeg(image, toStream, quality);
             }
-            else if (ImageFormat.Png.Equals(outputFormat))
+            else if (System.Drawing.Imaging.ImageFormat.Png.Equals(outputFormat))
             {
-                image.Save(toStream, ImageFormat.Png);
+                image.Save(toStream, System.Drawing.Imaging.ImageFormat.Png);
             }
             else
             {

+ 11 - 0
MediaBrowser.Controller/Drawing/ImageFormat.cs

@@ -0,0 +1,11 @@
+
+namespace MediaBrowser.Controller.Drawing
+{
+    public enum ImageFormat
+    {
+        Jpg,
+        Png,
+        Gif,
+        Bmp
+    }
+}

+ 92 - 23
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Entities
     /// <summary>
     /// Class BaseItem
     /// </summary>
-    public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData
+    public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData, IHasMetadata
     {
         protected BaseItem()
         {
@@ -767,25 +767,35 @@ namespace MediaBrowser.Controller.Entities
             }).ToList();
         }
 
+        public Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool resetResolveArgs = true)
+        {
+            return RefreshMetadata(new MetadataRefreshOptions { ResetResolveArgs = resetResolveArgs }, cancellationToken);
+        }
+
         /// <summary>
         /// Overrides the base implementation to refresh metadata for local trailers
         /// </summary>
+        /// <param name="options">The options.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
-        /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
-        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
-        /// <param name="resetResolveArgs">if set to <c>true</c> [reset resolve args].</param>
         /// <returns>true if a provider reports we changed</returns>
-        public virtual async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+        public async Task<bool> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            if (resetResolveArgs)
+            if (options.ResetResolveArgs)
             {
                 // Reload this
                 ResetResolveArgs();
             }
 
+            await ProviderManager.RefreshMetadata(this, options, cancellationToken).ConfigureAwait(false);
+
+            return false;
+        }
+
+        [Obsolete]
+        public virtual async Task<bool> RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false)
+        {
             // Refresh for the item
-            var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders);
+            var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh);
 
             cancellationToken.ThrowIfCancellationRequested();
 
@@ -800,15 +810,15 @@ namespace MediaBrowser.Controller.Entities
                 var hasThemeMedia = this as IHasThemeMedia;
                 if (hasThemeMedia != null)
                 {
-                    themeSongsChanged = await RefreshThemeSongs(hasThemeMedia, cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                    themeSongsChanged = await RefreshThemeSongs(hasThemeMedia, cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
 
-                    themeVideosChanged = await RefreshThemeVideos(hasThemeMedia, cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                    themeVideosChanged = await RefreshThemeVideos(hasThemeMedia, cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
                 }
 
                 var hasTrailers = this as IHasTrailers;
                 if (hasTrailers != null)
                 {
-                    localTrailersChanged = await RefreshLocalTrailers(hasTrailers, cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                    localTrailersChanged = await RefreshLocalTrailers(hasTrailers, cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
                 }
             }
 
@@ -829,14 +839,20 @@ namespace MediaBrowser.Controller.Entities
             return changed;
         }
 
-        private async Task<bool> RefreshLocalTrailers(IHasTrailers item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
+        private async Task<bool> RefreshLocalTrailers(IHasTrailers item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false)
         {
             var newItems = LoadLocalTrailers().ToList();
             var newItemIds = newItems.Select(i => i.Id).ToList();
 
             var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
 
-            var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false));
+            var tasks = newItems.Select(i => i.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = forceSave,
+                ReplaceAllMetadata = forceRefresh,
+                ResetResolveArgs = false
+
+            }, cancellationToken));
 
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
@@ -845,14 +861,20 @@ namespace MediaBrowser.Controller.Entities
             return itemsChanged || results.Contains(true);
         }
 
-        private async Task<bool> RefreshThemeVideos(IHasThemeMedia item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
+        private async Task<bool> RefreshThemeVideos(IHasThemeMedia item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false)
         {
             var newThemeVideos = LoadThemeVideos().ToList();
             var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToList();
 
             var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds);
 
-            var tasks = newThemeVideos.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false));
+            var tasks = newThemeVideos.Select(i => i.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = forceSave,
+                ReplaceAllMetadata = forceRefresh,
+                ResetResolveArgs = false
+
+            }, cancellationToken));
 
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
@@ -864,14 +886,20 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Refreshes the theme songs.
         /// </summary>
-        private async Task<bool> RefreshThemeSongs(IHasThemeMedia item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true)
+        private async Task<bool> RefreshThemeSongs(IHasThemeMedia item, CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false)
         {
             var newThemeSongs = LoadThemeSongs().ToList();
             var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToList();
 
             var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds);
 
-            var tasks = newThemeSongs.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false));
+            var tasks = newThemeSongs.Select(i => i.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = forceSave,
+                ReplaceAllMetadata = forceRefresh,
+                ResetResolveArgs = false
+
+            }, cancellationToken));
 
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
@@ -1456,7 +1484,13 @@ namespace MediaBrowser.Controller.Entities
 
             // Refresh metadata
             // Need to disable slow providers or the image might get re-downloaded
-            return RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false);
+            return RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = true,
+                ImageRefreshMode = MetadataRefreshMode.None,
+                MetadataRefreshMode = MetadataRefreshMode.None
+
+            }, CancellationToken.None);
         }
 
         /// <summary>
@@ -1482,8 +1516,10 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Validates that images within the item are still on the file system
         /// </summary>
-        public void ValidateImages()
+        public bool ValidateImages()
         {
+            var changed = false;
+
             // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
             var deletedKeys = Images
                 .Where(image => !File.Exists(image.Value))
@@ -1494,14 +1530,28 @@ namespace MediaBrowser.Controller.Entities
             foreach (var key in deletedKeys)
             {
                 Images.Remove(key);
+                changed = true;
             }
+
+            if (ValidateBackdrops())
+            {
+                changed = true;
+            }
+            if (ValidateScreenshots())
+            {
+                changed = true;
+            }
+
+            return changed;
         }
 
         /// <summary>
         /// Validates that backdrops within the item are still on the file system
         /// </summary>
-        public void ValidateBackdrops()
+        private bool ValidateBackdrops()
         {
+            var changed = false;
+
             // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
             var deletedImages = BackdropImagePaths
                 .Where(path => !File.Exists(path))
@@ -1513,7 +1563,11 @@ namespace MediaBrowser.Controller.Entities
                 BackdropImagePaths.Remove(path);
 
                 RemoveImageSourceForPath(path);
+
+                changed = true;
             }
+
+            return changed;
         }
 
         /// <summary>
@@ -1593,9 +1647,16 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// Validates the screenshots.
         /// </summary>
-        public void ValidateScreenshots()
+        private bool ValidateScreenshots()
         {
-            var hasScreenshots = (IHasScreenshots)this;
+            var changed = false;
+
+            var hasScreenshots = this as IHasScreenshots;
+
+            if (hasScreenshots == null)
+            {
+                return changed;
+            }
 
             // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
             var deletedImages = hasScreenshots.ScreenshotImagePaths
@@ -1606,7 +1667,10 @@ namespace MediaBrowser.Controller.Entities
             foreach (var path in deletedImages)
             {
                 hasScreenshots.ScreenshotImagePaths.Remove(path);
+                changed = true;
             }
+
+            return changed;
         }
 
         /// <summary>
@@ -1699,7 +1763,12 @@ namespace MediaBrowser.Controller.Entities
             FileSystem.SwapFiles(file1, file2);
 
             // Directory watchers should repeat this, but do a quick refresh first
-            return RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false);
+            return RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = true,
+                MetadataRefreshMode = MetadataRefreshMode.None
+
+            }, CancellationToken.None);
         }
 
         public virtual bool IsPlayed(User user)

+ 10 - 3
MediaBrowser.Controller/Entities/Folder.cs

@@ -3,6 +3,7 @@ using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using MoreLinq;
@@ -535,7 +536,13 @@ namespace MediaBrowser.Controller.Entities
             try
             {
                 //refresh it
-                await child.RefreshMetadata(cancellationToken, forceSave: currentTuple.Item2, forceRefresh: forceRefreshMetadata, resetResolveArgs: false).ConfigureAwait(false);
+                await child.RefreshMetadata(new MetadataRefreshOptions
+                {
+                    ForceSave = currentTuple.Item2,
+                    ReplaceAllMetadata = forceRefreshMetadata,
+                    ResetResolveArgs = false
+
+                }, cancellationToken).ConfigureAwait(false);
             }
             catch (IOException ex)
             {
@@ -907,9 +914,9 @@ namespace MediaBrowser.Controller.Entities
             return item;
         }
 
-        public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+        public override async Task<bool> RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false)
         {
-            var changed = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
+            var changed = await base.RefreshMetadataDirect(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
 
             return (SupportsShortcutChildren && LocationType == LocationType.FileSystem && RefreshLinkedChildren()) || changed;
         }

+ 26 - 1
MediaBrowser.Controller/Entities/IHasImages.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Model.Entities;
 using System;
+using System.Collections.Generic;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Controller.Entities
@@ -10,7 +11,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets the name.
         /// </summary>
         /// <value>The name.</value>
-        string Name { get; }
+        string Name { get; set; }
 
         /// <summary>
         /// Gets the path.
@@ -24,6 +25,12 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The identifier.</value>
         Guid Id { get; }
 
+        /// <summary>
+        /// Gets the type of the location.
+        /// </summary>
+        /// <value>The type of the location.</value>
+        LocationType LocationType { get; }
+
         /// <summary>
         /// Gets the image path.
         /// </summary>
@@ -81,6 +88,24 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// <returns>System.String.</returns>
         string GetPreferredMetadataLanguage();
+
+        /// <summary>
+        /// Validates the images and returns true or false indicating if any were removed.
+        /// </summary>
+        bool ValidateImages();
+
+        /// <summary>
+        /// Gets or sets the backdrop image paths.
+        /// </summary>
+        /// <value>The backdrop image paths.</value>
+        List<string> BackdropImagePaths { get; set; }
+
+        /// <summary>
+        /// Determines whether [contains image with source URL] [the specified URL].
+        /// </summary>
+        /// <param name="url">The URL.</param>
+        /// <returns><c>true</c> if [contains image with source URL] [the specified URL]; otherwise, <c>false</c>.</returns>
+        bool ContainsImageWithSourceUrl(string url);
     }
 
     public static class HasImagesExtensions

+ 4 - 2
MediaBrowser.Controller/Entities/IHasScreenshots.cs

@@ -14,8 +14,10 @@ namespace MediaBrowser.Controller.Entities
         List<string> ScreenshotImagePaths { get; set; }
 
         /// <summary>
-        /// Validates the screenshots.
+        /// Determines whether [contains image with source URL] [the specified URL].
         /// </summary>
-        void ValidateScreenshots();
+        /// <param name="url">The URL.</param>
+        /// <returns><c>true</c> if [contains image with source URL] [the specified URL]; otherwise, <c>false</c>.</returns>
+        bool ContainsImageWithSourceUrl(string url);
     }
 }

+ 12 - 7
MediaBrowser.Controller/Entities/Movies/Movie.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Model.Configuration;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using System;
 using System.Collections.Generic;
@@ -108,13 +109,11 @@ namespace MediaBrowser.Controller.Entities.Movies
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
         /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
-        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
-        /// <param name="resetResolveArgs">The reset resolve args.</param>
         /// <returns>Task{System.Boolean}.</returns>
-        public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+        public override async Task<bool> RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false)
         {
             // Kick off a task to refresh the main item
-            var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
+            var result = await base.RefreshMetadataDirect(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
 
             var specialFeaturesChanged = false;
 
@@ -122,7 +121,7 @@ namespace MediaBrowser.Controller.Entities.Movies
             // In other words, it must be part of the Parent/Child tree
             if (LocationType == LocationType.FileSystem && Parent != null && !IsInMixedFolder)
             {
-                specialFeaturesChanged = await RefreshSpecialFeatures(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                specialFeaturesChanged = await RefreshSpecialFeatures(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
             }
 
             return specialFeaturesChanged || result;
@@ -135,7 +134,13 @@ namespace MediaBrowser.Controller.Entities.Movies
 
             var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds);
 
-            var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false));
+            var tasks = newItems.Select(i => i.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = forceSave,
+                ReplaceAllMetadata = forceRefresh,
+                ResetResolveArgs = false
+
+            }, cancellationToken));
 
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 

+ 11 - 9
MediaBrowser.Controller/Entities/User.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Serialization;
 using System;
@@ -212,7 +213,12 @@ namespace MediaBrowser.Controller.Entities
             // Kick off a task to validate the media library
             Task.Run(() => ValidateMediaLibrary(new Progress<double>(), CancellationToken.None));
 
-            return RefreshMetadata(CancellationToken.None, forceSave: true, forceRefresh: true);
+            return RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = true,
+                ReplaceAllMetadata = true
+
+            }, CancellationToken.None);
         }
 
         /// <summary>
@@ -275,17 +281,13 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
         /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
-        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
         /// <returns>true if a provider reports we changed</returns>
-        public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+        public override async Task<bool> RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false)
         {
-            if (resetResolveArgs)
-            {
-                // Reload this
-                ResetResolveArgs();
-            }
+            // Reload this
+            ResetResolveArgs();
 
-            var updateReason = await ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+            var updateReason = await ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh).ConfigureAwait(false);
 
             var changed = updateReason.HasValue;
 

+ 10 - 6
MediaBrowser.Controller/Entities/Video.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using System;
@@ -164,13 +165,11 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="forceSave">if set to <c>true</c> [is new item].</param>
         /// <param name="forceRefresh">if set to <c>true</c> [force].</param>
-        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
-        /// <param name="resetResolveArgs">The reset resolve args.</param>
         /// <returns>true if a provider reports we changed</returns>
-        public override async Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true)
+        public override async Task<bool> RefreshMetadataDirect(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false)
         {
             // Kick off a task to refresh the main item
-            var result = await base.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs).ConfigureAwait(false);
+            var result = await base.RefreshMetadataDirect(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
 
             var additionalPartsChanged = false;
 
@@ -181,7 +180,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 try
                 {
-                    additionalPartsChanged = await RefreshAdditionalParts(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                    additionalPartsChanged = await RefreshAdditionalParts(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
                 }
                 catch (IOException ex)
                 {
@@ -208,7 +207,12 @@ namespace MediaBrowser.Controller.Entities
 
             var itemsChanged = !AdditionalPartIds.SequenceEqual(newItemIds);
 
-            var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders));
+            var tasks = newItems.Select(i => i.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = forceSave,
+                ReplaceAllMetadata = forceRefresh
+
+            }, cancellationToken));
 
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 

+ 2 - 3
MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using System.Threading;
 using System.Threading.Tasks;
@@ -11,8 +12,6 @@ namespace MediaBrowser.Controller.LiveTv
 
         string MediaType { get; }
 
-        LocationType LocationType { get; }
-
         RecordingInfo RecordingInfo { get; set; }
 
         string GetClientTypeName();
@@ -21,6 +20,6 @@ namespace MediaBrowser.Controller.LiveTv
 
         bool IsParentalAllowed(User user);
 
-        Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool forceSave = false, bool forceRefresh = false, bool allowSlowProviders = true, bool resetResolveArgs = true);
+        Task<bool> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken);
     }
 }

+ 3 - 2
MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs

@@ -1,4 +1,5 @@
-using System.IO;
+using MediaBrowser.Controller.Drawing;
+using System.IO;
 
 namespace MediaBrowser.Controller.LiveTv
 {
@@ -14,6 +15,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// Gets or sets the type of the MIME.
         /// </summary>
         /// <value>The type of the MIME.</value>
-        public string MimeType { get; set; }
+        public ImageFormat Format { get; set; }
     }
 }

+ 8 - 0
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -69,6 +69,7 @@
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
     <Compile Include="Drawing\IImageProcessor.cs" />
+    <Compile Include="Drawing\ImageFormat.cs" />
     <Compile Include="Drawing\ImageProcessingOptions.cs" />
     <Compile Include="Dto\IDtoService.cs" />
     <Compile Include="Entities\AdultVideo.cs" />
@@ -143,8 +144,15 @@
     <Compile Include="Persistence\IFileOrganizationRepository.cs" />
     <Compile Include="Persistence\MediaStreamQuery.cs" />
     <Compile Include="Providers\IDynamicInfoProvider.cs" />
+    <Compile Include="Providers\IHasMetadata.cs" />
     <Compile Include="Providers\IImageProvider.cs" />
+    <Compile Include="Providers\IRemoteImageProvider.cs" />
+    <Compile Include="Providers\ILocalImageProvider.cs" />
+    <Compile Include="Providers\IMetadataProvider.cs" />
+    <Compile Include="Providers\IMetadataService.cs" />
+    <Compile Include="Providers\MetadataRefreshOptions.cs" />
     <Compile Include="Providers\NameParser.cs" />
+    <Compile Include="Providers\ProviderResult.cs" />
     <Compile Include="Session\ISessionManager.cs" />
     <Compile Include="Drawing\ImageExtensions.cs" />
     <Compile Include="Entities\AggregateFolder.cs" />

+ 31 - 0
MediaBrowser.Controller/Providers/IHasMetadata.cs

@@ -0,0 +1,31 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Providers
+{
+    /// <summary>
+    /// Interface IHasMetadata
+    /// </summary>
+    public interface IHasMetadata : IHasImages, IHasProviderIds
+    {
+        /// <summary>
+        /// Gets the preferred metadata country code.
+        /// </summary>
+        /// <returns>System.String.</returns>
+        string GetPreferredMetadataCountryCode();
+
+        /// <summary>
+        /// Gets the locked fields.
+        /// </summary>
+        /// <value>The locked fields.</value>
+        List<MetadataFields> LockedFields { get; }
+
+        /// <summary>
+        /// Gets or sets the date last saved.
+        /// </summary>
+        /// <value>The date last saved.</value>
+        DateTime DateLastSaved { get; set; }
+    }
+}

+ 3 - 25
MediaBrowser.Controller/Providers/IImageProvider.cs

@@ -1,9 +1,4 @@
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
 
 namespace MediaBrowser.Controller.Providers
 {
@@ -26,26 +21,9 @@ namespace MediaBrowser.Controller.Providers
         bool Supports(IHasImages item);
 
         /// <summary>
-        /// Gets the images.
+        /// Gets the order.
         /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the images.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        int Priority { get; }
+        /// <value>The order.</value>
+        int Order { get; }
     }
 }

+ 58 - 0
MediaBrowser.Controller/Providers/ILocalImageProvider.cs

@@ -0,0 +1,58 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+    /// <summary>
+    /// This is just a marker interface
+    /// </summary>
+    public interface ILocalImageProvider : IImageProvider
+    {
+    }
+
+    public interface IImageFileProvider : ILocalImageProvider
+    {
+        List<LocalImageInfo> GetImages(IHasImages item);
+    }
+
+    public class LocalImageInfo
+    {
+        public string Path { get; set; }
+        public ImageType Type { get; set; }
+    }
+
+    public interface IDynamicImageProvider : ILocalImageProvider
+    {
+        /// <summary>
+        /// Gets the images.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>List{DynamicImageInfo}.</returns>
+        List<DynamicImageInfo> GetImageInfos(IHasImages item);
+
+        /// <summary>
+        /// Gets the image.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="info">The information.</param>
+        /// <returns>Task{DynamicImageResponse}.</returns>
+        Task<DynamicImageResponse> GetImage(IHasImages item, DynamicImageInfo info);
+    }
+
+    public class DynamicImageInfo
+    {
+        public string ImageId { get; set; }
+        public ImageType Type { get; set; }
+    }
+
+    public class DynamicImageResponse
+    {
+        public string Path { get; set; }
+        public Stream Stream { get; set; }
+        public ImageFormat Format { get; set; }
+    }
+}

+ 90 - 0
MediaBrowser.Controller/Providers/IMetadataProvider.cs

@@ -0,0 +1,90 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+    /// <summary>
+    /// Marker interface
+    /// </summary>
+    public interface IMetadataProvider
+    {
+        /// <summary>
+        /// Gets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        string Name { get; }
+    }
+
+    public interface IMetadataProvider<TItemType> : IMetadataProvider
+           where TItemType : IHasMetadata
+    {
+    }
+    
+    public interface ILocalMetadataProvider : IMetadataProvider
+    {
+        /// <summary>
+        /// Determines whether [has local metadata] [the specified item].
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns><c>true</c> if [has local metadata] [the specified item]; otherwise, <c>false</c>.</returns>
+        bool HasLocalMetadata(IHasMetadata item);
+    }
+
+    public interface IRemoteMetadataProvider : IMetadataProvider
+    {
+    }
+
+    public interface IRemoteMetadataProvider<TItemType> : IMetadataProvider<TItemType>, IRemoteMetadataProvider
+        where TItemType : IHasMetadata
+    {
+        Task<MetadataResult<TItemType>> GetMetadata(ItemId id, CancellationToken cancellationToken);
+    }
+
+    public interface ILocalMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ILocalMetadataProvider
+         where TItemType : IHasMetadata
+    {
+        Task<MetadataResult<TItemType>> GetMetadata(string path, CancellationToken cancellationToken);
+    }
+
+    public interface IHasChangeMonitor
+    {
+        /// <summary>
+        /// Determines whether the specified date has changed.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="date">The date.</param>
+        /// <returns><c>true</c> if the specified date has changed; otherwise, <c>false</c>.</returns>
+        bool HasChanged(IHasMetadata item, DateTime date);
+    }
+
+    public enum MetadataProviderType
+    {
+        Embedded = 0,
+        Local = 1,
+        Remote = 2
+    }
+
+    public class MetadataResult<T>
+        where T : IHasMetadata
+    {
+        public bool HasMetadata { get; set; }
+        public T Item { get; set; }
+    }
+
+    public class ItemId : IHasProviderIds
+    {
+        public string Name { get; set; }
+        public string MetadataLanguage { get; set; }
+        public string MetadataCountryCode { get; set; }
+
+        public Dictionary<string, string> ProviderIds { get; set; }
+
+        public ItemId()
+        {
+            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
+    }
+}

+ 38 - 0
MediaBrowser.Controller/Providers/IMetadataService.cs

@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+    public interface IMetadataService
+    {
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="providers">The providers.</param>
+        /// <param name="imageProviders">The image providers.</param>
+        void AddParts(IEnumerable<IMetadataProvider> providers, IEnumerable<IImageProvider> imageProviders);
+
+        /// <summary>
+        /// Determines whether this instance can refresh the specified item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns><c>true</c> if this instance can refresh the specified item; otherwise, <c>false</c>.</returns>
+        bool CanRefresh(IHasMetadata item);
+
+        /// <summary>
+        /// Refreshes the metadata.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="options">The options.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the order.
+        /// </summary>
+        /// <value>The order.</value>
+        int Order { get; }
+    }
+}

+ 15 - 5
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -14,15 +14,23 @@ namespace MediaBrowser.Controller.Providers
     /// </summary>
     public interface IProviderManager
     {
+        /// <summary>
+        /// Refreshes the metadata.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="options">The options.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task RefreshMetadata(IHasMetadata item, MetadataRefreshOptions options, CancellationToken cancellationToken);
+
         /// <summary>
         /// Executes the metadata providers.
         /// </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>
-        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
         /// <returns>Task{System.Boolean}.</returns>
-        Task<ItemUpdateType?> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false, bool allowSlowProviders = true);
+        Task<ItemUpdateType?> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false);
 
         /// <summary>
         /// Saves the image.
@@ -54,7 +62,9 @@ namespace MediaBrowser.Controller.Providers
         /// </summary>
         /// <param name="providers">The providers.</param>
         /// <param name="imageProviders">The image providers.</param>
-        void AddParts(IEnumerable<BaseMetadataProvider> providers, IEnumerable<IImageProvider> imageProviders);
+        /// <param name="metadataServices">The metadata services.</param>
+        /// <param name="metadataProviders">The metadata providers.</param>
+        void AddParts(IEnumerable<BaseMetadataProvider> providers, IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders);
 
         /// <summary>
         /// Gets the available remote images.
@@ -70,7 +80,7 @@ namespace MediaBrowser.Controller.Providers
         /// Gets the image providers.
         /// </summary>
         /// <param name="item">The item.</param>
-        /// <returns>IEnumerable{IImageProvider}.</returns>
-        IEnumerable<IImageProvider> GetImageProviders(BaseItem item);
+        /// <returns>IEnumerable{ImageProviderInfo}.</returns>
+        IEnumerable<ImageProviderInfo> GetImageProviderInfo(BaseItem item);
     }
 }

+ 48 - 0
MediaBrowser.Controller/Providers/IRemoteImageProvider.cs

@@ -0,0 +1,48 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+    /// <summary>
+    /// Interface IImageProvider
+    /// </summary>
+    public interface IRemoteImageProvider : IImageProvider
+    {
+        /// <summary>
+        /// Gets the supported images.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>IEnumerable{ImageType}.</returns>
+        IEnumerable<ImageType> GetSupportedImages(IHasImages item);
+        
+        /// <summary>
+        /// Gets the images.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="imageType">Type of the image.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
+        Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the images.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
+        Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the image response.
+        /// </summary>
+        /// <param name="url">The URL.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{HttpResponseInfo}.</returns>
+        Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken);
+    }
+}

+ 49 - 0
MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs

@@ -0,0 +1,49 @@
+using System;
+
+namespace MediaBrowser.Controller.Providers
+{
+    public class MetadataRefreshOptions : ImageRefreshOptions
+    {
+        /// <summary>
+        /// When paired with MetadataRefreshMode=FullRefresh, all existing data will be overwritten with new data from the providers.
+        /// </summary>
+        public bool ReplaceAllMetadata { get; set; }
+
+        public MetadataRefreshMode MetadataRefreshMode { get; set; }
+
+        /// <summary>
+        /// TODO: deprecate. Keeping this for now, for api compatibility
+        /// </summary>
+        [Obsolete]
+        public bool ForceSave { get; set; }
+
+        /// <summary>
+        /// TODO: deprecate. Keeping this for now, for api compatibility
+        /// </summary>
+        [Obsolete]
+        public bool ResetResolveArgs { get; set; }
+    }
+
+    public class ImageRefreshOptions
+    {
+        public MetadataRefreshMode ImageRefreshMode { get; set; }
+    }
+
+    public enum MetadataRefreshMode
+    {
+        /// <summary>
+        /// Providers will be executed based on default rules
+        /// </summary>
+        EnsureMetadata,
+
+        /// <summary>
+        /// No providers will be executed
+        /// </summary>
+        None,
+
+        /// <summary>
+        /// All providers will be executed to search for new metadata
+        /// </summary>
+        FullRefresh
+    }
+}

+ 60 - 0
MediaBrowser.Controller/Providers/ProviderResult.cs

@@ -0,0 +1,60 @@
+using System;
+
+namespace MediaBrowser.Controller.Providers
+{
+    public class ProviderResult
+    {
+        /// <summary>
+        /// Gets or sets the item identifier.
+        /// </summary>
+        /// <value>The item identifier.</value>
+        public Guid ItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance has refreshed metadata.
+        /// </summary>
+        /// <value><c>true</c> if this instance has refreshed metadata; otherwise, <c>false</c>.</value>
+        public bool HasRefreshedMetadata { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance has refreshed images.
+        /// </summary>
+        /// <value><c>true</c> if this instance has refreshed images; otherwise, <c>false</c>.</value>
+        public bool HasRefreshedImages { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date last refreshed.
+        /// </summary>
+        /// <value>The date last refreshed.</value>
+        public DateTime DateLastRefreshed { get; set; }
+
+        /// <summary>
+        /// Gets or sets the last result.
+        /// </summary>
+        /// <value>The last result.</value>
+        public ProviderRefreshStatus Status { get; set; }
+
+        /// <summary>
+        /// Gets or sets the last result error message.
+        /// </summary>
+        /// <value>The last result error message.</value>
+        public string ErrorMessage { get; set; }
+
+        public void AddStatus(ProviderRefreshStatus status, string errorMessage)
+        {
+            if (string.IsNullOrEmpty(ErrorMessage))
+            {
+                ErrorMessage = errorMessage;
+            }
+            if (Status == ProviderRefreshStatus.Success)
+            {
+                Status = status;
+            }
+        }
+
+        public ProviderResult()
+        {
+            Status = ProviderRefreshStatus.Success;
+        }
+    }
+}

+ 6 - 0
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -116,6 +116,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The overview.</value>
         public string Overview { get; set; }
 
+        /// <summary>
+        /// Gets or sets the name of the TMDB collection.
+        /// </summary>
+        /// <value>The name of the TMDB collection.</value>
+        public string TmdbCollectionName { get; set; }
+        
         /// <summary>
         /// Gets or sets the taglines.
         /// </summary>

+ 11 - 0
MediaBrowser.Model/Entities/IHasProviderIds.cs

@@ -20,6 +20,17 @@ namespace MediaBrowser.Model.Entities
     /// </summary>
     public static class ProviderIdsExtensions
     {
+        /// <summary>
+        /// Determines whether [has provider identifier] [the specified instance].
+        /// </summary>
+        /// <param name="instance">The instance.</param>
+        /// <param name="provider">The provider.</param>
+        /// <returns><c>true</c> if [has provider identifier] [the specified instance]; otherwise, <c>false</c>.</returns>
+        public static bool HasProviderId(this IHasProviderIds instance, MetadataProviders provider)
+        {
+            return !string.IsNullOrEmpty(instance.GetProviderId(provider.ToString()));
+        }
+        
         /// <summary>
         /// Gets a provider id
         /// </summary>

+ 3 - 3
MediaBrowser.Model/Providers/ImageProviderInfo.cs

@@ -12,9 +12,9 @@
         public string Name { get; set; }
 
         /// <summary>
-        /// Gets or sets the priority.
+        /// Gets or sets the order.
         /// </summary>
-        /// <value>The priority.</value>
-        public int Priority { get; set; }
+        /// <value>The order.</value>
+        public int Order { get; set; }
     }
 }

+ 5 - 0
MediaBrowser.Model/Querying/ItemFields.cs

@@ -150,6 +150,11 @@ namespace MediaBrowser.Model.Querying
         /// The tags
         /// </summary>
         Tags,
+
+        /// <summary>
+        /// The TMDB collection name
+        /// </summary>
+        TmdbCollectionName,
         
         /// <summary>
         /// The trailer url of the item

+ 327 - 0
MediaBrowser.Providers/All/LocalImageProvider.cs

@@ -0,0 +1,327 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Providers.All
+{
+    public class LocalImageProvider : IImageFileProvider
+    {
+        private readonly IFileSystem _fileSystem;
+
+        public LocalImageProvider(IFileSystem fileSystem)
+        {
+            _fileSystem = fileSystem;
+        }
+
+        public string Name
+        {
+            get { return "Local Images"; }
+        }
+
+        public int Order
+        {
+            get { return 0; }
+        }
+
+        public bool Supports(IHasImages item)
+        {
+            var locationType = item.LocationType;
+
+            if (locationType == LocationType.FileSystem)
+            {
+                // Episode has it's own provider
+                if (item is Episode)
+                {
+                    return false;
+                }
+
+                return true;
+            }
+            if (locationType == LocationType.Virtual)
+            {
+                var season = item as Season;
+
+                if (season != null)
+                {
+                    var series = season.Series;
+
+                    if (series != null && series.LocationType == LocationType.FileSystem)
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        private IEnumerable<string> GetFiles(IHasImages item, bool includeDirectories)
+        {
+            if (item.LocationType != LocationType.FileSystem)
+            {
+                return new List<string>();
+            }
+
+            var path = item.Path;
+            var fileInfo = _fileSystem.GetFileSystemInfo(path) as DirectoryInfo;
+
+            if (fileInfo == null)
+            {
+                path = Path.GetDirectoryName(path);
+            }
+
+            if (includeDirectories)
+            {
+                return Directory.EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly);
+            }
+            return Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly);
+        }
+
+        public List<LocalImageInfo> GetImages(IHasImages item)
+        {
+            var files = GetFileDictionary(GetFiles(item, true));
+
+            var list = new List<LocalImageInfo>();
+
+            PopulateImages(item, list, files);
+
+            return list;
+        }
+
+        private void PopulateImages(IHasImages item, List<LocalImageInfo> images, Dictionary<string, string> files)
+        {
+            var imagePrefix = string.Empty;
+
+            var baseItem = item as BaseItem;
+            if (baseItem != null && baseItem.IsInMixedFolder)
+            {
+                imagePrefix = Path.GetFileNameWithoutExtension(item.Path) + "-";
+            }
+
+            PopulatePrimaryImages(item, images, files, imagePrefix);
+            PopulateBackdrops(item, images, files, imagePrefix);
+            PopulateScreenshots(images, files, imagePrefix);
+
+            AddImage(files, images, imagePrefix + "logo", ImageType.Logo);
+            AddImage(files, images, imagePrefix + "clearart", ImageType.Art);
+            AddImage(files, images, imagePrefix + "disc", ImageType.Disc);
+            AddImage(files, images, imagePrefix + "cdart", ImageType.Disc);
+            AddImage(files, images, imagePrefix + "box", ImageType.Box);
+            AddImage(files, images, imagePrefix + "back", ImageType.BoxRear);
+            AddImage(files, images, imagePrefix + "boxrear", ImageType.BoxRear);
+            AddImage(files, images, imagePrefix + "menu", ImageType.Menu);
+
+            // Banner
+            AddImage(files, images, imagePrefix + "banner", ImageType.Banner);
+
+            // Thumb
+            AddImage(files, images, imagePrefix + "thumb", ImageType.Thumb);
+            AddImage(files, images, imagePrefix + "landscape", ImageType.Thumb);
+
+            var season = item as Season;
+
+            if (season != null)
+            {
+                PopulateSeasonImagesFromSeriesFolder(season, images);
+            }
+        }
+
+        private void PopulatePrimaryImages(IHasImages item, List<LocalImageInfo> images, Dictionary<string, string> files, string imagePrefix)
+        {
+            AddImage(files, images, imagePrefix + "folder", ImageType.Primary);
+            AddImage(files, images, imagePrefix + "cover", ImageType.Primary);
+            AddImage(files, images, imagePrefix + "poster", ImageType.Primary);
+            AddImage(files, images, imagePrefix + "default", ImageType.Primary);
+
+            // Support plex/xbmc convention
+            if (item is Series)
+            {
+                AddImage(files, images, imagePrefix + "show", ImageType.Primary);
+            }
+
+            // Support plex/xbmc convention
+            if (item is Movie || item is MusicVideo || item is AdultVideo)
+            {
+                AddImage(files, images, imagePrefix + "movie", ImageType.Primary);
+            }
+
+            if (string.IsNullOrEmpty(item.Path))
+            {
+                var name = Path.GetFileNameWithoutExtension(item.Path);
+
+                if (!string.IsNullOrEmpty(name))
+                {
+                    AddImage(files, images, name, ImageType.Primary);
+                    AddImage(files, images, name + "-poster", ImageType.Primary);
+                }
+            }
+        }
+
+        private void PopulateBackdrops(IHasImages item, List<LocalImageInfo> images, Dictionary<string, string> files, string imagePrefix)
+        {
+            PopulateBackdrops(images, files, imagePrefix, "backdrop", "backdrop", ImageType.Backdrop);
+
+            if (string.IsNullOrEmpty(item.Path))
+            {
+                var name = Path.GetFileNameWithoutExtension(item.Path);
+
+                if (!string.IsNullOrEmpty(name))
+                {
+                    AddImage(files, images, imagePrefix + name + "-fanart", ImageType.Backdrop);
+                }
+            }
+
+            PopulateBackdrops(images, files, imagePrefix, "fanart", "fanart-", ImageType.Backdrop);
+            PopulateBackdrops(images, files, imagePrefix, "background", "background-", ImageType.Backdrop);
+            PopulateBackdrops(images, files, imagePrefix, "art", "art-", ImageType.Backdrop);
+
+            string extraFanartFolder;
+            if (files.TryGetValue("extrafanart", out extraFanartFolder))
+            {
+                PopulateBackdropsFromExtraFanart(extraFanartFolder, images);
+            }
+        }
+
+        private void PopulateBackdropsFromExtraFanart(string path, List<LocalImageInfo> images)
+        {
+            var imageFiles = Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly)
+                .Where(i =>
+                {
+                    var extension = Path.GetExtension(i);
+
+                    if (string.IsNullOrEmpty(extension))
+                    {
+                        return false;
+                    }
+
+                    return BaseItem.SupportedImageExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+                });
+
+            images.AddRange(imageFiles.Select(i => new LocalImageInfo
+            {
+                Path = i,
+                Type = ImageType.Backdrop
+            }));
+        }
+
+        private void PopulateScreenshots(List<LocalImageInfo> images, Dictionary<string, string> files, string imagePrefix)
+        {
+            PopulateBackdrops(images, files, imagePrefix, "screenshot", "screenshot", ImageType.Screenshot);
+        }
+
+        private void PopulateBackdrops(List<LocalImageInfo> images, Dictionary<string, string> files, string imagePrefix, string firstFileName, string subsequentFileNamePrefix, ImageType type)
+        {
+            AddImage(files, images, imagePrefix + firstFileName, type);
+
+            var unfound = 0;
+            for (var i = 1; i <= 20; i++)
+            {
+                // Screenshot Image
+                var found = AddImage(files, images, imagePrefix + subsequentFileNamePrefix + i, type);
+
+                if (!found)
+                {
+                    unfound++;
+
+                    if (unfound >= 3)
+                    {
+                        break;
+                    }
+                }
+            }
+        }
+
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private void PopulateSeasonImagesFromSeriesFolder(Season season, List<LocalImageInfo> images)
+        {
+            var seasonNumber = season.IndexNumber;
+
+            var series = season.Series;
+            if (!seasonNumber.HasValue || series.LocationType != LocationType.FileSystem)
+            {
+                return;
+            }
+
+            var files = GetFileDictionary(GetFiles(series, false));
+
+            // Try using the season name
+            var prefix = season.Name.ToLower().Replace(" ", string.Empty);
+
+            var filenamePrefixes = new List<string> { prefix };
+
+            var seasonMarker = seasonNumber.Value == 0
+                                   ? "-specials"
+                                   : seasonNumber.Value.ToString("00", _usCulture);
+
+            // Get this one directly from the file system since we have to go up a level
+            if (!string.Equals(prefix, seasonMarker, StringComparison.OrdinalIgnoreCase))
+            {
+                filenamePrefixes.Add("season" + seasonMarker);
+            }
+
+            foreach (var filename in filenamePrefixes)
+            {
+                AddImage(files, images, filename + "-poster", ImageType.Primary);
+                AddImage(files, images, filename + "-fanart", ImageType.Backdrop);
+                AddImage(files, images, filename + "-banner", ImageType.Banner);
+                AddImage(files, images, filename + "-landscape", ImageType.Thumb);
+            }
+        }
+
+        private Dictionary<string, string> GetFileDictionary(IEnumerable<string> paths)
+        {
+            var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+            foreach (var path in paths)
+            {
+                var filename = Path.GetFileName(path);
+
+                if (!string.IsNullOrEmpty(filename))
+                {
+                    dict[filename] = path;
+                }
+            }
+
+            return dict;
+        }
+
+        private bool AddImage(Dictionary<string, string> dict, List<LocalImageInfo> images, string name, ImageType type)
+        {
+            var image = GetImage(dict, name);
+
+            if (image != null)
+            {
+                images.Add(new LocalImageInfo
+                {
+                    Path = image,
+                    Type = type
+                });
+
+                return true;
+            }
+
+            return false;
+        }
+
+        private string GetImage(Dictionary<string, string> dict, string name)
+        {
+            return BaseItem.SupportedImageExtensions
+                .Select(i =>
+                {
+                    var filename = name + i;
+                    string path;
+
+                    return dict.TryGetValue(filename, out path) ? path : null;
+                })
+                .FirstOrDefault(i => i != null);
+        }
+    }
+}

+ 28 - 0
MediaBrowser.Providers/BaseXmlProvider.cs

@@ -0,0 +1,28 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Providers;
+using System;
+using System.Threading;
+
+namespace MediaBrowser.Providers
+{
+    public abstract class BaseXmlProvider: IHasChangeMonitor
+    {
+        protected static readonly SemaphoreSlim XmlParsingResourcePool = new SemaphoreSlim(4, 4);
+
+        protected IFileSystem FileSystem;
+
+        protected BaseXmlProvider(IFileSystem fileSystem)
+        {
+            FileSystem = fileSystem;
+        }
+
+        protected abstract string GetXmlPath(string path);
+
+        public bool HasChanged(IHasMetadata item, DateTime date)
+        {
+            var path = GetXmlPath(item.Path);
+
+            return FileSystem.GetLastWriteTimeUtc(path) > date;
+        }
+    }
+}

+ 2 - 2
MediaBrowser.Providers/CollectionFolderImageProvider.cs

@@ -1,5 +1,4 @@
-using System.Collections.Generic;
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -7,6 +6,7 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 

+ 25 - 4
MediaBrowser.Providers/ImagesByName/GameGenresManualImageProvider.cs → MediaBrowser.Providers/GameGenres/GameGenreImageProvider.cs

@@ -5,15 +5,17 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Genres;
+using MediaBrowser.Providers.ImagesByName;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Providers.ImagesByName
+namespace MediaBrowser.Providers.GameGenres
 {
-    public class GameGenresManualImageProvider : IImageProvider
+    public class GameGenreImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
@@ -21,7 +23,7 @@ namespace MediaBrowser.Providers.ImagesByName
 
         private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1);
 
-        public GameGenresManualImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
+        public GameGenreImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
         {
             _config = config;
             _httpClient = httpClient;
@@ -43,6 +45,15 @@ namespace MediaBrowser.Providers.ImagesByName
             return item is GameGenre;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Thumb
+            };
+        }
+
         public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken);
@@ -120,9 +131,19 @@ namespace MediaBrowser.Providers.ImagesByName
             return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken);
         }
 
-        public int Priority
+        public int Order
         {
             get { return 0; }
         }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = GenreImageProvider.ImageDownloadResourcePool
+            });
+        }
     }
 }

+ 42 - 0
MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs

@@ -0,0 +1,42 @@
+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.Providers.Manager;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.GameGenres
+{
+    public class GameGenreMetadataService : MetadataService<GameGenre>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public GameGenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="lockedFields">The locked fields.</param>
+        /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+        /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
+        protected override void MergeData(GameGenre source, GameGenre target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        }
+
+        protected override Task SaveItem(GameGenre item, ItemUpdateType reason, CancellationToken cancellationToken)
+        {
+            return _libraryManager.UpdateItem(item, reason, cancellationToken);
+        }
+    }
+}

+ 26 - 4
MediaBrowser.Providers/ImagesByName/GenresManualImageProvider.cs → MediaBrowser.Providers/Genres/GenreImageProvider.cs

@@ -5,15 +5,16 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.ImagesByName;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Providers.ImagesByName
+namespace MediaBrowser.Providers.Genres
 {
-    public class GenresManualImageProvider : IImageProvider
+    public class GenreImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
@@ -21,7 +22,9 @@ namespace MediaBrowser.Providers.ImagesByName
 
         private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1);
 
-        public GenresManualImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
+        public static SemaphoreSlim ImageDownloadResourcePool = new SemaphoreSlim(5, 5);
+
+        public GenreImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
         {
             _config = config;
             _httpClient = httpClient;
@@ -43,6 +46,15 @@ namespace MediaBrowser.Providers.ImagesByName
             return item is Genre;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Thumb
+            };
+        }
+
         public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken);
@@ -120,9 +132,19 @@ namespace MediaBrowser.Providers.ImagesByName
             return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken);
         }
 
-        public int Priority
+        public int Order
         {
             get { return 0; }
         }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = ImageDownloadResourcePool
+            });
+        }
     }
 }

+ 42 - 0
MediaBrowser.Providers/Genres/GenreMetadataService.cs

@@ -0,0 +1,42 @@
+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.Providers.Manager;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Genres
+{
+    public class GenreMetadataService : MetadataService<Genre>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public GenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="lockedFields">The locked fields.</param>
+        /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+        /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
+        protected override void MergeData(Genre source, Genre target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        }
+
+        protected override Task SaveItem(Genre item, ItemUpdateType reason, CancellationToken cancellationToken)
+        {
+            return _libraryManager.UpdateItem(item, reason, cancellationToken);
+        }
+    }
+}

+ 0 - 11
MediaBrowser.Providers/ImageFromMediaLocationProvider.cs

@@ -145,17 +145,6 @@ namespace MediaBrowser.Providers
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            // Make sure current backdrop paths still exist
-            item.ValidateBackdrops();
-
-            var hasScreenshots = item as IHasScreenshots;
-            if (hasScreenshots != null)
-            {
-                hasScreenshots.ValidateScreenshots();
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
             var args = GetResolveArgsContainingImages(item);
 
             PopulateBaseItemImages(item, args);

+ 0 - 160
MediaBrowser.Providers/ImagesByName/GameGenreImageProvider.cs

@@ -1,160 +0,0 @@
-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.ImagesByName
-{
-    public class GameGenreImageProvider : BaseMetadataProvider
-    {
-        private readonly IProviderManager _providerManager;
-        private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5);
-
-        public GameGenreImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
-            : base(logManager, configurationManager)
-        {
-            _providerManager = providerManager;
-        }
-
-        public override bool Supports(BaseItem item)
-        {
-            return item is GameGenre;
-        }
-
-        public override bool RequiresInternet
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        public override ItemUpdateType ItemUpdateType
-        {
-            get
-            {
-                return ItemUpdateType.ImageUpdate;
-            }
-        }
-
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb))
-            {
-                return false;
-            }
-
-            // Try again periodically in case new images were added
-            if ((DateTime.UtcNow - providerInfo.LastRefreshed).TotalDays > 7)
-            {
-                return true;
-            }
-
-            return base.NeedsRefreshInternal(item, providerInfo);
-        }
-
-        protected override bool RefreshOnVersionChange
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        protected override string ProviderVersion
-        {
-            get
-            {
-                return "8";
-            }
-        }
-
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb))
-            {
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-                return true;
-            }
-
-            var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, GameGenresManualImageProvider.ProviderName).ConfigureAwait(false);
-
-            await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-
-        private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
-        {
-            if (!item.LockedFields.Contains(MetadataFields.Images))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (!item.HasImage(ImageType.Primary))
-                {
-                    await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false);
-                }
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (!item.HasImage(ImageType.Thumb))
-                {
-                    await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false);
-                }
-            }
-
-            if (!item.LockedFields.Contains(MetadataFields.Backdrops))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (item.BackdropImagePaths.Count == 0)
-                {
-                    foreach (var image in images.Where(i => i.Type == ImageType.Backdrop))
-                    {
-                        await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken)
-                            .ConfigureAwait(false);
-
-                        break;
-                    }
-                }
-            }
-        }
-
-
-        private async Task SaveImage(BaseItem item, IEnumerable<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken)
-        {
-            foreach (var image in images.Where(i => i.Type == type))
-            {
-                try
-                {
-                    await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false);
-                    break;
-                }
-                catch (HttpException ex)
-                {
-                    // Sometimes fanart has bad url's in their xml
-                    if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
-                    {
-                        continue;
-                    }
-                    break;
-                }
-            }
-        }
-
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Third; }
-        }
-    }
-}

+ 0 - 160
MediaBrowser.Providers/ImagesByName/GenreImageProvider.cs

@@ -1,160 +0,0 @@
-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.ImagesByName
-{
-    public class GenreImageProvider : BaseMetadataProvider
-    {
-        private readonly IProviderManager _providerManager;
-        private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5);
-
-        public GenreImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
-            : base(logManager, configurationManager)
-        {
-            _providerManager = providerManager;
-        }
-
-        public override bool Supports(BaseItem item)
-        {
-            return item is Genre;
-        }
-
-        public override bool RequiresInternet
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        public override ItemUpdateType ItemUpdateType
-        {
-            get
-            {
-                return ItemUpdateType.ImageUpdate;
-            }
-        }
-
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb))
-            {
-                return false;
-            }
-
-            // Try again periodically in case new images were added
-            if ((DateTime.UtcNow - providerInfo.LastRefreshed).TotalDays > 7)
-            {
-                return true;
-            }
-
-            return base.NeedsRefreshInternal(item, providerInfo);
-        }
-
-        protected override bool RefreshOnVersionChange
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        protected override string ProviderVersion
-        {
-            get
-            {
-                return "8";
-            }
-        }
-
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb))
-            {
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-                return true;
-            }
-
-            var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, GenresManualImageProvider.ProviderName).ConfigureAwait(false);
-
-            await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-
-        private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
-        {
-            if (!item.LockedFields.Contains(MetadataFields.Images))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (!item.HasImage(ImageType.Primary))
-                {
-                    await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false);
-                }
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (!item.HasImage(ImageType.Thumb))
-                {
-                    await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false);
-                }
-            }
-
-            if (!item.LockedFields.Contains(MetadataFields.Backdrops))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (item.BackdropImagePaths.Count == 0)
-                {
-                    foreach (var image in images.Where(i => i.Type == ImageType.Backdrop))
-                    {
-                        await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken)
-                            .ConfigureAwait(false);
-
-                        break;
-                    }
-                }
-            }
-        }
-
-
-        private async Task SaveImage(BaseItem item, IEnumerable<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken)
-        {
-            foreach (var image in images.Where(i => i.Type == type))
-            {
-                try
-                {
-                    await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false);
-                    break;
-                }
-                catch (HttpException ex)
-                {
-                    // Sometimes fanart has bad url's in their xml
-                    if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
-                    {
-                        continue;
-                    }
-                    break;
-                }
-            }
-        }
-
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Third; }
-        }
-    }
-}

+ 0 - 161
MediaBrowser.Providers/ImagesByName/MusicGenreImageProvider.cs

@@ -1,161 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-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.ImagesByName
-{
-    public class MusicGenreImageProvider : BaseMetadataProvider
-    {
-        private readonly IProviderManager _providerManager;
-        private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5);
-
-        public MusicGenreImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
-            : base(logManager, configurationManager)
-        {
-            _providerManager = providerManager;
-        }
-
-        public override bool Supports(BaseItem item)
-        {
-            return item is MusicGenre;
-        }
-
-        public override bool RequiresInternet
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        public override ItemUpdateType ItemUpdateType
-        {
-            get
-            {
-                return ItemUpdateType.ImageUpdate;
-            }
-        }
-
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb))
-            {
-                return false;
-            }
-
-            // Try again periodically in case new images were added
-            if ((DateTime.UtcNow - providerInfo.LastRefreshed).TotalDays > 7)
-            {
-                return true;
-            }
-
-            return base.NeedsRefreshInternal(item, providerInfo);
-        }
-
-        protected override bool RefreshOnVersionChange
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        protected override string ProviderVersion
-        {
-            get
-            {
-                return "8";
-            }
-        }
-
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb))
-            {
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-                return true;
-            }
-
-            var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, MusicGenresManualImageProvider.ProviderName).ConfigureAwait(false);
-
-            await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-
-        private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
-        {
-            if (!item.LockedFields.Contains(MetadataFields.Images))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (!item.HasImage(ImageType.Primary))
-                {
-                    await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false);
-                }
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (!item.HasImage(ImageType.Thumb))
-                {
-                    await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false);
-                }
-            }
-
-            if (!item.LockedFields.Contains(MetadataFields.Backdrops))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (item.BackdropImagePaths.Count == 0)
-                {
-                    foreach (var image in images.Where(i => i.Type == ImageType.Backdrop))
-                    {
-                        await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken)
-                            .ConfigureAwait(false);
-
-                        break;
-                    }
-                }
-            }
-        }
-
-
-        private async Task SaveImage(BaseItem item, IEnumerable<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken)
-        {
-            foreach (var image in images.Where(i => i.Type == type))
-            {
-                try
-                {
-                    await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false);
-                    break;
-                }
-                catch (HttpException ex)
-                {
-                    // Sometimes fanart has bad url's in their xml
-                    if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
-                    {
-                        continue;
-                    }
-                    break;
-                }
-            }
-        }
-
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Third; }
-        }
-    }
-}

+ 0 - 160
MediaBrowser.Providers/ImagesByName/StudioImageProvider.cs

@@ -1,160 +0,0 @@
-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.ImagesByName
-{
-    public class StudioImageProvider : BaseMetadataProvider
-    {
-        private readonly IProviderManager _providerManager;
-        private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(5, 5);
-
-        public StudioImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
-            : base(logManager, configurationManager)
-        {
-            _providerManager = providerManager;
-        }
-
-        public override bool Supports(BaseItem item)
-        {
-            return item is Studio;
-        }
-
-        public override bool RequiresInternet
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        public override ItemUpdateType ItemUpdateType
-        {
-            get
-            {
-                return ItemUpdateType.ImageUpdate;
-            }
-        }
-
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb))
-            {
-                return false;
-            }
-
-            // Try again periodically in case new images were added
-            if ((DateTime.UtcNow - providerInfo.LastRefreshed).TotalDays > 7)
-            {
-                return true;
-            }
-
-            return base.NeedsRefreshInternal(item, providerInfo);
-        }
-
-        protected override bool RefreshOnVersionChange
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        protected override string ProviderVersion
-        {
-            get
-            {
-                return "6";
-            }
-        }
-
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Thumb))
-            {
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-                return true;
-            }
-
-            var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, StudiosManualImageProvider.ProviderName).ConfigureAwait(false);
-
-            await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-
-        private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
-        {
-            if (!item.LockedFields.Contains(MetadataFields.Images))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (!item.HasImage(ImageType.Primary))
-                {
-                    await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false);
-                }
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (!item.HasImage(ImageType.Thumb))
-                {
-                    await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false);
-                }
-            }
-
-            if (!item.LockedFields.Contains(MetadataFields.Backdrops))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (item.BackdropImagePaths.Count == 0)
-                {
-                    foreach (var image in images.Where(i => i.Type == ImageType.Backdrop))
-                    {
-                        await _providerManager.SaveImage(item, image.Url, _resourcePool, ImageType.Backdrop, null, cancellationToken)
-                            .ConfigureAwait(false);
-
-                        break;
-                    }
-                }
-            }
-        }
-
-
-        private async Task SaveImage(BaseItem item, IEnumerable<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken)
-        {
-            foreach (var image in images.Where(i => i.Type == type))
-            {
-                try
-                {
-                    await _providerManager.SaveImage(item, image.Url, _resourcePool, type, null, cancellationToken).ConfigureAwait(false);
-                    break;
-                }
-                catch (HttpException ex)
-                {
-                    // Sometimes fanart has bad url's in their xml
-                    if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
-                    {
-                        continue;
-                    }
-                    break;
-                }
-            }
-        }
-
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Third; }
-        }
-    }
-}

+ 1 - 1
MediaBrowser.Server.Implementations/Providers/ImageSaver.cs → MediaBrowser.Providers/Manager/ImageSaver.cs

@@ -15,7 +15,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Server.Implementations.Providers
+namespace MediaBrowser.Providers.Manager
 {
     /// <summary>
     /// Class ImageSaver

+ 431 - 0
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;
+        }
+    }
+}

+ 367 - 0
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; }
+    }
+}

+ 55 - 22
MediaBrowser.Server.Implementations/Providers/ProviderManager.cs → MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -2,7 +2,6 @@
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
@@ -17,7 +16,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Server.Implementations.Providers
+namespace MediaBrowser.Providers.Manager
 {
     /// <summary>
     /// Class ProviderManager
@@ -51,11 +50,15 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <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>
@@ -63,6 +66,8 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <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");
@@ -78,11 +83,34 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// </summary>
         /// <param name="providers">The providers.</param>
         /// <param name="imageProviders">The image providers.</param>
-        public void AddParts(IEnumerable<BaseMetadataProvider> providers, IEnumerable<IImageProvider> imageProviders)
+        /// <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.OrderByDescending(i => i.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>
@@ -91,9 +119,9 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="item">The item.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="force">if set to <c>true</c> [force].</param>
-        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
         /// <returns>Task{System.Boolean}.</returns>
-        public async Task<ItemUpdateType?> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false, bool allowSlowProviders = true)
+        /// <exception cref="System.ArgumentNullException">item</exception>
+        public async Task<ItemUpdateType?> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false)
         {
             if (item == null)
             {
@@ -126,12 +154,6 @@ namespace MediaBrowser.Server.Implementations.Providers
                     continue;
                 }
 
-                // Skip if is slow and we aren't allowing slow ones
-                if (provider.IsSlow && !allowSlowProviders)
-                {
-                    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)
@@ -371,7 +393,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
         public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, CancellationToken cancellationToken, string providerName = null, ImageType? type = null)
         {
-            var providers = GetImageProviders(item);
+            var providers = GetRemoteImageProviders(item);
 
             if (!string.IsNullOrEmpty(providerName))
             {
@@ -396,7 +418,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <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, IImageProvider i, string preferredLanguage, ImageType? type = null)
+        private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken, IRemoteImageProvider i, string preferredLanguage, ImageType? type = null)
         {
             try
             {
@@ -414,7 +436,7 @@ namespace MediaBrowser.Server.Implementations.Providers
             }
             catch (Exception ex)
             {
-                _logger.ErrorException("{0} failed in GetImages for type {1}", ex, i.GetType().Name, item.GetType().Name);
+                _logger.ErrorException("{0} failed in GetImageInfos for type {1}", ex, i.GetType().Name, item.GetType().Name);
                 return new List<RemoteImageInfo>();
             }
         }
@@ -430,14 +452,9 @@ namespace MediaBrowser.Server.Implementations.Providers
             return images;
         }
 
-        /// <summary>
-        /// Gets the supported image providers.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>IEnumerable{IImageProvider}.</returns>
-        public IEnumerable<IImageProvider> GetImageProviders(BaseItem item)
+        private IEnumerable<IRemoteImageProvider> GetRemoteImageProviders(BaseItem item)
         {
-            return ImageProviders.Where(i =>
+            return RemoteImageProviders.Where(i =>
             {
                 try
                 {
@@ -448,6 +465,22 @@ namespace MediaBrowser.Server.Implementations.Providers
                     _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
+
             });
         }
     }

+ 21 - 14
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -64,6 +64,14 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="All\LocalImageProvider.cs" />
+    <Compile Include="GameGenres\GameGenreMetadataService.cs" />
+    <Compile Include="Genres\GenreMetadataService.cs" />
+    <Compile Include="Manager\ImageSaver.cs" />
+    <Compile Include="Manager\ItemImageProvider.cs" />
+    <Compile Include="Manager\ProviderManager.cs" />
+    <Compile Include="Manager\MetadataService.cs" />
+    <Compile Include="BaseXmlProvider.cs" />
     <Compile Include="CollectionFolderImageProvider.cs" />
     <Compile Include="FanartBaseProvider.cs" />
     <Compile Include="FolderProviderFromXml.cs" />
@@ -72,12 +80,9 @@
     <Compile Include="Games\GameSystemProviderFromXml.cs" />
     <Compile Include="ImageFromMediaLocationProvider.cs" />
     <Compile Include="ImagesByNameProvider.cs" />
-    <Compile Include="ImagesByName\MusicGenreImageProvider.cs" />
-    <Compile Include="ImagesByName\MusicGenresManualImageProvider.cs" />
-    <Compile Include="ImagesByName\GameGenreImageProvider.cs" />
-    <Compile Include="ImagesByName\GameGenresManualImageProvider.cs" />
-    <Compile Include="ImagesByName\GenreImageProvider.cs" />
-    <Compile Include="ImagesByName\GenresManualImageProvider.cs" />
+    <Compile Include="MusicGenres\MusicGenreImageProvider.cs" />
+    <Compile Include="GameGenres\GameGenreImageProvider.cs" />
+    <Compile Include="Genres\GenreImageProvider.cs" />
     <Compile Include="ImagesByName\ImageUtils.cs" />
     <Compile Include="LiveTv\ChannelProviderFromXml.cs" />
     <Compile Include="MediaInfo\AudioImageProvider.cs" />
@@ -88,8 +93,8 @@
     <Compile Include="Movies\BoxSetProviderFromXml.cs" />
     <Compile Include="Movies\ManualMovieDbImageProvider.cs" />
     <Compile Include="Movies\ManualFanartMovieImageProvider.cs" />
-    <Compile Include="Movies\ManualMovieDbPersonImageProvider.cs" />
-    <Compile Include="Movies\MovieDbPersonImageProvider.cs" />
+    <Compile Include="MusicGenres\MusicGenreMetadataService.cs" />
+    <Compile Include="People\MovieDbPersonImageProvider.cs" />
     <Compile Include="Movies\MovieUpdatesPrescanTask.cs" />
     <Compile Include="Movies\MovieXmlParser.cs" />
     <Compile Include="Movies\FanArtMovieProvider.cs" />
@@ -98,8 +103,6 @@
     <Compile Include="Movies\MovieDbProvider.cs" />
     <Compile Include="Movies\MovieProviderFromXml.cs" />
     <Compile Include="Movies\OpenMovieDatabaseProvider.cs" />
-    <Compile Include="Movies\PersonProviderFromXml.cs" />
-    <Compile Include="Movies\MovieDbPersonProvider.cs" />
     <Compile Include="Music\AlbumInfoFromSongProvider.cs" />
     <Compile Include="Music\AlbumProviderFromXml.cs" />
     <Compile Include="Music\ArtistInfoFromSongProvider.cs" />
@@ -118,7 +121,11 @@
     <Compile Include="Music\MusicBrainzAlbumProvider.cs" />
     <Compile Include="Music\MusicVideoXmlParser.cs" />
     <Compile Include="Music\SoundtrackPostScanTask.cs" />
+    <Compile Include="People\PersonMetadataService.cs" />
+    <Compile Include="People\PersonXmlProvider.cs" />
+    <Compile Include="People\MovieDbPersonProvider.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="ProviderUtils.cs" />
     <Compile Include="RefreshIntrosTask.cs" />
     <Compile Include="Savers\AlbumXmlSaver.cs" />
     <Compile Include="Savers\ArtistXmlSaver.cs" />
@@ -133,8 +140,8 @@
     <Compile Include="Savers\SeasonXmlSaver.cs" />
     <Compile Include="Savers\SeriesXmlSaver.cs" />
     <Compile Include="Savers\XmlSaverHelpers.cs" />
-    <Compile Include="ImagesByName\StudioImageProvider.cs" />
-    <Compile Include="ImagesByName\StudiosManualImageProvider.cs" />
+    <Compile Include="Studios\StudiosImageProvider.cs" />
+    <Compile Include="Studios\StudioMetadataService.cs" />
     <Compile Include="TV\EpisodeImageFromMediaLocationProvider.cs" />
     <Compile Include="TV\EpisodeIndexNumberProvider.cs" />
     <Compile Include="TV\EpisodeProviderFromXml.cs" />
@@ -145,7 +152,7 @@
     <Compile Include="TV\ManualFanartSeasonProvider.cs" />
     <Compile Include="TV\ManualFanartSeriesProvider.cs" />
     <Compile Include="TV\ManualTvdbEpisodeImageProvider.cs" />
-    <Compile Include="TV\ManualTvdbPersonImageProvider.cs" />
+    <Compile Include="People\TvdbPersonImageProvider.cs" />
     <Compile Include="TV\ManualTvdbSeasonImageProvider.cs" />
     <Compile Include="TV\ManualTvdbSeriesImageProvider.cs" />
     <Compile Include="TV\SeasonIndexNumberProvider.cs" />
@@ -157,7 +164,6 @@
     <Compile Include="TV\SeriesPostScanTask.cs" />
     <Compile Include="TV\SeriesProviderFromXml.cs" />
     <Compile Include="TV\SeriesXmlParser.cs" />
-    <Compile Include="TV\TvdbPersonImageProvider.cs" />
     <Compile Include="TV\TvdbPrescanTask.cs" />
     <Compile Include="TV\TvdbSeriesImageProvider.cs" />
     <Compile Include="UserRootFolderNameProvider.cs" />
@@ -180,6 +186,7 @@
   <ItemGroup>
     <None Include="packages.config" />
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 31 - 4
MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
@@ -16,14 +17,16 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.Movies
 {
-    public class ManualFanartMovieImageProvider : IImageProvider
+    public class ManualFanartMovieImageProvider : IRemoteImageProvider
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
-        public ManualFanartMovieImageProvider(IServerConfigurationManager config)
+        public ManualFanartMovieImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -41,6 +44,20 @@ namespace MediaBrowser.Providers.Movies
             return FanArtMovieProvider.SupportsItem(item);
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Thumb,
+                ImageType.Art,
+                ImageType.Logo,
+                ImageType.Disc,
+                ImageType.Banner,
+                ImageType.Backdrop
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -294,9 +311,19 @@ namespace MediaBrowser.Providers.Movies
             }
         }
 
-        public int Priority
+        public int Order
         {
             get { return 1; }
         }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = FanartBaseProvider.FanArtResourcePool
+            });
+        }
     }
 }

+ 27 - 5
MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
@@ -14,15 +15,17 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Providers.Movies
 {
-    class ManualMovieDbImageProvider : IImageProvider
+    class ManualMovieDbImageProvider : IRemoteImageProvider
     {
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
-        public ManualMovieDbImageProvider(IJsonSerializer jsonSerializer, IServerConfigurationManager config)
+        public ManualMovieDbImageProvider(IJsonSerializer jsonSerializer, IServerConfigurationManager config, IHttpClient httpClient)
         {
             _jsonSerializer = jsonSerializer;
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -40,6 +43,15 @@ namespace MediaBrowser.Providers.Movies
             return MovieDbImagesProvider.SupportsItem(item);
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Backdrop
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -167,9 +179,19 @@ namespace MediaBrowser.Providers.Movies
             return null;
         }
 
-        public int Priority
+        public int Order
         {
-            get { return 2; }
+            get { return 0; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = MovieDbProvider.Current.MovieDbResourcePool
+            });
         }
     }
 }

+ 0 - 207
MediaBrowser.Providers/Movies/MovieDbPersonImageProvider.cs

@@ -1,207 +0,0 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
-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.Providers;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Providers.Movies
-{
-    /// <summary>
-    /// Class MovieDbPersonImageProvider.
-    /// </summary>
-    public class MovieDbPersonImageProvider : BaseMetadataProvider
-    {
-        /// <summary>
-        /// The _provider manager
-        /// </summary>
-        private readonly IProviderManager _providerManager;
-
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MediaBrowser.Providers.Movies.MovieDbImagesProvider"/> class.
-        /// </summary>
-        /// <param name="logManager">The log manager.</param>
-        /// <param name="configurationManager">The configuration manager.</param>
-        /// <param name="providerManager">The provider manager.</param>
-        public MovieDbPersonImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem)
-            : base(logManager, configurationManager)
-        {
-            _providerManager = providerManager;
-            _fileSystem = fileSystem;
-        }
-
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Third; }
-        }
-
-        /// <summary>
-        /// Supports the specified item.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        public override bool Supports(BaseItem item)
-        {
-            return item is Person;
-        }
-
-        public override ItemUpdateType ItemUpdateType
-        {
-            get
-            {
-                return ItemUpdateType.ImageUpdate;
-            }
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether [requires internet].
-        /// </summary>
-        /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
-        public override bool RequiresInternet
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether [refresh on version change].
-        /// </summary>
-        /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
-        protected override bool RefreshOnVersionChange
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        /// <summary>
-        /// Gets the provider version.
-        /// </summary>
-        /// <value>The provider version.</value>
-        protected override string ProviderVersion
-        {
-            get
-            {
-                return "3";
-            }
-        }
-
-        /// <summary>
-        /// Needses the refresh internal.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="providerInfo">The provider info.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            if (string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)))
-            {
-                return false;
-            }
-
-            // Don't refresh if we already have both poster and backdrop and we're not refreshing images
-            if (item.HasImage(ImageType.Primary))
-            {
-                return false;
-            }
-
-            return base.NeedsRefreshInternal(item, providerInfo);
-        }
-
-        /// <summary>
-        /// Needses the refresh based on compare date.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="providerInfo">The provider info.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            var provderId = item.GetProviderId(MetadataProviders.Tmdb);
-
-            if (!string.IsNullOrEmpty(provderId))
-            {
-                // Process images
-                var path = MovieDbPersonProvider.GetPersonDataFilePath(ConfigurationManager.ApplicationPaths, provderId);
-
-                var fileInfo = new FileInfo(path);
-
-                if (fileInfo.Exists)
-                {
-                    return _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed;
-                }
-
-                return false;
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="force">if set to <c>true</c> [force].</param>
-        /// <param name="cancellationToken">The cancellation token</param>
-        /// <returns>Task{System.Boolean}.</returns>
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualMovieDbPersonImageProvider.ProviderName).ConfigureAwait(false);
-            await ProcessImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-
-        /// <summary>
-        /// Processes the images.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="images">The images.</param>
-        /// <param name="cancellationToken">The cancellation token</param>
-        /// <returns>Task.</returns>
-        private async Task ProcessImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            var eligiblePosters = images
-                .Where(i => i.Type == ImageType.Primary)
-                .ToList();
-
-            //        poster
-            if (eligiblePosters.Count > 0 && !item.HasImage(ImageType.Primary) && !item.LockedFields.Contains(MetadataFields.Images))
-            {
-                var poster = eligiblePosters[0];
-
-                var url = poster.Url;
-
-                var img = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-                {
-                    Url = url,
-                    CancellationToken = cancellationToken
-
-                }).ConfigureAwait(false);
-
-                await _providerManager.SaveImage(item, img, MimeTypes.GetMimeType(url), ImageType.Primary, null, url, cancellationToken)
-                                    .ConfigureAwait(false);
-            }
-        }
-    }
-}

+ 0 - 440
MediaBrowser.Providers/Movies/MovieDbPersonProvider.cs

@@ -1,440 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
-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.Serialization;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Providers.Movies
-{
-    /// <summary>
-    /// Class TmdbPersonProvider
-    /// </summary>
-    public class MovieDbPersonProvider : BaseMetadataProvider
-    {
-        protected readonly IProviderManager ProviderManager;
-
-        internal static MovieDbPersonProvider Current { get; private set; }
-
-        const string DataFileName = "info.json";
-        private readonly IFileSystem _fileSystem;
-
-        public MovieDbPersonProvider(IJsonSerializer jsonSerializer, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem)
-            : base(logManager, configurationManager)
-        {
-            if (jsonSerializer == null)
-            {
-                throw new ArgumentNullException("jsonSerializer");
-            }
-            JsonSerializer = jsonSerializer;
-            ProviderManager = providerManager;
-            _fileSystem = fileSystem;
-            Current = this;
-        }
-
-        /// <summary>
-        /// Gets the json serializer.
-        /// </summary>
-        /// <value>The json serializer.</value>
-        protected IJsonSerializer JsonSerializer { get; private set; }
-
-        /// <summary>
-        /// Supportses the specified item.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        public override bool Supports(BaseItem item)
-        {
-            return item is Person;
-        }
-
-        protected override bool RefreshOnVersionChange
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        protected override string ProviderVersion
-        {
-            get
-            {
-                return "3";
-            }
-        }
-
-        public override ItemUpdateType ItemUpdateType
-        {
-            get
-            {
-                return ItemUpdateType.MetadataDownload;
-            }
-        }
-
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            if (HasAltMeta(item))
-                return false;
-
-            return base.NeedsRefreshInternal(item, providerInfo);
-        }
-
-        protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            var provderId = item.GetProviderId(MetadataProviders.Tmdb);
-
-            if (!string.IsNullOrEmpty(provderId))
-            {
-                // Process images
-                var path = GetPersonDataPath(ConfigurationManager.ApplicationPaths, provderId);
-
-                var file = Path.Combine(path, DataFileName);
-                var fileInfo = new FileInfo(file);
-
-                if (fileInfo.Exists)
-                {
-                    return _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed;
-                }
-
-                return true;
-            }
-
-            return base.NeedsRefreshBasedOnCompareDate(item, providerInfo);
-        }
-
-        internal static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
-        {
-            var letter = tmdbId.GetMD5().ToString().Substring(0, 1);
-
-            var seriesDataPath = Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId);
-
-            return seriesDataPath;
-        }
-
-        internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)
-        {
-            var letter = tmdbId.GetMD5().ToString().Substring(0, 1);
-
-            var seriesDataPath = Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId);
-
-            return Path.Combine(seriesDataPath, DataFileName);
-        }
-
-        internal static string GetPersonsDataPath(IApplicationPaths appPaths)
-        {
-            var dataPath = Path.Combine(appPaths.DataPath, "tmdb-people");
-
-            return dataPath;
-        }
-
-        private bool HasAltMeta(BaseItem item)
-        {
-            return item.LocationType == LocationType.FileSystem && item.ResolveArgs.ContainsMetaFileByName("person.xml");
-        }
-
-        /// <summary>
-        /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="force">if set to <c>true</c> [force].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{System.Boolean}.</returns>
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            var person = (Person)item;
-
-            var id = person.GetProviderId(MetadataProviders.Tmdb);
-
-            // We don't already have an Id, need to fetch it
-            if (string.IsNullOrEmpty(id))
-            {
-                id = await GetTmdbId(item, cancellationToken).ConfigureAwait(false);
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            if (!string.IsNullOrEmpty(id))
-            {
-                await FetchInfo(person, id, force, cancellationToken).ConfigureAwait(false);
-            }
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Second; }
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether [requires internet].
-        /// </summary>
-        /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
-        public override bool RequiresInternet
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
-        /// <summary>
-        /// Gets the TMDB id.
-        /// </summary>
-        /// <param name="person">The person.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{System.String}.</returns>
-        private async Task<string> GetTmdbId(BaseItem person, CancellationToken cancellationToken)
-        {
-            string url = string.Format(@"http://api.themoviedb.org/3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(person.Name), MovieDbProvider.ApiKey);
-            PersonSearchResults searchResult = null;
-
-            using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = MovieDbProvider.AcceptHeader
-
-            }).ConfigureAwait(false))
-            {
-                searchResult = JsonSerializer.DeserializeFromStream<PersonSearchResults>(json);
-            }
-
-            return searchResult != null && searchResult.Total_Results > 0 ? searchResult.Results[0].Id.ToString(_usCulture) : null;
-        }
-
-        /// <summary>
-        /// Fetches the info.
-        /// </summary>
-        /// <param name="person">The person.</param>
-        /// <param name="id">The id.</param>
-        /// <param name="isForcedRefresh">if set to <c>true</c> [is forced refresh].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        private async Task FetchInfo(Person person, string id, bool isForcedRefresh, CancellationToken cancellationToken)
-        {
-            await EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false);
-
-            if (isForcedRefresh || !HasAltMeta(person))
-            {
-                var dataFilePath = GetPersonDataFilePath(ConfigurationManager.ApplicationPaths, id);
-
-                var info = JsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
-
-                cancellationToken.ThrowIfCancellationRequested();
-
-                ProcessInfo(person, info);
-            }
-        }
-
-        internal async Task EnsurePersonInfo(string id, CancellationToken cancellationToken)
-        {
-            var personDataPath = GetPersonDataPath(ConfigurationManager.ApplicationPaths, id);
-
-            var fileInfo = _fileSystem.GetFileSystemInfo(personDataPath);
-
-            if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7)
-            {
-                return;
-            }
-
-            var url = string.Format(@"http://api.themoviedb.org/3/person/{1}?api_key={0}&append_to_response=credits,images", MovieDbProvider.ApiKey, id);
-
-            using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                AcceptHeader = MovieDbProvider.AcceptHeader
-
-            }).ConfigureAwait(false))
-            {
-                Directory.CreateDirectory(personDataPath);
-
-                using (var fs = _fileSystem.GetFileStream(Path.Combine(personDataPath, DataFileName), FileMode.Create, FileAccess.Write, FileShare.Read, true))
-                {
-                    await json.CopyToAsync(fs).ConfigureAwait(false);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Processes the info.
-        /// </summary>
-        /// <param name="person">The person.</param>
-        /// <param name="searchResult">The search result.</param>
-        protected void ProcessInfo(Person person, PersonResult searchResult)
-        {
-            if (!person.LockedFields.Contains(MetadataFields.Overview))
-            {
-                person.Overview = searchResult.biography;
-            }
-
-            DateTime date;
-
-            if (DateTime.TryParseExact(searchResult.birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
-            {
-                person.PremiereDate = date.ToUniversalTime();
-            }
-
-            if (DateTime.TryParseExact(searchResult.deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
-            {
-                person.EndDate = date.ToUniversalTime();
-            }
-
-            if (!string.IsNullOrEmpty(searchResult.homepage))
-            {
-                person.HomePageUrl = searchResult.homepage;
-            }
-
-            if (!person.LockedFields.Contains(MetadataFields.ProductionLocations))
-            {
-                if (!string.IsNullOrEmpty(searchResult.place_of_birth))
-                {
-                    person.PlaceOfBirth = searchResult.place_of_birth;
-                }
-            }
-
-            person.SetProviderId(MetadataProviders.Tmdb, searchResult.id.ToString(_usCulture));
-        }
-
-        #region Result Objects
-        /// <summary>
-        /// Class PersonSearchResult
-        /// </summary>
-        public class PersonSearchResult
-        {
-            /// <summary>
-            /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult.
-            /// </summary>
-            /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
-            public bool Adult { get; set; }
-            /// <summary>
-            /// Gets or sets the id.
-            /// </summary>
-            /// <value>The id.</value>
-            public int Id { get; set; }
-            /// <summary>
-            /// Gets or sets the name.
-            /// </summary>
-            /// <value>The name.</value>
-            public string Name { get; set; }
-            /// <summary>
-            /// Gets or sets the profile_ path.
-            /// </summary>
-            /// <value>The profile_ path.</value>
-            public string Profile_Path { get; set; }
-        }
-
-        /// <summary>
-        /// Class PersonSearchResults
-        /// </summary>
-        public class PersonSearchResults
-        {
-            /// <summary>
-            /// Gets or sets the page.
-            /// </summary>
-            /// <value>The page.</value>
-            public int Page { get; set; }
-            /// <summary>
-            /// Gets or sets the results.
-            /// </summary>
-            /// <value>The results.</value>
-            public List<PersonSearchResult> Results { get; set; }
-            /// <summary>
-            /// Gets or sets the total_ pages.
-            /// </summary>
-            /// <value>The total_ pages.</value>
-            public int Total_Pages { get; set; }
-            /// <summary>
-            /// Gets or sets the total_ results.
-            /// </summary>
-            /// <value>The total_ results.</value>
-            public int Total_Results { get; set; }
-        }
-
-        public class Cast
-        {
-            public int id { get; set; }
-            public string title { get; set; }
-            public string character { get; set; }
-            public string original_title { get; set; }
-            public string poster_path { get; set; }
-            public string release_date { get; set; }
-            public bool adult { get; set; }
-        }
-
-        public class Crew
-        {
-            public int id { get; set; }
-            public string title { get; set; }
-            public string original_title { get; set; }
-            public string department { get; set; }
-            public string job { get; set; }
-            public string poster_path { get; set; }
-            public string release_date { get; set; }
-            public bool adult { get; set; }
-        }
-
-        public class Credits
-        {
-            public List<Cast> cast { get; set; }
-            public List<Crew> crew { get; set; }
-        }
-
-        public class Profile
-        {
-            public string file_path { get; set; }
-            public int width { get; set; }
-            public int height { get; set; }
-            public object iso_639_1 { get; set; }
-            public double aspect_ratio { get; set; }
-        }
-
-        public class Images
-        {
-            public List<Profile> profiles { get; set; }
-        }
-
-        public class PersonResult
-        {
-            public bool adult { get; set; }
-            public List<object> also_known_as { get; set; }
-            public string biography { get; set; }
-            public string birthday { get; set; }
-            public string deathday { get; set; }
-            public string homepage { get; set; }
-            public int id { get; set; }
-            public string imdb_id { get; set; }
-            public string name { get; set; }
-            public string place_of_birth { get; set; }
-            public double popularity { get; set; }
-            public string profile_path { get; set; }
-            public Credits credits { get; set; }
-            public Images images { get; set; }
-        }
-
-        #endregion
-    }
-}

+ 0 - 89
MediaBrowser.Providers/Movies/PersonProviderFromXml.cs

@@ -1,89 +0,0 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Logging;
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Providers.Movies
-{
-    class PersonProviderFromXml : BaseMetadataProvider
-    {
-        private readonly IFileSystem _fileSystem;
-
-        public PersonProviderFromXml(ILogManager logManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem)
-            : base(logManager, configurationManager)
-        {
-            _fileSystem = fileSystem;
-        }
-
-        /// <summary>
-        /// Supportses the specified item.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        public override bool Supports(BaseItem item)
-        {
-            return item is Person;
-        }
-
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Second; }
-        }
-
-        private const string XmlFileName = "person.xml";
-        protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            var xml = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, XmlFileName));
-
-            if (xml == null)
-            {
-                return false;
-            }
-
-            return _fileSystem.GetLastWriteTimeUtc(xml) > item.DateLastSaved;
-        }
-
-        /// <summary>
-        /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="force">if set to <c>true</c> [force].</param>
-        /// <param name="providerInfo">The provider information.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{System.Boolean}.</returns>
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, XmlFileName));
-
-            if (metadataFile != null)
-            {
-                var path = metadataFile.FullName;
-
-                await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-                try
-                {
-                    new BaseItemXmlParser<Person>(Logger).Fetch((Person)item, path, cancellationToken);
-                }
-                finally
-                {
-                    XmlParsingResourcePool.Release();
-                }
-            }
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-    }
-}

+ 27 - 5
MediaBrowser.Providers/Music/ManualFanartAlbumProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
@@ -17,14 +18,16 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.Music
 {
-    public class ManualFanartAlbumProvider : IImageProvider
+    public class ManualFanartAlbumProvider : IRemoteImageProvider
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
-        public ManualFanartAlbumProvider(IServerConfigurationManager config)
+        public ManualFanartAlbumProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -42,6 +45,15 @@ namespace MediaBrowser.Providers.Music
             return item is MusicAlbum;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Disc
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -325,9 +337,19 @@ namespace MediaBrowser.Providers.Music
             list.Add(info);
         }
 
-        public int Priority
+        public int Order
         {
-            get { return 1; }
+            get { return 0; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = FanartBaseProvider.FanArtResourcePool
+            });
         }
     }
 }

+ 30 - 5
MediaBrowser.Providers/Music/ManualFanartArtistProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
@@ -17,14 +18,16 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.Music
 {
-    public class ManualFanartArtistProvider : IImageProvider
+    public class ManualFanartArtistProvider : IRemoteImageProvider
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
-        public ManualFanartArtistProvider(IServerConfigurationManager config)
+        public ManualFanartArtistProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -42,6 +45,18 @@ namespace MediaBrowser.Providers.Music
             return item is MusicArtist;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Logo,
+                ImageType.Art,
+                ImageType.Banner,
+                ImageType.Backdrop
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -334,9 +349,19 @@ namespace MediaBrowser.Providers.Music
             list.Add(info);
         }
 
-        public int Priority
+        public int Order
         {
-            get { return 1; }
+            get { return 0; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = FanartBaseProvider.FanArtResourcePool
+            });
         }
     }
 }

+ 32 - 5
MediaBrowser.Providers/Music/ManualLastFmImageProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -11,8 +12,15 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Providers.Music
 {
-    public class ManualLastFmImageProvider : IImageProvider
+    public class ManualLastFmImageProvider : IRemoteImageProvider
     {
+        private readonly IHttpClient _httpClient;
+
+        public ManualLastFmImageProvider(IHttpClient httpClient)
+        {
+            _httpClient = httpClient;
+        }
+
         public string Name
         {
             get { return ProviderName; }
@@ -28,6 +36,14 @@ namespace MediaBrowser.Providers.Music
             return item is MusicAlbum || item is MusicArtist;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -72,7 +88,8 @@ namespace MediaBrowser.Providers.Music
             var info = new RemoteImageInfo
             {
                 ProviderName = Name,
-                Url = url
+                Url = url,
+                Type = ImageType.Primary
             };
 
             if (string.Equals(size, "mega", StringComparison.OrdinalIgnoreCase))
@@ -95,9 +112,19 @@ namespace MediaBrowser.Providers.Music
             return info;
         }
 
-        public int Priority
+        public int Order
+        {
+            get { return 1; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
         {
-            get { return 0; }
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = LastfmBaseProvider.LastfmResourcePool
+            });
         }
     }
 }

+ 25 - 4
MediaBrowser.Providers/ImagesByName/MusicGenresManualImageProvider.cs → MediaBrowser.Providers/MusicGenres/MusicGenreImageProvider.cs

@@ -6,15 +6,17 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Genres;
+using MediaBrowser.Providers.ImagesByName;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Providers.ImagesByName
+namespace MediaBrowser.Providers.MusicGenres
 {
-    public class MusicGenresManualImageProvider : IImageProvider
+    public class MusicGenreImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
@@ -22,7 +24,7 @@ namespace MediaBrowser.Providers.ImagesByName
 
         private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1);
 
-        public MusicGenresManualImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
+        public MusicGenreImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
         {
             _config = config;
             _httpClient = httpClient;
@@ -44,6 +46,15 @@ namespace MediaBrowser.Providers.ImagesByName
             return item is MusicGenre;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Thumb
+            };
+        }
+
         public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken);
@@ -121,9 +132,19 @@ namespace MediaBrowser.Providers.ImagesByName
             return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken);
         }
 
-        public int Priority
+        public int Order
         {
             get { return 0; }
         }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = GenreImageProvider.ImageDownloadResourcePool
+            });
+        }
     }
 }

+ 42 - 0
MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs

@@ -0,0 +1,42 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Providers.Manager;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.MusicGenres
+{
+    public class MusicGenreMetadataService : MetadataService<MusicGenre>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public MusicGenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="lockedFields">The locked fields.</param>
+        /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+        /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
+        protected override void MergeData(MusicGenre source, MusicGenre target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        }
+
+        protected override Task SaveItem(MusicGenre item, ItemUpdateType reason, CancellationToken cancellationToken)
+        {
+            return _libraryManager.UpdateItem(item, reason, cancellationToken);
+        }
+    }
+}

+ 27 - 5
MediaBrowser.Providers/Movies/ManualMovieDbPersonImageProvider.cs → MediaBrowser.Providers/People/MovieDbPersonImageProvider.cs

@@ -1,26 +1,30 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Movies;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Providers.Movies
+namespace MediaBrowser.Providers.People
 {
-    public class ManualMovieDbPersonImageProvider : IImageProvider
+    public class MovieDbPersonImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly IJsonSerializer _jsonSerializer;
+        private readonly IHttpClient _httpClient;
 
-        public ManualMovieDbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer)
+        public MovieDbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClient httpClient)
         {
             _config = config;
             _jsonSerializer = jsonSerializer;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -38,6 +42,14 @@ namespace MediaBrowser.Providers.Movies
             return item is Person;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -120,9 +132,19 @@ namespace MediaBrowser.Providers.Movies
             return profile.iso_639_1 == null ? null : profile.iso_639_1.ToString();
         }
 
-        public int Priority
+        public int Order
         {
             get { return 0; }
         }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = MovieDbProvider.Current.MovieDbResourcePool
+            });
+        }
     }
 }

+ 289 - 0
MediaBrowser.Providers/People/MovieDbPersonProvider.cs

@@ -0,0 +1,289 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Movies;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.People
+{
+    public class MovieDbPersonProvider : IRemoteMetadataProvider<Person>
+    {
+        const string DataFileName = "info.json";
+        
+        internal static MovieDbPersonProvider Current { get; private set; }
+        
+        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _configurationManager;
+
+        public MovieDbPersonProvider(IFileSystem fileSystem, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer)
+        {
+            _fileSystem = fileSystem;
+            _configurationManager = configurationManager;
+            _jsonSerializer = jsonSerializer;
+            Current = this;
+        }
+
+        public string Name
+        {
+            get { return "TheMovieDb"; }
+        }
+        
+        public async Task<MetadataResult<Person>> GetMetadata(ItemId id, CancellationToken cancellationToken)
+        {
+            var tmdbId = id.GetProviderId(MetadataProviders.Tmdb);
+
+            // We don't already have an Id, need to fetch it
+            if (string.IsNullOrEmpty(tmdbId))
+            {
+                tmdbId = await GetTmdbId(id.Name, cancellationToken).ConfigureAwait(false);
+            }
+
+            var result = new MetadataResult<Person>();
+
+            if (!string.IsNullOrEmpty(tmdbId))
+            {
+                await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false);
+
+                var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
+
+                var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
+
+                var item = new Person();
+                result.HasMetadata = true;
+
+                item.Name = info.name;
+                item.HomePageUrl = info.homepage;
+                item.PlaceOfBirth = info.place_of_birth;
+                item.Overview = info.biography;
+
+                DateTime date;
+
+                if (DateTime.TryParseExact(info.birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
+                {
+                    item.PremiereDate = date.ToUniversalTime();
+                }
+
+                if (DateTime.TryParseExact(info.deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
+                {
+                    item.EndDate = date.ToUniversalTime();
+                }
+
+                item.SetProviderId(MetadataProviders.Tmdb, info.id.ToString(_usCulture));
+
+                if (!string.IsNullOrEmpty(info.imdb_id))
+                {
+                    item.SetProviderId(MetadataProviders.Imdb, info.imdb_id);
+                }
+
+                result.Item = item;
+            }
+
+            return result;
+        }
+
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        /// <summary>
+        /// Gets the TMDB id.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{System.String}.</returns>
+        private async Task<string> GetTmdbId(string name, CancellationToken cancellationToken)
+        {
+            string url = string.Format(@"http://api.themoviedb.org/3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(name), MovieDbProvider.ApiKey);
+            PersonSearchResults searchResult = null;
+
+            using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+            {
+                Url = url,
+                CancellationToken = cancellationToken,
+                AcceptHeader = MovieDbProvider.AcceptHeader
+
+            }).ConfigureAwait(false))
+            {
+                searchResult = _jsonSerializer.DeserializeFromStream<PersonSearchResults>(json);
+            }
+
+            return searchResult != null && searchResult.Total_Results > 0 ? searchResult.Results[0].Id.ToString(_usCulture) : null;
+        }
+
+        internal async Task EnsurePersonInfo(string id, CancellationToken cancellationToken)
+        {
+            var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, id);
+
+            var fileInfo = _fileSystem.GetFileSystemInfo(dataFilePath);
+
+            if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7)
+            {
+                return;
+            }
+
+            var url = string.Format(@"http://api.themoviedb.org/3/person/{1}?api_key={0}&append_to_response=credits,images", MovieDbProvider.ApiKey, id);
+
+            using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
+            {
+                Url = url,
+                CancellationToken = cancellationToken,
+                AcceptHeader = MovieDbProvider.AcceptHeader
+
+            }).ConfigureAwait(false))
+            {
+                Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
+
+                using (var fs = _fileSystem.GetFileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
+                {
+                    await json.CopyToAsync(fs).ConfigureAwait(false);
+                }
+            }
+        }
+
+        private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
+        {
+            var letter = tmdbId.GetMD5().ToString().Substring(0, 1);
+
+            return Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId);
+        }
+
+        internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)
+        {
+            return Path.Combine(GetPersonDataPath(appPaths, tmdbId), DataFileName);
+        }
+
+        private static string GetPersonsDataPath(IApplicationPaths appPaths)
+        {
+            return Path.Combine(appPaths.DataPath, "tmdb-people");
+        }
+
+        #region Result Objects
+        /// <summary>
+        /// Class PersonSearchResult
+        /// </summary>
+        public class PersonSearchResult
+        {
+            /// <summary>
+            /// Gets or sets a value indicating whether this <see cref="MovieDbPersonProvider.PersonSearchResult" /> is adult.
+            /// </summary>
+            /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+            public bool Adult { get; set; }
+            /// <summary>
+            /// Gets or sets the id.
+            /// </summary>
+            /// <value>The id.</value>
+            public int Id { get; set; }
+            /// <summary>
+            /// Gets or sets the name.
+            /// </summary>
+            /// <value>The name.</value>
+            public string Name { get; set; }
+            /// <summary>
+            /// Gets or sets the profile_ path.
+            /// </summary>
+            /// <value>The profile_ path.</value>
+            public string Profile_Path { get; set; }
+        }
+
+        /// <summary>
+        /// Class PersonSearchResults
+        /// </summary>
+        public class PersonSearchResults
+        {
+            /// <summary>
+            /// Gets or sets the page.
+            /// </summary>
+            /// <value>The page.</value>
+            public int Page { get; set; }
+            /// <summary>
+            /// Gets or sets the results.
+            /// </summary>
+            /// <value>The results.</value>
+            public List<MovieDbPersonProvider.PersonSearchResult> Results { get; set; }
+            /// <summary>
+            /// Gets or sets the total_ pages.
+            /// </summary>
+            /// <value>The total_ pages.</value>
+            public int Total_Pages { get; set; }
+            /// <summary>
+            /// Gets or sets the total_ results.
+            /// </summary>
+            /// <value>The total_ results.</value>
+            public int Total_Results { get; set; }
+        }
+
+        public class Cast
+        {
+            public int id { get; set; }
+            public string title { get; set; }
+            public string character { get; set; }
+            public string original_title { get; set; }
+            public string poster_path { get; set; }
+            public string release_date { get; set; }
+            public bool adult { get; set; }
+        }
+
+        public class Crew
+        {
+            public int id { get; set; }
+            public string title { get; set; }
+            public string original_title { get; set; }
+            public string department { get; set; }
+            public string job { get; set; }
+            public string poster_path { get; set; }
+            public string release_date { get; set; }
+            public bool adult { get; set; }
+        }
+
+        public class Credits
+        {
+            public List<Cast> cast { get; set; }
+            public List<Crew> crew { get; set; }
+        }
+
+        public class Profile
+        {
+            public string file_path { get; set; }
+            public int width { get; set; }
+            public int height { get; set; }
+            public object iso_639_1 { get; set; }
+            public double aspect_ratio { get; set; }
+        }
+
+        public class Images
+        {
+            public List<Profile> profiles { get; set; }
+        }
+
+        public class PersonResult
+        {
+            public bool adult { get; set; }
+            public List<object> also_known_as { get; set; }
+            public string biography { get; set; }
+            public string birthday { get; set; }
+            public string deathday { get; set; }
+            public string homepage { get; set; }
+            public int id { get; set; }
+            public string imdb_id { get; set; }
+            public string name { get; set; }
+            public string place_of_birth { get; set; }
+            public double popularity { get; set; }
+            public string profile_path { get; set; }
+            public Credits credits { get; set; }
+            public Images images { get; set; }
+        }
+
+        #endregion
+    }
+}

+ 47 - 0
MediaBrowser.Providers/People/PersonMetadataService.cs

@@ -0,0 +1,47 @@
+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.Providers.Manager;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.People
+{
+    public class PersonMetadataService : MetadataService<Person>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public PersonMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="lockedFields">The locked fields.</param>
+        /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+        /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
+        protected override void MergeData(Person source, Person target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+            if (replaceData || string.IsNullOrEmpty(target.PlaceOfBirth))
+            {
+                target.PlaceOfBirth = source.PlaceOfBirth;
+            }
+        }
+
+        protected override Task SaveItem(Person item, ItemUpdateType reason, CancellationToken cancellationToken)
+        {
+            return _libraryManager.UpdateItem(item, reason, cancellationToken);
+        }
+    }
+}

+ 64 - 0
MediaBrowser.Providers/People/PersonXmlProvider.cs

@@ -0,0 +1,64 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.People
+{
+    public class PersonXmlProvider : BaseXmlProvider, ILocalMetadataProvider<Person>
+    {
+        private readonly ILogger _logger;
+
+        public PersonXmlProvider(IFileSystem fileSystem, ILogger logger)
+            : base(fileSystem)
+        {
+            _logger = logger;
+        }
+
+        public async Task<MetadataResult<Person>> GetMetadata(string path, CancellationToken cancellationToken)
+        {
+            path = GetXmlPath(path);
+
+            var result = new MetadataResult<Person>();
+
+            await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                var person = new Person();
+
+                new BaseItemXmlParser<Person>(_logger).Fetch(person, path, cancellationToken);
+                result.HasMetadata = true;
+                result.Item = person;
+            }
+            catch (FileNotFoundException)
+            {
+                result.HasMetadata = false;
+            }
+            finally
+            {
+                XmlParsingResourcePool.Release();
+            }
+
+            return result;
+        }
+
+        public string Name
+        {
+            get { return "Media Browser Xml"; }
+        }
+
+        protected override string GetXmlPath(string path)
+        {
+            return Path.Combine(path, "person.xml");
+        }
+
+        public bool HasLocalMetadata(IHasMetadata item)
+        {
+            return File.Exists(GetXmlPath(item.Path));
+        }
+    }
+}

+ 28 - 6
MediaBrowser.Providers/TV/ManualTvdbPersonImageProvider.cs → MediaBrowser.Providers/People/TvdbPersonImageProvider.cs

@@ -1,10 +1,12 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.TV;
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -14,17 +16,19 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.Providers.People
 {
-    public class ManualTvdbPersonImageProvider : IImageProvider
+    public class TvdbPersonImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly ILibraryManager _library;
+        private readonly IHttpClient _httpClient;
 
-        public ManualTvdbPersonImageProvider(IServerConfigurationManager config, ILibraryManager library)
+        public TvdbPersonImageProvider(IServerConfigurationManager config, ILibraryManager library, IHttpClient httpClient)
         {
             _config = config;
             _library = library;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -42,6 +46,14 @@ namespace MediaBrowser.Providers.TV
             return item is Person;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -184,9 +196,19 @@ namespace MediaBrowser.Providers.TV
             return null;
         }
 
-        public int Priority
+        public int Order
         {
-            get { return 0; }
+            get { return 1; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
+            });
         }
     }
 }

+ 126 - 0
MediaBrowser.Providers/ProviderUtils.cs

@@ -0,0 +1,126 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Providers
+{
+    public static class ProviderUtils
+    {
+        public static void MergeBaseItemData(BaseItem source, BaseItem target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            if (!lockedFields.Contains(MetadataFields.Name))
+            {
+                if (replaceData || string.IsNullOrEmpty(target.Name))
+                {
+                    target.Name = source.Name;
+                }
+            }
+
+            if (replaceData || !target.CommunityRating.HasValue)
+            {
+                target.CommunityRating = source.CommunityRating;
+            }
+
+            if (replaceData || !target.EndDate.HasValue)
+            {
+                target.EndDate = source.EndDate;
+            }
+
+            if (!lockedFields.Contains(MetadataFields.Genres))
+            {
+                if (replaceData || target.Genres.Count == 0)
+                {
+                    target.Genres = source.Genres;
+                }
+            }
+
+            if (replaceData || string.IsNullOrEmpty(target.HomePageUrl))
+            {
+                target.HomePageUrl = source.HomePageUrl;
+            }
+
+            if (replaceData || !target.IndexNumber.HasValue)
+            {
+                target.IndexNumber = source.IndexNumber;
+            }
+
+            if (!lockedFields.Contains(MetadataFields.OfficialRating))
+            {
+                if (replaceData || string.IsNullOrEmpty(target.OfficialRating))
+                {
+                    target.OfficialRating = source.OfficialRating;
+                }
+            }
+
+            if (replaceData || string.IsNullOrEmpty(target.OfficialRatingDescription))
+            {
+                target.OfficialRatingDescription = source.OfficialRatingDescription;
+            }
+
+            if (!lockedFields.Contains(MetadataFields.Overview))
+            {
+                if (replaceData || string.IsNullOrEmpty(target.Overview))
+                {
+                    target.Overview = source.Overview;
+                }
+            }
+
+            if (replaceData || !target.ParentIndexNumber.HasValue)
+            {
+                target.ParentIndexNumber = source.ParentIndexNumber;
+            }
+
+            if (!lockedFields.Contains(MetadataFields.Cast))
+            {
+                if (replaceData || target.People.Count == 0)
+                {
+                    target.People = source.People;
+                }
+            }
+
+            if (replaceData || !target.PremiereDate.HasValue)
+            {
+                target.PremiereDate = source.PremiereDate;
+            }
+
+            if (replaceData || !target.ProductionYear.HasValue)
+            {
+                target.ProductionYear = source.ProductionYear;
+            }
+
+            if (!lockedFields.Contains(MetadataFields.Runtime))
+            {
+                if (replaceData || !target.RunTimeTicks.HasValue)
+                {
+                    target.RunTimeTicks = source.RunTimeTicks;
+                }
+            }
+
+            if (!lockedFields.Contains(MetadataFields.Studios))
+            {
+                if (replaceData || target.Studios.Count == 0)
+                {
+                    target.Studios = source.Studios;
+                }
+            }
+
+            if (replaceData || !target.VoteCount.HasValue)
+            {
+                target.VoteCount = source.VoteCount;
+            }
+
+            foreach (var id in source.ProviderIds)
+            {
+                target.ProviderIds[id.Key] = id.Value;
+            }
+
+            if (mergeMetadataSettings)
+            {
+                target.ForcedSortName = source.ForcedSortName;
+                target.LockedFields = source.LockedFields;
+                target.DontFetchMeta = source.DontFetchMeta;
+                target.DisplayMediaType = source.DisplayMediaType;
+            }
+        }
+    }
+}

+ 41 - 0
MediaBrowser.Providers/Studios/StudioMetadataService.cs

@@ -0,0 +1,41 @@
+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.Providers.Manager;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Studios
+{
+    public class StudioMetadataService : MetadataService<Studio>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public StudioMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="lockedFields">The locked fields.</param>
+        /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+        protected override void MergeData(Studio source, Studio target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        }
+
+        protected override Task SaveItem(Studio item, ItemUpdateType reason, CancellationToken cancellationToken)
+        {
+            return _libraryManager.UpdateItem(item, reason, cancellationToken);
+        }
+    }
+}

+ 25 - 4
MediaBrowser.Providers/ImagesByName/StudiosManualImageProvider.cs → MediaBrowser.Providers/Studios/StudiosImageProvider.cs

@@ -5,15 +5,17 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Genres;
+using MediaBrowser.Providers.ImagesByName;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Providers.ImagesByName
+namespace MediaBrowser.Providers.Studios
 {
-    public class StudiosManualImageProvider : IImageProvider
+    public class StudiosImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
@@ -21,7 +23,7 @@ namespace MediaBrowser.Providers.ImagesByName
 
         private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1);
 
-        public StudiosManualImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
+        public StudiosImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
         {
             _config = config;
             _httpClient = httpClient;
@@ -43,6 +45,15 @@ namespace MediaBrowser.Providers.ImagesByName
             return item is Studio;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Thumb
+            };
+        }
+
         public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, cancellationToken);
@@ -120,9 +131,19 @@ namespace MediaBrowser.Providers.ImagesByName
             return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken);
         }
 
-        public int Priority
+        public int Order
         {
             get { return 0; }
         }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = GenreImageProvider.ImageDownloadResourcePool
+            });
+        }
     }
 }

+ 27 - 5
MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -17,14 +18,16 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class ManualFanartSeasonImageProvider : IImageProvider
+    public class ManualFanartSeasonImageProvider : IRemoteImageProvider
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
-        public ManualFanartSeasonImageProvider(IServerConfigurationManager config)
+        public ManualFanartSeasonImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -42,6 +45,15 @@ namespace MediaBrowser.Providers.TV
             return item is Season;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Backdrop, 
+                ImageType.Thumb
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -245,9 +257,19 @@ namespace MediaBrowser.Providers.TV
             }
         }
 
-        public int Priority
+        public int Order
         {
-            get { return 0; }
+            get { return 1; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = FanartBaseProvider.FanArtResourcePool
+            });
         }
     }
 }

+ 31 - 5
MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
@@ -17,14 +18,16 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class ManualFanartSeriesImageProvider : IImageProvider
+    public class ManualFanartSeriesImageProvider : IRemoteImageProvider
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
-        public ManualFanartSeriesImageProvider(IServerConfigurationManager config)
+        public ManualFanartSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -42,6 +45,19 @@ namespace MediaBrowser.Providers.TV
             return item is Series;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Thumb,
+                ImageType.Art,
+                ImageType.Logo,
+                ImageType.Backdrop,
+                ImageType.Banner
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -302,9 +318,19 @@ namespace MediaBrowser.Providers.TV
             }
         }
 
-        public int Priority
+        public int Order
         {
-            get { return 0; }
+            get { return 1; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = FanartBaseProvider.FanArtResourcePool
+            });
         }
     }
 }

+ 25 - 4
MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
@@ -16,14 +17,16 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class ManualTvdbEpisodeImageProvider : IImageProvider
+    public class ManualTvdbEpisodeImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly IHttpClient _httpClient;
 
-        public ManualTvdbEpisodeImageProvider(IServerConfigurationManager config)
+        public ManualTvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -36,6 +39,14 @@ namespace MediaBrowser.Providers.TV
             return item is Episode;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -161,9 +172,19 @@ namespace MediaBrowser.Providers.TV
             };
         }
 
-        public int Priority
+        public int Order
         {
             get { return 0; }
         }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
+            });
+        }
     }
 }

+ 28 - 5
MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
@@ -18,14 +19,16 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class ManualTvdbSeasonImageProvider : IImageProvider
+    public class ManualTvdbSeasonImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly IHttpClient _httpClient;
 
-        public ManualTvdbSeasonImageProvider(IServerConfigurationManager config)
+        public ManualTvdbSeasonImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -43,6 +46,16 @@ namespace MediaBrowser.Providers.TV
             return item is Season;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Banner,
+                ImageType.Backdrop
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -308,9 +321,19 @@ namespace MediaBrowser.Providers.TV
 
         }
 
-        public int Priority
+        public int Order
         {
-            get { return 1; }
+            get { return 0; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
+            });
         }
     }
 }

+ 28 - 5
MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
@@ -18,14 +19,16 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class ManualTvdbSeriesImageProvider : IImageProvider
+    public class ManualTvdbSeriesImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
-        public ManualTvdbSeriesImageProvider(IServerConfigurationManager config)
+        public ManualTvdbSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
+            _httpClient = httpClient;
         }
 
         public string Name
@@ -43,6 +46,16 @@ namespace MediaBrowser.Providers.TV
             return item is Series;
         }
 
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
+        {
+            return new List<ImageType>
+            {
+                ImageType.Primary, 
+                ImageType.Banner,
+                ImageType.Backdrop
+            };
+        }
+
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -304,9 +317,19 @@ namespace MediaBrowser.Providers.TV
 
         }
 
-        public int Priority
+        public int Order
         {
-            get { return 1; }
+            get { return 0; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
+            });
         }
     }
 }

+ 0 - 98
MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs

@@ -1,98 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Providers;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Providers.TV
-{
-    public class TvdbPersonImageProvider : BaseMetadataProvider
-    {
-        private readonly IProviderManager _providerManager;
-
-        public TvdbPersonImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
-            : base(logManager, configurationManager)
-        {
-            _providerManager = providerManager;
-        }
-
-        protected override bool RefreshOnVersionChange
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        protected override string ProviderVersion
-        {
-            get
-            {
-                return "2";
-            }
-        }
-
-        public override bool RequiresInternet
-        {
-            get
-            {
-                return true;
-            }
-        }
-
-        public override bool Supports(BaseItem item)
-        {
-            return item is Person;
-        }
-
-        /// <summary>
-        /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="force">if set to <c>true</c> [force].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{System.Boolean}.</returns>
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            if (string.IsNullOrEmpty(item.PrimaryImagePath))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualTvdbPersonImageProvider.ProviderName).ConfigureAwait(false);
-
-                await DownloadImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
-
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-                return true;
-            }
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-
-        private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
-        {
-            if (!item.HasImage(ImageType.Primary) && !item.LockedFields.Contains(MetadataFields.Images))
-            {
-                var image = images.FirstOrDefault(i => i.Type == ImageType.Primary);
-
-                if (image != null)
-                {
-                    await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken)
-                      .ConfigureAwait(false);
-                }
-            }
-        }
-
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Fourth; }
-        }
-    }
-}

+ 0 - 8
MediaBrowser.Providers/VirtualItemImageValidator.cs

@@ -44,14 +44,6 @@ namespace MediaBrowser.Providers
         public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
         {
             item.ValidateImages();
-            item.ValidateBackdrops();
-
-            var hasScreenshots = item as IHasScreenshots;
-
-            if (hasScreenshots != null)
-            {
-                hasScreenshots.ValidateScreenshots();
-            }
 
             SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
             return TrueTaskResult;

+ 6 - 6
MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs

@@ -388,18 +388,18 @@ namespace MediaBrowser.Server.Implementations.Drawing
         /// <param name="image">The image.</param>
         /// <param name="outputFormat">The output format.</param>
         /// <returns>ImageFormat.</returns>
-        private ImageFormat GetOutputFormat(Image image, ImageOutputFormat outputFormat)
+        private System.Drawing.Imaging.ImageFormat GetOutputFormat(Image image, ImageOutputFormat outputFormat)
         {
             switch (outputFormat)
             {
                 case ImageOutputFormat.Bmp:
-                    return ImageFormat.Bmp;
+                    return System.Drawing.Imaging.ImageFormat.Bmp;
                 case ImageOutputFormat.Gif:
-                    return ImageFormat.Gif;
+                    return System.Drawing.Imaging.ImageFormat.Gif;
                 case ImageOutputFormat.Jpg:
-                    return ImageFormat.Jpeg;
+                    return System.Drawing.Imaging.ImageFormat.Jpeg;
                 case ImageOutputFormat.Png:
-                    return ImageFormat.Png;
+                    return System.Drawing.Imaging.ImageFormat.Png;
                 default:
                     return image.RawFormat;
             }
@@ -787,7 +787,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
                                 //And then save it in the cache
                                 using (var outputStream = _fileSystem.GetFileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
                                 {
-                                    newImage.Save(ImageFormat.Png, outputStream, 100);
+                                    newImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100);
                                 }
                             }
                         }

+ 5 - 0
MediaBrowser.Server.Implementations/Dto/DtoService.cs

@@ -1024,6 +1024,11 @@ namespace MediaBrowser.Server.Implementations.Dto
                 {
                     dto.SpecialFeatureCount = specialFeatureCount;
                 }
+
+                if (fields.Contains(ItemFields.TmdbCollectionName))
+                {
+                    dto.TmdbCollectionName = movie.TmdbCollectionName;
+                }
             }
 
             // Add EpisodeInfo

+ 3 - 3
MediaBrowser.Server.Implementations/Library/ResolverHelper.cs

@@ -46,7 +46,7 @@ namespace MediaBrowser.Server.Implementations.Library
             }
 
             // Make sure the item has a name
-            EnsureName(item);
+            EnsureName(item, args);
 
             item.DontFetchMeta = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
                 item.Parents.Any(i => i.DontFetchMeta);
@@ -59,13 +59,13 @@ namespace MediaBrowser.Server.Implementations.Library
         /// Ensures the name.
         /// </summary>
         /// <param name="item">The item.</param>
-        private static void EnsureName(BaseItem item)
+        private static void EnsureName(BaseItem item, ItemResolveArgs args)
         {
             // If the subclass didn't supply a name, add it here
             if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
             {
                 //we use our resolve args name here to get the name of the containg folder, not actual video file
-                item.Name = GetMBName(item.ResolveArgs.FileInfo.Name, (item.ResolveArgs.FileInfo.Attributes & FileAttributes.Directory) == FileAttributes.Directory);
+                item.Name = GetMBName(args.FileInfo.Name, (args.FileInfo.Attributes & FileAttributes.Directory) == FileAttributes.Directory);
             }
         }
 

+ 6 - 1
MediaBrowser.Server.Implementations/Library/UserManager.cs

@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
 using System;
 using System.Collections.Generic;
@@ -192,7 +193,11 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task.</returns>
         public Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false)
         {
-            var tasks = Users.Select(user => user.RefreshMetadata(cancellationToken, forceRefresh: force)).ToList();
+            var tasks = Users.Select(user => user.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ReplaceAllMetadata = force
+
+            }, cancellationToken)).ToList();
 
             return Task.WhenAll(tasks);
         }

+ 0 - 1
MediaBrowser.Server.Implementations/Library/Validators/GenresPostScanTask.cs

@@ -16,7 +16,6 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
         /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
         /// </summary>
         /// <param name="libraryManager">The library manager.</param>
-        /// <param name="userManager">The user manager.</param>
         public GenresPostScanTask(ILibraryManager libraryManager)
         {
             _libraryManager = libraryManager;

+ 9 - 1
MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
 using System;
 using System.Collections.Generic;
@@ -88,7 +89,14 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
 
                     var itemByName = _libraryManager.GetPerson(name);
 
-                    await itemByName.RefreshMetadata(cancellationToken, allowSlowProviders: false).ConfigureAwait(false);
+                    // The only purpose here is to be able to react to image changes without running the people task. 
+                    // All other metadata can wait for that.
+                    await itemByName.RefreshMetadata(new MetadataRefreshOptions
+                    {
+                        ImageRefreshMode = MetadataRefreshMode.None,
+                        MetadataRefreshMode = MetadataRefreshMode.None
+
+                    }, cancellationToken).ConfigureAwait(false);
 
                     foreach (var libraryId in counts.Keys)
                     {

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs

@@ -116,7 +116,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                         if (response != null)
                         {
                             imageStream = response.Stream;
-                            contentType = response.MimeType;
+                            contentType = "image/" + response.Format.ToString().ToLower();
                         }
                     }
                     catch (NotImplementedException)

+ 19 - 3
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
@@ -328,7 +329,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             // Set this now so we don't cause additional file system access during provider executions
             item.ResetResolveArgs(fileInfo);
 
-            await item.RefreshMetadata(cancellationToken, forceSave: isNew, resetResolveArgs: false);
+            await item.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = isNew,
+                ResetResolveArgs = false
+
+            }, cancellationToken);
 
             return item;
         }
@@ -383,7 +389,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
             item.StartDate = info.StartDate;
 
-            await item.RefreshMetadata(cancellationToken, forceSave: isNew, resetResolveArgs: false);
+            await item.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = isNew,
+                ResetResolveArgs = false
+
+            }, cancellationToken);
 
             return item;
         }
@@ -435,7 +446,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
             item.RecordingInfo = info;
             item.ServiceName = serviceName;
 
-            await item.RefreshMetadata(cancellationToken, forceSave: isNew, resetResolveArgs: false);
+            await item.RefreshMetadata(new MetadataRefreshOptions
+            {
+                ForceSave = isNew,
+                ResetResolveArgs = false
+
+            }, cancellationToken);
 
             _libraryManager.RegisterItem((BaseItem)item);
 

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs

@@ -116,7 +116,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                         if (response != null)
                         {
                             imageStream = response.Stream;
-                            contentType = response.MimeType;
+                            contentType = "image/" + response.Format.ToString().ToLower();
                         }
                     }
                     catch (NotImplementedException)

+ 1 - 1
MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs

@@ -118,7 +118,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                         if (response != null)
                         {
                             imageStream = response.Stream;
-                            contentType = response.MimeType;
+                            contentType = "image/" + response.Format.ToString().ToLower();
                         }
                     }
                     catch (NotImplementedException)

+ 0 - 2
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -189,8 +189,6 @@
     <Compile Include="Persistence\SqliteShrinkMemoryTimer.cs" />
     <Compile Include="Persistence\TypeMapper.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="Providers\ImageSaver.cs" />
-    <Compile Include="Providers\ProviderManager.cs" />
     <Compile Include="Roku\RokuControllerFactory.cs" />
     <Compile Include="ScheduledTasks\PeopleValidationTask.cs" />
     <Compile Include="ScheduledTasks\ChapterImagesTask.cs" />

+ 2 - 2
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -33,6 +33,7 @@ using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Updates;
 using MediaBrowser.Providers;
+using MediaBrowser.Providers.Manager;
 using MediaBrowser.Server.Implementations;
 using MediaBrowser.Server.Implementations.BdInfo;
 using MediaBrowser.Server.Implementations.Configuration;
@@ -47,7 +48,6 @@ using MediaBrowser.Server.Implementations.LiveTv;
 using MediaBrowser.Server.Implementations.Localization;
 using MediaBrowser.Server.Implementations.MediaEncoder;
 using MediaBrowser.Server.Implementations.Persistence;
-using MediaBrowser.Server.Implementations.Providers;
 using MediaBrowser.Server.Implementations.ServerManager;
 using MediaBrowser.Server.Implementations.Session;
 using MediaBrowser.Server.Implementations.WebSocket;
@@ -491,7 +491,7 @@ namespace MediaBrowser.ServerApplication
                                     GetExports<IPeoplePrescanTask>(),
                                     GetExports<IMetadataSaver>());
 
-            ProviderManager.AddParts(GetExports<BaseMetadataProvider>(), GetExports<IImageProvider>());
+            ProviderManager.AddParts(GetExports<BaseMetadataProvider>(), GetExports<IImageProvider>(), GetExports<IMetadataService>(), GetExports<IMetadataProvider>());
 
             ImageProcessor.AddParts(GetExports<IImageEnhancer>());
 

+ 4 - 3
MediaBrowser.ServerApplication/LibraryExplorer.xaml.cs

@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Querying;
@@ -220,14 +221,14 @@ namespace MediaBrowser.ServerApplication
                 if (item is IHasMediaStreams)
                 {
                     var mediaStreams = _itemRepository.GetMediaStreams(new MediaStreamQuery
-                    { 
+                    {
                         ItemId = item.Id
 
                     }).ToList();
 
                     if (mediaStreams.Count > 0)
                     {
-                        json += "\n\nMedia Streams:\n\n"+FormatJson(_jsonSerializer.SerializeToString(mediaStreams));
+                        json += "\n\nMedia Streams:\n\n" + FormatJson(_jsonSerializer.SerializeToString(mediaStreams));
                     }
                 }
 
@@ -356,7 +357,7 @@ namespace MediaBrowser.ServerApplication
                 var item = ((TreeViewItem)tvwLibrary.SelectedItem).Tag as BaseItem;
                 if (item != null)
                 {
-                    item.RefreshMetadata(CancellationToken.None, forceRefresh: cbxForce.IsChecked.Value);
+                    item.RefreshMetadata(new MetadataRefreshOptions { ReplaceAllMetadata = cbxForce.IsChecked.Value }, CancellationToken.None);
                     tvwLibrary_SelectedItemChanged(this, null);
                 }
             }