Browse Source

Merge branch 'master' of https://github.com/MediaBrowser/MediaBrowser

Eric Reed 11 years ago
parent
commit
ab5145bcd7
100 changed files with 3665 additions and 2266 deletions
  1. 7 2
      MediaBrowser.Api/Images/ImageService.cs
  2. 12 10
      MediaBrowser.Api/Images/RemoteImageService.cs
  3. 41 8
      MediaBrowser.Api/ItemRefreshService.cs
  4. 39 24
      MediaBrowser.Api/Library/LibraryStructureService.cs
  5. 4 4
      MediaBrowser.Controller/Drawing/ImageExtensions.cs
  6. 11 0
      MediaBrowser.Controller/Drawing/ImageFormat.cs
  7. 98 24
      MediaBrowser.Controller/Entities/BaseItem.cs
  8. 10 3
      MediaBrowser.Controller/Entities/Folder.cs
  9. 26 1
      MediaBrowser.Controller/Entities/IHasImages.cs
  10. 4 2
      MediaBrowser.Controller/Entities/IHasScreenshots.cs
  11. 12 7
      MediaBrowser.Controller/Entities/Movies/Movie.cs
  12. 11 9
      MediaBrowser.Controller/Entities/User.cs
  13. 10 6
      MediaBrowser.Controller/Entities/Video.cs
  14. 0 29
      MediaBrowser.Controller/IO/IDirectoryWatchers.cs
  15. 36 0
      MediaBrowser.Controller/Library/ILibraryMonitor.cs
  16. 2 3
      MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs
  17. 3 2
      MediaBrowser.Controller/LiveTv/StreamResponseInfo.cs
  18. 11 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  19. 0 17
      MediaBrowser.Controller/Persistence/IItemRepository.cs
  20. 31 0
      MediaBrowser.Controller/Providers/IHasMetadata.cs
  21. 3 25
      MediaBrowser.Controller/Providers/IImageProvider.cs
  22. 66 0
      MediaBrowser.Controller/Providers/ILocalImageProvider.cs
  23. 68 0
      MediaBrowser.Controller/Providers/IMetadataProvider.cs
  24. 38 0
      MediaBrowser.Controller/Providers/IMetadataService.cs
  25. 15 5
      MediaBrowser.Controller/Providers/IProviderManager.cs
  26. 48 0
      MediaBrowser.Controller/Providers/IProviderRepository.cs
  27. 48 0
      MediaBrowser.Controller/Providers/IRemoteImageProvider.cs
  28. 35 0
      MediaBrowser.Controller/Providers/ItemId.cs
  29. 49 0
      MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
  30. 122 0
      MediaBrowser.Controller/Providers/MetadataStatus.cs
  31. 2 2
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  32. 6 0
      MediaBrowser.Model/Dto/BaseItemDto.cs
  33. 11 0
      MediaBrowser.Model/Entities/IHasProviderIds.cs
  34. 3 3
      MediaBrowser.Model/Providers/ImageProviderInfo.cs
  35. 5 0
      MediaBrowser.Model/Querying/ItemFields.cs
  36. 327 0
      MediaBrowser.Providers/All/LocalImageProvider.cs
  37. 34 0
      MediaBrowser.Providers/BaseXmlProvider.cs
  38. 2 2
      MediaBrowser.Providers/CollectionFolderImageProvider.cs
  39. 25 4
      MediaBrowser.Providers/GameGenres/GameGenreImageProvider.cs
  40. 42 0
      MediaBrowser.Providers/GameGenres/GameGenreMetadataService.cs
  41. 26 4
      MediaBrowser.Providers/Genres/GenreImageProvider.cs
  42. 42 0
      MediaBrowser.Providers/Genres/GenreMetadataService.cs
  43. 0 11
      MediaBrowser.Providers/ImageFromMediaLocationProvider.cs
  44. 0 160
      MediaBrowser.Providers/ImagesByName/GameGenreImageProvider.cs
  45. 0 160
      MediaBrowser.Providers/ImagesByName/GenreImageProvider.cs
  46. 0 161
      MediaBrowser.Providers/ImagesByName/MusicGenreImageProvider.cs
  47. 0 160
      MediaBrowser.Providers/ImagesByName/StudioImageProvider.cs
  48. 37 0
      MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs
  49. 0 91
      MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs
  50. 59 0
      MediaBrowser.Providers/LiveTv/ChannelXmlProvider.cs
  51. 41 0
      MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs
  52. 15 12
      MediaBrowser.Providers/Manager/ImageSaver.cs
  53. 435 0
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  54. 358 0
      MediaBrowser.Providers/Manager/MetadataService.cs
  55. 66 33
      MediaBrowser.Providers/Manager/ProviderManager.cs
  56. 24 15
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  57. 31 4
      MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs
  58. 27 5
      MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs
  59. 0 207
      MediaBrowser.Providers/Movies/MovieDbPersonImageProvider.cs
  60. 0 440
      MediaBrowser.Providers/Movies/MovieDbPersonProvider.cs
  61. 2 1
      MediaBrowser.Providers/Movies/MovieDbProvider.cs
  62. 0 89
      MediaBrowser.Providers/Movies/PersonProviderFromXml.cs
  63. 27 5
      MediaBrowser.Providers/Music/ManualFanartAlbumProvider.cs
  64. 30 5
      MediaBrowser.Providers/Music/ManualFanartArtistProvider.cs
  65. 32 5
      MediaBrowser.Providers/Music/ManualLastFmImageProvider.cs
  66. 10 5
      MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs
  67. 25 4
      MediaBrowser.Providers/MusicGenres/MusicGenreImageProvider.cs
  68. 42 0
      MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
  69. 27 5
      MediaBrowser.Providers/People/MovieDbPersonImageProvider.cs
  70. 289 0
      MediaBrowser.Providers/People/MovieDbPersonProvider.cs
  71. 47 0
      MediaBrowser.Providers/People/PersonMetadataService.cs
  72. 59 0
      MediaBrowser.Providers/People/PersonXmlProvider.cs
  73. 28 6
      MediaBrowser.Providers/People/TvdbPersonImageProvider.cs
  74. 126 0
      MediaBrowser.Providers/ProviderUtils.cs
  75. 41 0
      MediaBrowser.Providers/Studios/StudioMetadataService.cs
  76. 25 4
      MediaBrowser.Providers/Studios/StudiosImageProvider.cs
  77. 27 5
      MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs
  78. 31 5
      MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs
  79. 25 4
      MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs
  80. 28 5
      MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs
  81. 28 5
      MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs
  82. 0 98
      MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs
  83. 0 8
      MediaBrowser.Providers/VirtualItemImageValidator.cs
  84. 6 6
      MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs
  85. 5 0
      MediaBrowser.Server.Implementations/Dto/DtoService.cs
  86. 13 7
      MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs
  87. 5 11
      MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs
  88. 4 4
      MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs
  89. 4 4
      MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs
  90. 68 126
      MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
  91. 8 8
      MediaBrowser.Server.Implementations/Library/LibraryManager.cs
  92. 3 3
      MediaBrowser.Server.Implementations/Library/ResolverHelper.cs
  93. 6 1
      MediaBrowser.Server.Implementations/Library/UserManager.cs
  94. 0 1
      MediaBrowser.Server.Implementations/Library/Validators/GenresPostScanTask.cs
  95. 9 1
      MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs
  96. 43 86
      MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs
  97. 19 3
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  98. 43 86
      MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs
  99. 1 1
      MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs
  100. 1 3
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

+ 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")]
         [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
-    
+
     /// <summary>
     /// <summary>
     /// Class UpdateItemImageIndex
     /// Class UpdateItemImageIndex
     /// </summary>
     /// </summary>
@@ -799,7 +799,12 @@ namespace MediaBrowser.Api.Images
 
 
                 await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, null, CancellationToken.None).ConfigureAwait(false);
                 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.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 using ServiceStack;
 using ServiceStack;
+using ServiceStack.Text.Controller;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using ServiceStack.Text.Controller;
 
 
 namespace MediaBrowser.Api.Images
 namespace MediaBrowser.Api.Images
 {
 {
@@ -193,12 +193,7 @@ namespace MediaBrowser.Api.Images
 
 
         private List<ImageProviderInfo> GetImageProviders(BaseItem item)
         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)
         public object Get(GetRemoteImages request)
@@ -229,7 +224,9 @@ namespace MediaBrowser.Api.Images
             var result = new RemoteImageResult
             var result = new RemoteImageResult
             {
             {
                 TotalRecordCount = imagesList.Count,
                 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)
             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 _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>
         /// <summary>

+ 41 - 8
MediaBrowser.Api/ItemRefreshService.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using ServiceStack;
 using ServiceStack;
 using System;
 using System;
 using System.Linq;
 using System.Linq;
@@ -131,7 +132,11 @@ namespace MediaBrowser.Api
 
 
             try
             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)
             catch (Exception ex)
             {
             {
@@ -152,7 +157,11 @@ namespace MediaBrowser.Api
 
 
             try
             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)
             catch (Exception ex)
             {
             {
@@ -173,7 +182,11 @@ namespace MediaBrowser.Api
 
 
             try
             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)
             catch (Exception ex)
             {
             {
@@ -194,7 +207,11 @@ namespace MediaBrowser.Api
 
 
             try
             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)
             catch (Exception ex)
             {
             {
@@ -215,7 +232,11 @@ namespace MediaBrowser.Api
 
 
             try
             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)
             catch (Exception ex)
             {
             {
@@ -236,7 +257,11 @@ namespace MediaBrowser.Api
 
 
             try
             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)
             catch (Exception ex)
             {
             {
@@ -266,7 +291,11 @@ namespace MediaBrowser.Api
 
 
             try
             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)
                 if (item.IsFolder)
                 {
                 {
@@ -301,7 +330,11 @@ namespace MediaBrowser.Api
         {
         {
             foreach (var child in collectionFolder.Children.ToList())
             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)
                 if (child.IsFolder)
                 {
                 {

+ 39 - 24
MediaBrowser.Api/Library/LibraryStructureService.cs

@@ -167,6 +167,17 @@ namespace MediaBrowser.Api.Library
         public bool RefreshLibrary { get; set; }
         public bool RefreshLibrary { get; set; }
     }
     }
 
 
+    [Route("/Library/Changes/Path", "POST")]
+    public class ReportChangedPath : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        [ApiMember(Name = "Path", Description = "The path that was changed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string Path { get; set; }
+    }
+    
     /// <summary>
     /// <summary>
     /// Class LibraryStructureService
     /// Class LibraryStructureService
     /// </summary>
     /// </summary>
@@ -187,7 +198,7 @@ namespace MediaBrowser.Api.Library
         /// </summary>
         /// </summary>
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
 
 
-        private readonly IDirectoryWatchers _directoryWatchers;
+        private readonly ILibraryMonitor _libraryMonitor;
 
 
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
@@ -199,7 +210,7 @@ namespace MediaBrowser.Api.Library
         /// <param name="userManager">The user manager.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <exception cref="System.ArgumentNullException">appPaths</exception>
         /// <exception cref="System.ArgumentNullException">appPaths</exception>
-        public LibraryStructureService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IDirectoryWatchers directoryWatchers, IFileSystem fileSystem, ILogger logger)
+        public LibraryStructureService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IFileSystem fileSystem, ILogger logger)
         {
         {
             if (appPaths == null)
             if (appPaths == null)
             {
             {
@@ -209,11 +220,26 @@ namespace MediaBrowser.Api.Library
             _userManager = userManager;
             _userManager = userManager;
             _appPaths = appPaths;
             _appPaths = appPaths;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _directoryWatchers = directoryWatchers;
+            _libraryMonitor = libraryMonitor;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _logger = logger;
             _logger = logger;
         }
         }
 
 
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <exception cref="System.ArgumentException">Please supply a Path</exception>
+        public void Post(ReportChangedPath request)
+        {
+            if (string.IsNullOrEmpty(request.Path))
+            {
+                throw new ArgumentException("Please supply a Path");
+            }
+
+            _libraryMonitor.ReportFileSystemChanged(request.Path);
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the specified request.
         /// Gets the specified request.
         /// </summary>
         /// </summary>
@@ -270,8 +296,7 @@ namespace MediaBrowser.Api.Library
                 throw new ArgumentException("There is already a media collection with the name " + name + ".");
                 throw new ArgumentException("There is already a media collection with the name " + name + ".");
             }
             }
 
 
-            _directoryWatchers.Stop();
-            _directoryWatchers.TemporarilyIgnore(virtualFolderPath);
+            _libraryMonitor.Stop();
 
 
             try
             try
             {
             {
@@ -294,10 +319,8 @@ namespace MediaBrowser.Api.Library
                 // No need to start if scanning the library because it will handle it
                 // No need to start if scanning the library because it will handle it
                 if (!request.RefreshLibrary)
                 if (!request.RefreshLibrary)
                 {
                 {
-                    _directoryWatchers.Start();
+                    _libraryMonitor.Start();
                 }
                 }
-
-                _directoryWatchers.RemoveTempIgnore(virtualFolderPath);
             }
             }
 
 
             if (request.RefreshLibrary)
             if (request.RefreshLibrary)
@@ -348,9 +371,7 @@ namespace MediaBrowser.Api.Library
                 throw new ArgumentException("There is already a media collection with the name " + newPath + ".");
                 throw new ArgumentException("There is already a media collection with the name " + newPath + ".");
             }
             }
 
 
-            _directoryWatchers.Stop();
-            _directoryWatchers.TemporarilyIgnore(currentPath);
-            _directoryWatchers.TemporarilyIgnore(newPath);
+            _libraryMonitor.Stop();
 
 
             try
             try
             {
             {
@@ -376,11 +397,8 @@ namespace MediaBrowser.Api.Library
                 // No need to start if scanning the library because it will handle it
                 // No need to start if scanning the library because it will handle it
                 if (!request.RefreshLibrary)
                 if (!request.RefreshLibrary)
                 {
                 {
-                    _directoryWatchers.Start();
+                    _libraryMonitor.Start();
                 }
                 }
-
-                _directoryWatchers.RemoveTempIgnore(currentPath);
-                _directoryWatchers.RemoveTempIgnore(newPath);
             }
             }
 
 
             if (request.RefreshLibrary)
             if (request.RefreshLibrary)
@@ -420,8 +438,7 @@ namespace MediaBrowser.Api.Library
                 throw new DirectoryNotFoundException("The media folder does not exist");
                 throw new DirectoryNotFoundException("The media folder does not exist");
             }
             }
 
 
-            _directoryWatchers.Stop();
-            _directoryWatchers.TemporarilyIgnore(path);
+            _libraryMonitor.Stop();
 
 
             try
             try
             {
             {
@@ -437,10 +454,8 @@ namespace MediaBrowser.Api.Library
                 // No need to start if scanning the library because it will handle it
                 // No need to start if scanning the library because it will handle it
                 if (!request.RefreshLibrary)
                 if (!request.RefreshLibrary)
                 {
                 {
-                    _directoryWatchers.Start();
+                    _libraryMonitor.Start();
                 }
                 }
-
-                _directoryWatchers.RemoveTempIgnore(path);
             }
             }
 
 
             if (request.RefreshLibrary)
             if (request.RefreshLibrary)
@@ -460,7 +475,7 @@ namespace MediaBrowser.Api.Library
                 throw new ArgumentNullException("request");
                 throw new ArgumentNullException("request");
             }
             }
 
 
-            _directoryWatchers.Stop();
+            _libraryMonitor.Stop();
 
 
             try
             try
             {
             {
@@ -485,7 +500,7 @@ namespace MediaBrowser.Api.Library
                 // No need to start if scanning the library because it will handle it
                 // No need to start if scanning the library because it will handle it
                 if (!request.RefreshLibrary)
                 if (!request.RefreshLibrary)
                 {
                 {
-                    _directoryWatchers.Start();
+                    _libraryMonitor.Start();
                 }
                 }
             }
             }
 
 
@@ -506,7 +521,7 @@ namespace MediaBrowser.Api.Library
                 throw new ArgumentNullException("request");
                 throw new ArgumentNullException("request");
             }
             }
 
 
-            _directoryWatchers.Stop();
+            _libraryMonitor.Stop();
 
 
             try
             try
             {
             {
@@ -531,7 +546,7 @@ namespace MediaBrowser.Api.Library
                 // No need to start if scanning the library because it will handle it
                 // No need to start if scanning the library because it will handle it
                 if (!request.RefreshLibrary)
                 if (!request.RefreshLibrary)
                 {
                 {
-                    _directoryWatchers.Start();
+                    _libraryMonitor.Start();
                 }
                 }
             }
             }
 
 

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

@@ -18,17 +18,17 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="image">The image.</param>
         /// <param name="image">The image.</param>
         /// <param name="toStream">To stream.</param>
         /// <param name="toStream">To stream.</param>
         /// <param name="quality">The quality.</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
             // 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
             // 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);
                 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
             else
             {
             {

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

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

+ 98 - 24
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Entities
     /// <summary>
     /// <summary>
     /// Class BaseItem
     /// Class BaseItem
     /// </summary>
     /// </summary>
-    public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData
+    public abstract class BaseItem : IHasProviderIds, ILibraryItem, IHasImages, IHasUserData, IHasMetadata
     {
     {
         protected BaseItem()
         protected BaseItem()
         {
         {
@@ -364,11 +364,16 @@ namespace MediaBrowser.Controller.Entities
             }
             }
         }
         }
 
 
+        private string _forcedSortName;
         /// <summary>
         /// <summary>
         /// Gets or sets the name of the forced sort.
         /// Gets or sets the name of the forced sort.
         /// </summary>
         /// </summary>
         /// <value>The name of the forced sort.</value>
         /// <value>The name of the forced sort.</value>
-        public string ForcedSortName { get; set; }
+        public string ForcedSortName
+        {
+            get { return _forcedSortName; }
+            set { _forcedSortName = value; _sortName = null; }
+        }
 
 
         private string _sortName;
         private string _sortName;
         /// <summary>
         /// <summary>
@@ -767,25 +772,35 @@ namespace MediaBrowser.Controller.Entities
             }).ToList();
             }).ToList();
         }
         }
 
 
+        public Task<bool> RefreshMetadata(CancellationToken cancellationToken, bool resetResolveArgs = true)
+        {
+            return RefreshMetadata(new MetadataRefreshOptions { ResetResolveArgs = resetResolveArgs }, cancellationToken);
+        }
+
         /// <summary>
         /// <summary>
         /// Overrides the base implementation to refresh metadata for local trailers
         /// Overrides the base implementation to refresh metadata for local trailers
         /// </summary>
         /// </summary>
+        /// <param name="options">The options.</param>
         /// <param name="cancellationToken">The cancellation token.</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>
         /// <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
                 // Reload this
                 ResetResolveArgs();
                 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
             // Refresh for the item
-            var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh, allowSlowProviders);
+            var itemRefreshTask = ProviderManager.ExecuteMetadataProviders(this, cancellationToken, forceRefresh);
 
 
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
@@ -800,15 +815,15 @@ namespace MediaBrowser.Controller.Entities
                 var hasThemeMedia = this as IHasThemeMedia;
                 var hasThemeMedia = this as IHasThemeMedia;
                 if (hasThemeMedia != null)
                 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;
                 var hasTrailers = this as IHasTrailers;
                 if (hasTrailers != null)
                 if (hasTrailers != null)
                 {
                 {
-                    localTrailersChanged = await RefreshLocalTrailers(hasTrailers, cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                    localTrailersChanged = await RefreshLocalTrailers(hasTrailers, cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
@@ -829,14 +844,20 @@ namespace MediaBrowser.Controller.Entities
             return changed;
             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 newItems = LoadLocalTrailers().ToList();
             var newItemIds = newItems.Select(i => i.Id).ToList();
             var newItemIds = newItems.Select(i => i.Id).ToList();
 
 
             var itemsChanged = !item.LocalTrailerIds.SequenceEqual(newItemIds);
             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);
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
 
@@ -845,14 +866,20 @@ namespace MediaBrowser.Controller.Entities
             return itemsChanged || results.Contains(true);
             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 newThemeVideos = LoadThemeVideos().ToList();
             var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToList();
             var newThemeVideoIds = newThemeVideos.Select(i => i.Id).ToList();
 
 
             var themeVideosChanged = !item.ThemeVideoIds.SequenceEqual(newThemeVideoIds);
             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);
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
 
@@ -864,14 +891,20 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// <summary>
         /// Refreshes the theme songs.
         /// Refreshes the theme songs.
         /// </summary>
         /// </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 newThemeSongs = LoadThemeSongs().ToList();
             var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToList();
             var newThemeSongIds = newThemeSongs.Select(i => i.Id).ToList();
 
 
             var themeSongsChanged = !item.ThemeSongIds.SequenceEqual(newThemeSongIds);
             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);
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
 
@@ -1456,7 +1489,13 @@ namespace MediaBrowser.Controller.Entities
 
 
             // Refresh metadata
             // Refresh metadata
             // Need to disable slow providers or the image might get re-downloaded
             // 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>
         /// <summary>
@@ -1482,8 +1521,10 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// <summary>
         /// Validates that images within the item are still on the file system
         /// Validates that images within the item are still on the file system
         /// </summary>
         /// </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
             // 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
             var deletedKeys = Images
                 .Where(image => !File.Exists(image.Value))
                 .Where(image => !File.Exists(image.Value))
@@ -1494,14 +1535,28 @@ namespace MediaBrowser.Controller.Entities
             foreach (var key in deletedKeys)
             foreach (var key in deletedKeys)
             {
             {
                 Images.Remove(key);
                 Images.Remove(key);
+                changed = true;
             }
             }
+
+            if (ValidateBackdrops())
+            {
+                changed = true;
+            }
+            if (ValidateScreenshots())
+            {
+                changed = true;
+            }
+
+            return changed;
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Validates that backdrops within the item are still on the file system
         /// Validates that backdrops within the item are still on the file system
         /// </summary>
         /// </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
             // 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
             var deletedImages = BackdropImagePaths
                 .Where(path => !File.Exists(path))
                 .Where(path => !File.Exists(path))
@@ -1513,7 +1568,11 @@ namespace MediaBrowser.Controller.Entities
                 BackdropImagePaths.Remove(path);
                 BackdropImagePaths.Remove(path);
 
 
                 RemoveImageSourceForPath(path);
                 RemoveImageSourceForPath(path);
+
+                changed = true;
             }
             }
+
+            return changed;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1593,9 +1652,16 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// <summary>
         /// Validates the screenshots.
         /// Validates the screenshots.
         /// </summary>
         /// </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
             // 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
             var deletedImages = hasScreenshots.ScreenshotImagePaths
@@ -1606,7 +1672,10 @@ namespace MediaBrowser.Controller.Entities
             foreach (var path in deletedImages)
             foreach (var path in deletedImages)
             {
             {
                 hasScreenshots.ScreenshotImagePaths.Remove(path);
                 hasScreenshots.ScreenshotImagePaths.Remove(path);
+                changed = true;
             }
             }
+
+            return changed;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1699,7 +1768,12 @@ namespace MediaBrowser.Controller.Entities
             FileSystem.SwapFiles(file1, file2);
             FileSystem.SwapFiles(file1, file2);
 
 
             // Directory watchers should repeat this, but do a quick refresh first
             // 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)
         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.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Localization;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MoreLinq;
 using MoreLinq;
@@ -535,7 +536,13 @@ namespace MediaBrowser.Controller.Entities
             try
             try
             {
             {
                 //refresh it
                 //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)
             catch (IOException ex)
             {
             {
@@ -907,9 +914,9 @@ namespace MediaBrowser.Controller.Entities
             return item;
             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;
             return (SupportsShortcutChildren && LocationType == LocationType.FileSystem && RefreshLinkedChildren()) || changed;
         }
         }

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

@@ -1,5 +1,6 @@
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using System;
 using System;
+using System.Collections.Generic;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Controller.Entities
 namespace MediaBrowser.Controller.Entities
@@ -10,7 +11,7 @@ namespace MediaBrowser.Controller.Entities
         /// Gets the name.
         /// Gets the name.
         /// </summary>
         /// </summary>
         /// <value>The name.</value>
         /// <value>The name.</value>
-        string Name { get; }
+        string Name { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the path.
         /// Gets the path.
@@ -24,6 +25,12 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The identifier.</value>
         /// <value>The identifier.</value>
         Guid Id { get; }
         Guid Id { get; }
 
 
+        /// <summary>
+        /// Gets the type of the location.
+        /// </summary>
+        /// <value>The type of the location.</value>
+        LocationType LocationType { get; }
+
         /// <summary>
         /// <summary>
         /// Gets the image path.
         /// Gets the image path.
         /// </summary>
         /// </summary>
@@ -81,6 +88,24 @@ namespace MediaBrowser.Controller.Entities
         /// </summary>
         /// </summary>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         string GetPreferredMetadataLanguage();
         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
     public static class HasImagesExtensions

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

@@ -14,8 +14,10 @@ namespace MediaBrowser.Controller.Entities
         List<string> ScreenshotImagePaths { get; set; }
         List<string> ScreenshotImagePaths { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Validates the screenshots.
+        /// Determines whether [contains image with source URL] [the specified URL].
         /// </summary>
         /// </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 MediaBrowser.Model.Entities;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -108,13 +109,11 @@ namespace MediaBrowser.Controller.Entities.Movies
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="forceSave">if set to <c>true</c> [is new item].</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="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>
         /// <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
             // 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;
             var specialFeaturesChanged = false;
 
 
@@ -122,7 +121,7 @@ namespace MediaBrowser.Controller.Entities.Movies
             // In other words, it must be part of the Parent/Child tree
             // In other words, it must be part of the Parent/Child tree
             if (LocationType == LocationType.FileSystem && Parent != null && !IsInMixedFolder)
             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;
             return specialFeaturesChanged || result;
@@ -135,7 +134,13 @@ namespace MediaBrowser.Controller.Entities.Movies
 
 
             var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds);
             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);
             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.Common.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using System;
 using System;
@@ -212,7 +213,12 @@ namespace MediaBrowser.Controller.Entities
             // Kick off a task to validate the media library
             // Kick off a task to validate the media library
             Task.Run(() => ValidateMediaLibrary(new Progress<double>(), CancellationToken.None));
             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>
         /// <summary>
@@ -275,17 +281,13 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="forceSave">if set to <c>true</c> [is new item].</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="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>
         /// <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;
             var changed = updateReason.HasValue;
 
 

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

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using System;
 using System;
@@ -164,13 +165,11 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="forceSave">if set to <c>true</c> [is new item].</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="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>
         /// <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
             // 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;
             var additionalPartsChanged = false;
 
 
@@ -181,7 +180,7 @@ namespace MediaBrowser.Controller.Entities
             {
             {
                 try
                 try
                 {
                 {
-                    additionalPartsChanged = await RefreshAdditionalParts(cancellationToken, forceSave, forceRefresh, allowSlowProviders).ConfigureAwait(false);
+                    additionalPartsChanged = await RefreshAdditionalParts(cancellationToken, forceSave, forceRefresh).ConfigureAwait(false);
                 }
                 }
                 catch (IOException ex)
                 catch (IOException ex)
                 {
                 {
@@ -208,7 +207,12 @@ namespace MediaBrowser.Controller.Entities
 
 
             var itemsChanged = !AdditionalPartIds.SequenceEqual(newItemIds);
             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);
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
 

+ 0 - 29
MediaBrowser.Controller/IO/IDirectoryWatchers.cs

@@ -1,29 +0,0 @@
-using System;
-
-namespace MediaBrowser.Controller.IO
-{
-    public interface IDirectoryWatchers : IDisposable
-    {
-        /// <summary>
-        /// Add the path to our temporary ignore list.  Use when writing to a path within our listening scope.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        void TemporarilyIgnore(string path);
-
-        /// <summary>
-        /// Removes the temp ignore.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        void RemoveTempIgnore(string path);
-
-        /// <summary>
-        /// Starts this instance.
-        /// </summary>
-        void Start();
-
-        /// <summary>
-        /// Stops this instance.
-        /// </summary>
-        void Stop();
-    }
-}

+ 36 - 0
MediaBrowser.Controller/Library/ILibraryMonitor.cs

@@ -0,0 +1,36 @@
+using System;
+
+namespace MediaBrowser.Controller.Library
+{
+    public interface ILibraryMonitor : IDisposable
+    {
+        /// <summary>
+        /// Starts this instance.
+        /// </summary>
+        void Start();
+
+        /// <summary>
+        /// Stops this instance.
+        /// </summary>
+        void Stop();
+
+        /// <summary>
+        /// Reports the file system change beginning.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        void ReportFileSystemChangeBeginning(string path);
+
+        /// <summary>
+        /// Reports the file system change complete.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="refreshPath">if set to <c>true</c> [refresh path].</param>
+        void ReportFileSystemChangeComplete(string path, bool refreshPath);
+
+        /// <summary>
+        /// Reports the file system changed.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        void ReportFileSystemChanged(string path);
+    }
+}

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

@@ -1,4 +1,5 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -11,8 +12,6 @@ namespace MediaBrowser.Controller.LiveTv
 
 
         string MediaType { get; }
         string MediaType { get; }
 
 
-        LocationType LocationType { get; }
-
         RecordingInfo RecordingInfo { get; set; }
         RecordingInfo RecordingInfo { get; set; }
 
 
         string GetClientTypeName();
         string GetClientTypeName();
@@ -21,6 +20,6 @@ namespace MediaBrowser.Controller.LiveTv
 
 
         bool IsParentalAllowed(User user);
         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
 namespace MediaBrowser.Controller.LiveTv
 {
 {
@@ -14,6 +15,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// Gets or sets the type of the MIME.
         /// Gets or sets the type of the MIME.
         /// </summary>
         /// </summary>
         /// <value>The type of the MIME.</value>
         /// <value>The type of the MIME.</value>
-        public string MimeType { get; set; }
+        public ImageFormat Format { get; set; }
     }
     }
 }
 }

+ 11 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -69,6 +69,7 @@
       <Link>Properties\SharedVersion.cs</Link>
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
     </Compile>
     <Compile Include="Drawing\IImageProcessor.cs" />
     <Compile Include="Drawing\IImageProcessor.cs" />
+    <Compile Include="Drawing\ImageFormat.cs" />
     <Compile Include="Drawing\ImageProcessingOptions.cs" />
     <Compile Include="Drawing\ImageProcessingOptions.cs" />
     <Compile Include="Dto\IDtoService.cs" />
     <Compile Include="Dto\IDtoService.cs" />
     <Compile Include="Entities\AdultVideo.cs" />
     <Compile Include="Entities\AdultVideo.cs" />
@@ -143,8 +144,17 @@
     <Compile Include="Persistence\IFileOrganizationRepository.cs" />
     <Compile Include="Persistence\IFileOrganizationRepository.cs" />
     <Compile Include="Persistence\MediaStreamQuery.cs" />
     <Compile Include="Persistence\MediaStreamQuery.cs" />
     <Compile Include="Providers\IDynamicInfoProvider.cs" />
     <Compile Include="Providers\IDynamicInfoProvider.cs" />
+    <Compile Include="Providers\IHasMetadata.cs" />
     <Compile Include="Providers\IImageProvider.cs" />
     <Compile Include="Providers\IImageProvider.cs" />
+    <Compile Include="Providers\IProviderRepository.cs" />
+    <Compile Include="Providers\IRemoteImageProvider.cs" />
+    <Compile Include="Providers\ILocalImageProvider.cs" />
+    <Compile Include="Providers\IMetadataProvider.cs" />
+    <Compile Include="Providers\IMetadataService.cs" />
+    <Compile Include="Providers\ItemId.cs" />
+    <Compile Include="Providers\MetadataRefreshOptions.cs" />
     <Compile Include="Providers\NameParser.cs" />
     <Compile Include="Providers\NameParser.cs" />
+    <Compile Include="Providers\MetadataStatus.cs" />
     <Compile Include="Session\ISessionManager.cs" />
     <Compile Include="Session\ISessionManager.cs" />
     <Compile Include="Drawing\ImageExtensions.cs" />
     <Compile Include="Drawing\ImageExtensions.cs" />
     <Compile Include="Entities\AggregateFolder.cs" />
     <Compile Include="Entities\AggregateFolder.cs" />
@@ -174,7 +184,7 @@
     <Compile Include="Entities\Video.cs" />
     <Compile Include="Entities\Video.cs" />
     <Compile Include="Entities\CollectionFolder.cs" />
     <Compile Include="Entities\CollectionFolder.cs" />
     <Compile Include="Entities\Year.cs" />
     <Compile Include="Entities\Year.cs" />
-    <Compile Include="IO\IDirectoryWatchers.cs" />
+    <Compile Include="Library\ILibraryMonitor.cs" />
     <Compile Include="IServerApplicationHost.cs" />
     <Compile Include="IServerApplicationHost.cs" />
     <Compile Include="IServerApplicationPaths.cs" />
     <Compile Include="IServerApplicationPaths.cs" />
     <Compile Include="Library\SearchHintInfo.cs" />
     <Compile Include="Library\SearchHintInfo.cs" />

+ 0 - 17
MediaBrowser.Controller/Persistence/IItemRepository.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -112,22 +111,6 @@ namespace MediaBrowser.Controller.Persistence
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         Task SaveMediaStreams(Guid id, IEnumerable<MediaStream> streams, CancellationToken cancellationToken);
         Task SaveMediaStreams(Guid id, IEnumerable<MediaStream> streams, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Gets the provider history.
-        /// </summary>
-        /// <param name="itemId">The item identifier.</param>
-        /// <returns>IEnumerable{BaseProviderInfo}.</returns>
-        IEnumerable<BaseProviderInfo> GetProviderHistory(Guid itemId);
-
-        /// <summary>
-        /// Saves the provider history.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <param name="history">The history.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SaveProviderHistory(Guid id, IEnumerable<BaseProviderInfo> history, CancellationToken cancellationToken);
     }
     }
 }
 }
 
 

+ 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.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
 namespace MediaBrowser.Controller.Providers
 {
 {
@@ -26,26 +21,9 @@ namespace MediaBrowser.Controller.Providers
         bool Supports(IHasImages item);
         bool Supports(IHasImages item);
 
 
         /// <summary>
         /// <summary>
-        /// Gets the images.
+        /// Gets the order.
         /// </summary>
         /// </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; }
     }
     }
 }
 }

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

@@ -0,0 +1,66 @@
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+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 supported images.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>IEnumerable{ImageType}.</returns>
+        IEnumerable<ImageType> GetSupportedImages(IHasImages item);
+
+        /// <summary>
+        /// Gets the image.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{DynamicImageResponse}.</returns>
+        Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken);
+    }
+
+    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; }
+        public bool HasImage { get; set; }
+
+        public void SetFormatFromMimeType(string mimeType)
+        {
+            
+        }
+    }
+}

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

@@ -0,0 +1,68 @@
+using System;
+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 item has changed.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="date">The date.</param>
+        /// <returns><c>true</c> if the specified item has changed; otherwise, <c>false</c>.</returns>
+        bool HasChanged(IHasMetadata item, DateTime date);
+    }
+
+    public class MetadataResult<T>
+        where T : IHasMetadata
+    {
+        public bool HasMetadata { get; set; }
+        public T Item { get; set; }
+    }
+
+}

+ 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>
     /// </summary>
     public interface IProviderManager
     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>
         /// <summary>
         /// Executes the metadata providers.
         /// Executes the metadata providers.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="force">if set to <c>true</c> [force].</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>
         /// <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>
         /// <summary>
         /// Saves the image.
         /// Saves the image.
@@ -54,7 +62,9 @@ namespace MediaBrowser.Controller.Providers
         /// </summary>
         /// </summary>
         /// <param name="providers">The providers.</param>
         /// <param name="providers">The providers.</param>
         /// <param name="imageProviders">The image 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>
         /// <summary>
         /// Gets the available remote images.
         /// Gets the available remote images.
@@ -70,7 +80,7 @@ namespace MediaBrowser.Controller.Providers
         /// Gets the image providers.
         /// Gets the image providers.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <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/IProviderRepository.cs

@@ -0,0 +1,48 @@
+using MediaBrowser.Controller.Persistence;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+    public interface IProviderRepository : IRepository
+    {
+        /// <summary>
+        /// Gets the provider history.
+        /// </summary>
+        /// <param name="itemId">The item identifier.</param>
+        /// <returns>IEnumerable{BaseProviderInfo}.</returns>
+        IEnumerable<BaseProviderInfo> GetProviderHistory(Guid itemId);
+
+        /// <summary>
+        /// Saves the provider history.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="history">The history.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SaveProviderHistory(Guid id, IEnumerable<BaseProviderInfo> history, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the metadata status.
+        /// </summary>
+        /// <param name="itemId">The item identifier.</param>
+        /// <returns>MetadataStatus.</returns>
+        MetadataStatus GetMetadataStatus(Guid itemId);
+
+        /// <summary>
+        /// Saves the metadata status.
+        /// </summary>
+        /// <param name="status">The status.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SaveMetadataStatus(MetadataStatus status, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Initializes this instance.
+        /// </summary>
+        /// <returns>Task.</returns>
+        Task Initialize();
+    }
+}

+ 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);
+    }
+}

+ 35 - 0
MediaBrowser.Controller/Providers/ItemId.cs

@@ -0,0 +1,35 @@
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Controller.Providers
+{
+    public class ItemId : IHasProviderIds
+    {
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name { get; set; }
+        /// <summary>
+        /// Gets or sets the metadata language.
+        /// </summary>
+        /// <value>The metadata language.</value>
+        public string MetadataLanguage { get; set; }
+        /// <summary>
+        /// Gets or sets the metadata country code.
+        /// </summary>
+        /// <value>The metadata country code.</value>
+        public string MetadataCountryCode { get; set; }
+        /// <summary>
+        /// Gets or sets the provider ids.
+        /// </summary>
+        /// <value>The provider ids.</value>
+        public Dictionary<string, string> ProviderIds { get; set; }
+
+        public ItemId()
+        {
+            ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+        }
+    }
+}

+ 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
+    }
+}

+ 122 - 0
MediaBrowser.Controller/Providers/MetadataStatus.cs

@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Common.Extensions;
+
+namespace MediaBrowser.Controller.Providers
+{
+    public class MetadataStatus
+    {
+        /// <summary>
+        /// Gets or sets the item identifier.
+        /// </summary>
+        /// <value>The item identifier.</value>
+        public Guid ItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date last metadata refresh.
+        /// </summary>
+        /// <value>The date last metadata refresh.</value>
+        public DateTime? DateLastMetadataRefresh { get; set; }
+
+        /// <summary>
+        /// Gets or sets the date last images refresh.
+        /// </summary>
+        /// <value>The date last images refresh.</value>
+        public DateTime? DateLastImagesRefresh { get; set; }
+
+        /// <summary>
+        /// Gets or sets the last result.
+        /// </summary>
+        /// <value>The last result.</value>
+        public ProviderRefreshStatus LastStatus { get; set; }
+
+        /// <summary>
+        /// Gets or sets the last result error message.
+        /// </summary>
+        /// <value>The last result error message.</value>
+        public string LastErrorMessage { get; set; }
+
+        /// <summary>
+        /// Gets or sets the providers refreshed.
+        /// </summary>
+        /// <value>The providers refreshed.</value>
+        public List<Guid> MetadataProvidersRefreshed { get; set; }
+        public List<Guid> ImageProvidersRefreshed { get; set; }
+
+        public void AddStatus(ProviderRefreshStatus status, string errorMessage)
+        {
+            if (LastStatus != status)
+            {
+                IsDirty = true;
+            }
+
+            if (string.IsNullOrEmpty(LastErrorMessage))
+            {
+                LastErrorMessage = errorMessage;
+            }
+            if (LastStatus == ProviderRefreshStatus.Success)
+            {
+                LastStatus = status;
+            }
+        }
+
+        public MetadataStatus()
+        {
+            LastStatus = ProviderRefreshStatus.Success;
+
+            MetadataProvidersRefreshed = new List<Guid>();
+            ImageProvidersRefreshed = new List<Guid>();
+        }
+
+        public bool IsDirty { get; private set; }
+
+        public void SetDateLastMetadataRefresh(DateTime date)
+        {
+            if (date != (DateLastMetadataRefresh ?? DateTime.MinValue))
+            {
+                IsDirty = true;
+            }
+
+            DateLastMetadataRefresh = date;
+        }
+
+        public void SetDateLastImagesRefresh(DateTime date)
+        {
+            if (date != (DateLastImagesRefresh ?? DateTime.MinValue))
+            {
+                IsDirty = true;
+            }
+
+            DateLastImagesRefresh = date;
+        }
+
+        public void AddImageProvidersRefreshed(List<Guid> providerIds)
+        {
+            var count = ImageProvidersRefreshed.Count;
+
+            providerIds.AddRange(ImageProvidersRefreshed);
+
+            ImageProvidersRefreshed = providerIds.Distinct().ToList();
+
+            if (ImageProvidersRefreshed.Count != count)
+            {
+                IsDirty = true;
+            }
+        }
+
+        public void AddMetadataProvidersRefreshed(List<Guid> providerIds)
+        {
+            var count = MetadataProvidersRefreshed.Count;
+
+            providerIds.AddRange(MetadataProvidersRefreshed);
+
+            MetadataProvidersRefreshed = providerIds.Distinct().ToList();
+
+            if (MetadataProvidersRefreshed.Count != count)
+            {
+                IsDirty = true;
+            }
+        }
+    }
+}

+ 2 - 2
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -165,7 +165,7 @@ namespace MediaBrowser.Model.Configuration
         /// different directories and files.
         /// different directories and files.
         /// </summary>
         /// </summary>
         /// <value>The file watcher delay.</value>
         /// <value>The file watcher delay.</value>
-        public int FileWatcherDelay { get; set; }
+        public int RealtimeWatcherDelay { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether [enable dashboard response caching].
         /// Gets or sets a value indicating whether [enable dashboard response caching].
@@ -250,7 +250,7 @@ namespace MediaBrowser.Model.Configuration
             MaxResumePct = 90;
             MaxResumePct = 90;
             MinResumeDurationSeconds = Convert.ToInt32(TimeSpan.FromMinutes(5).TotalSeconds);
             MinResumeDurationSeconds = Convert.ToInt32(TimeSpan.FromMinutes(5).TotalSeconds);
 
 
-            FileWatcherDelay = 8;
+            RealtimeWatcherDelay = 20;
 
 
             RecentItemDays = 10;
             RecentItemDays = 10;
 
 

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

@@ -116,6 +116,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The overview.</value>
         /// <value>The overview.</value>
         public string Overview { get; set; }
         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>
         /// <summary>
         /// Gets or sets the taglines.
         /// Gets or sets the taglines.
         /// </summary>
         /// </summary>

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

@@ -20,6 +20,17 @@ namespace MediaBrowser.Model.Entities
     /// </summary>
     /// </summary>
     public static class ProviderIdsExtensions
     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>
         /// <summary>
         /// Gets a provider id
         /// Gets a provider id
         /// </summary>
         /// </summary>

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

@@ -12,9 +12,9 @@
         public string Name { get; set; }
         public string Name { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets the priority.
+        /// Gets or sets the order.
         /// </summary>
         /// </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
         /// The tags
         /// </summary>
         /// </summary>
         Tags,
         Tags,
+
+        /// <summary>
+        /// The TMDB collection name
+        /// </summary>
+        TmdbCollectionName,
         
         
         /// <summary>
         /// <summary>
         /// The trailer url of the item
         /// 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);
+        }
+    }
+}

+ 34 - 0
MediaBrowser.Providers/BaseXmlProvider.cs

@@ -0,0 +1,34 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Providers;
+using System;
+using System.IO;
+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;
+        }
+
+        public bool HasLocalMetadata(IHasMetadata item)
+        {
+            return File.Exists(GetXmlPath(item.Path));
+        }
+    }
+}

+ 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.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -7,6 +6,7 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using System;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 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.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Genres;
+using MediaBrowser.Providers.ImagesByName;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 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 IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
@@ -21,7 +23,7 @@ namespace MediaBrowser.Providers.ImagesByName
 
 
         private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1);
         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;
             _config = config;
             _httpClient = httpClient;
             _httpClient = httpClient;
@@ -43,6 +45,15 @@ namespace MediaBrowser.Providers.ImagesByName
             return item is GameGenre;
             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)
         public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, 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);
             return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken);
         }
         }
 
 
-        public int Priority
+        public int Order
         {
         {
             get { return 0; }
             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, IProviderRepository providerRepo, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo)
+        {
+            _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.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.ImagesByName;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 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 IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
@@ -21,7 +22,9 @@ namespace MediaBrowser.Providers.ImagesByName
 
 
         private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1);
         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;
             _config = config;
             _httpClient = httpClient;
             _httpClient = httpClient;
@@ -43,6 +46,15 @@ namespace MediaBrowser.Providers.ImagesByName
             return item is Genre;
             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)
         public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, 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);
             return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken);
         }
         }
 
 
-        public int Priority
+        public int Order
         {
         {
             get { return 0; }
             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, IProviderRepository providerRepo, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo)
+        {
+            _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();
             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);
             var args = GetResolveArgsContainingImages(item);
 
 
             PopulateBaseItemImages(item, args);
             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; }
-        }
-    }
-}

+ 37 - 0
MediaBrowser.Providers/LiveTv/ChannelMetadataService.cs

@@ -0,0 +1,37 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+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.LiveTv
+{
+    public class ChannelMetadataService : MetadataService<LiveTvChannel>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public ChannelMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        protected override void MergeData(LiveTvChannel source, LiveTvChannel target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        }
+
+        protected override Task SaveItem(LiveTvChannel item, ItemUpdateType reason, CancellationToken cancellationToken)
+        {
+            return _libraryManager.UpdateItem(item, reason, cancellationToken);
+        }
+    }
+}

+ 0 - 91
MediaBrowser.Providers/LiveTv/ChannelProviderFromXml.cs

@@ -1,91 +0,0 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Logging;
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Providers.LiveTv
-{
-    class ChannelProviderFromXml : BaseMetadataProvider
-    {
-        private readonly IFileSystem _fileSystem;
-
-        public ChannelProviderFromXml(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 LiveTvChannel;
-        }
-
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Second; }
-        }
-
-        private const string XmlFileName = "channel.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="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<LiveTvChannel>(Logger).Fetch((LiveTvChannel)item, path, cancellationToken);
-                }
-                finally
-                {
-                    XmlParsingResourcePool.Release();
-                }
-
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-                return true;
-            }
-
-            return false;
-        }
-    }
-}

+ 59 - 0
MediaBrowser.Providers/LiveTv/ChannelXmlProvider.cs

@@ -0,0 +1,59 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.LiveTv
+{
+    public class ChannelXmlProvider : BaseXmlProvider, ILocalMetadataProvider<LiveTvChannel>
+    {
+        private readonly ILogger _logger;
+
+        public ChannelXmlProvider(IFileSystem fileSystem, ILogger logger)
+            : base(fileSystem)
+        {
+            _logger = logger;
+        }
+
+        public async Task<MetadataResult<LiveTvChannel>> GetMetadata(string path, CancellationToken cancellationToken)
+        {
+            path = GetXmlPath(path);
+
+            var result = new MetadataResult<LiveTvChannel>();
+
+            await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                var item = new LiveTvChannel();
+
+                new BaseItemXmlParser<LiveTvChannel>(_logger).Fetch(item, path, cancellationToken);
+                result.HasMetadata = true;
+                result.Item = item;
+            }
+            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, "channel.xml");
+        }
+    }
+}

+ 41 - 0
MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs

@@ -0,0 +1,41 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+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.LiveTv
+{
+   public class ProgramMetadataService : MetadataService<LiveTvProgram>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public ProgramMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo)
+        {
+            _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(LiveTvProgram source, LiveTvProgram target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        }
+
+        protected override Task SaveItem(LiveTvProgram item, ItemUpdateType reason, CancellationToken cancellationToken)
+        {
+            return _libraryManager.UpdateItem(item, reason, cancellationToken);
+        }
+    }
+}

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

@@ -3,7 +3,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
@@ -15,7 +15,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
-namespace MediaBrowser.Server.Implementations.Providers
+namespace MediaBrowser.Providers.Manager
 {
 {
     /// <summary>
     /// <summary>
     /// Class ImageSaver
     /// Class ImageSaver
@@ -36,7 +36,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <summary>
         /// <summary>
         /// The _directory watchers
         /// The _directory watchers
         /// </summary>
         /// </summary>
-        private readonly IDirectoryWatchers _directoryWatchers;
+        private readonly ILibraryMonitor _libraryMonitor;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
 
 
@@ -44,11 +44,11 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// Initializes a new instance of the <see cref="ImageSaver"/> class.
         /// Initializes a new instance of the <see cref="ImageSaver"/> class.
         /// </summary>
         /// </summary>
         /// <param name="config">The config.</param>
         /// <param name="config">The config.</param>
-        /// <param name="directoryWatchers">The directory watchers.</param>
-        public ImageSaver(IServerConfigurationManager config, IDirectoryWatchers directoryWatchers, IFileSystem fileSystem, ILogger logger)
+        /// <param name="libraryMonitor">The directory watchers.</param>
+        public ImageSaver(IServerConfigurationManager config, ILibraryMonitor libraryMonitor, IFileSystem fileSystem, ILogger logger)
         {
         {
             _config = config;
             _config = config;
-            _directoryWatchers = directoryWatchers;
+            _libraryMonitor = libraryMonitor;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _logger = logger;
             _logger = logger;
             _remoteImageCache = new FileSystemRepository(config.ApplicationPaths.DownloadedImagesDataPath);
             _remoteImageCache = new FileSystemRepository(config.ApplicationPaths.DownloadedImagesDataPath);
@@ -160,7 +160,7 @@ namespace MediaBrowser.Server.Implementations.Providers
             // Delete the current path
             // Delete the current path
             if (!string.IsNullOrEmpty(currentPath) && !paths.Contains(currentPath, StringComparer.OrdinalIgnoreCase))
             if (!string.IsNullOrEmpty(currentPath) && !paths.Contains(currentPath, StringComparer.OrdinalIgnoreCase))
             {
             {
-                _directoryWatchers.TemporarilyIgnore(currentPath);
+                _libraryMonitor.ReportFileSystemChangeBeginning(currentPath);
 
 
                 try
                 try
                 {
                 {
@@ -179,7 +179,7 @@ namespace MediaBrowser.Server.Implementations.Providers
                 }
                 }
                 finally
                 finally
                 {
                 {
-                    _directoryWatchers.RemoveTempIgnore(currentPath);
+                    _libraryMonitor.ReportFileSystemChangeComplete(currentPath, false);
                 }
                 }
             }
             }
         }
         }
@@ -197,8 +197,8 @@ namespace MediaBrowser.Server.Implementations.Providers
 
 
             var parentFolder = Path.GetDirectoryName(path);
             var parentFolder = Path.GetDirectoryName(path);
 
 
-            _directoryWatchers.TemporarilyIgnore(path);
-            _directoryWatchers.TemporarilyIgnore(parentFolder);
+            _libraryMonitor.ReportFileSystemChangeBeginning(path);
+            _libraryMonitor.ReportFileSystemChangeBeginning(parentFolder);
 
 
             try
             try
             {
             {
@@ -223,8 +223,8 @@ namespace MediaBrowser.Server.Implementations.Providers
             }
             }
             finally
             finally
             {
             {
-                _directoryWatchers.RemoveTempIgnore(path);
-                _directoryWatchers.RemoveTempIgnore(parentFolder);
+                _libraryMonitor.ReportFileSystemChangeComplete(path, false);
+                _libraryMonitor.ReportFileSystemChangeComplete(parentFolder, false);
             }
             }
         }
         }
 
 
@@ -348,6 +348,9 @@ namespace MediaBrowser.Server.Implementations.Providers
                 case ImageType.Art:
                 case ImageType.Art:
                     filename = "clearart";
                     filename = "clearart";
                     break;
                     break;
+                case ImageType.BoxRear:
+                    filename = "back";
+                    break;
                 case ImageType.Disc:
                 case ImageType.Disc:
                     filename = item is MusicAlbum ? "cdart" : "disc";
                     filename = item is MusicAlbum ? "cdart" : "disc";
                     break;
                     break;

+ 435 - 0
MediaBrowser.Providers/Manager/ItemImageProvider.cs

@@ -0,0 +1,435 @@
+using MediaBrowser.Common.Extensions;
+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();
+
+            var providerIds = new List<Guid>();
+
+            foreach (var provider in providers.OfType<IRemoteImageProvider>())
+            {
+                await RefreshFromProvider(item, provider, options, result, cancellationToken).ConfigureAwait(false);
+
+                providerIds.Add(provider.GetType().FullName.GetMD5());
+            }
+
+            foreach (var provider in providers.OfType<IDynamicImageProvider>())
+            {
+                await RefreshFromProvider(item, provider, result, cancellationToken).ConfigureAwait(false);
+
+                providerIds.Add(provider.GetType().FullName.GetMD5());
+            }
+
+            result.Providers = providerIds;
+
+            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.GetSupportedImages(item);
+
+                foreach (var imageType in images)
+                {
+                    if (!item.HasImage(imageType))
+                    {
+                        var response = await provider.GetImage(item, imageType, cancellationToken).ConfigureAwait(false);
+
+                        if (response.HasImage)
+                        {
+                            var mimeType = "image/" + response.Format.ToString().ToLower();
+
+                            await _providerManager.SaveImage((BaseItem)item, response.Stream, mimeType, imageType, 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;
+        }
+    }
+}

+ 358 - 0
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -0,0 +1,358 @@
+using MediaBrowser.Common.Extensions;
+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 readonly IProviderRepository _providerRepo;
+
+        private IMetadataProvider<TItemType>[] _providers = { };
+
+        private IImageProvider[] _imageProviders = { };
+
+        protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo)
+        {
+            ServerConfigurationManager = serverConfigurationManager;
+            Logger = logger;
+            ProviderManager = providerManager;
+            _providerRepo = providerRepo;
+        }
+
+        /// <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>>()
+                .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(MetadataStatus result)
+        {
+            return _providerRepo.SaveMetadataStatus(result, CancellationToken.None);
+        }
+
+        /// <summary>
+        /// Gets the last result.
+        /// </summary>
+        /// <param name="itemId">The item identifier.</param>
+        /// <returns>ProviderResult.</returns>
+        protected MetadataStatus GetLastResult(Guid itemId)
+        {
+            return _providerRepo.GetMetadataStatus(itemId) ?? new MetadataStatus { 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 = lastResult;
+            refreshResult.LastErrorMessage = string.Empty;
+            refreshResult.LastStatus = ProviderRefreshStatus.Success;
+
+            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.DateLastMetadataRefresh.HasValue, 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.SetDateLastMetadataRefresh(DateTime.UtcNow);
+                    refreshResult.AddImageProvidersRefreshed(result.Providers);
+                }
+            }
+
+            // Next run remote image providers, but only if local image providers didn't throw an exception
+            if (!localImagesFailed)
+            {
+                if ((options.ImageRefreshMode == MetadataRefreshMode.EnsureMetadata && !lastResult.DateLastImagesRefresh.HasValue) ||
+                                            options.ImageRefreshMode == MetadataRefreshMode.FullRefresh)
+                {
+                    var result = await itemImageProvider.RefreshImages(itemOfType, imageProviders, options, cancellationToken).ConfigureAwait(false);
+
+                    updateType = updateType | result.UpdateType;
+                    refreshResult.AddStatus(result.Status, result.ErrorMessage);
+                    refreshResult.SetDateLastImagesRefresh(DateTime.UtcNow);
+                    refreshResult.AddImageProvidersRefreshed(result.Providers);
+                }
+            }
+
+            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.IsDirty)
+            {
+                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, currentItem.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>
+        /// 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(IHasMetadata 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,
+                Providers = providers.Select(i => i.GetType().FullName.GetMD5()).ToList()
+            };
+
+            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; }
+        public List<Guid> Providers { get; set; }
+    }
+}

+ 66 - 33
MediaBrowser.Server.Implementations/Providers/ProviderManager.cs → MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -2,7 +2,6 @@
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
@@ -17,7 +16,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
-namespace MediaBrowser.Server.Implementations.Providers
+namespace MediaBrowser.Providers.Manager
 {
 {
     /// <summary>
     /// <summary>
     /// Class ProviderManager
     /// Class ProviderManager
@@ -37,7 +36,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <summary>
         /// <summary>
         /// The _directory watchers
         /// The _directory watchers
         /// </summary>
         /// </summary>
-        private readonly IDirectoryWatchers _directoryWatchers;
+        private readonly ILibraryMonitor _libraryMonitor;
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the configuration manager.
         /// Gets or sets the configuration manager.
@@ -51,26 +50,32 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <value>The metadata providers enumerable.</value>
         /// <value>The metadata providers enumerable.</value>
         private BaseMetadataProvider[] MetadataProviders { get; set; }
         private BaseMetadataProvider[] MetadataProviders { get; set; }
 
 
+        private IRemoteImageProvider[] RemoteImageProviders { get; set; }
         private IImageProvider[] ImageProviders { get; set; }
         private IImageProvider[] ImageProviders { get; set; }
+
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
 
 
-        private readonly IItemRepository _itemRepo;
+        private readonly IProviderRepository _providerRepo;
+
+        private IMetadataService[] _metadataServices = { };
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ProviderManager" /> class.
         /// Initializes a new instance of the <see cref="ProviderManager" /> class.
         /// </summary>
         /// </summary>
         /// <param name="httpClient">The HTTP client.</param>
         /// <param name="httpClient">The HTTP client.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
-        /// <param name="directoryWatchers">The directory watchers.</param>
+        /// <param name="libraryMonitor">The directory watchers.</param>
         /// <param name="logManager">The log manager.</param>
         /// <param name="logManager">The log manager.</param>
-        public ProviderManager(IHttpClient httpClient, IServerConfigurationManager configurationManager, IDirectoryWatchers directoryWatchers, ILogManager logManager, IFileSystem fileSystem, IItemRepository itemRepo)
+        /// <param name="fileSystem">The file system.</param>
+        /// <param name="providerRepo">The provider repo.</param>
+        public ProviderManager(IHttpClient httpClient, IServerConfigurationManager configurationManager, ILibraryMonitor libraryMonitor, ILogManager logManager, IFileSystem fileSystem, IProviderRepository providerRepo)
         {
         {
             _logger = logManager.GetLogger("ProviderManager");
             _logger = logManager.GetLogger("ProviderManager");
             _httpClient = httpClient;
             _httpClient = httpClient;
             ConfigurationManager = configurationManager;
             ConfigurationManager = configurationManager;
-            _directoryWatchers = directoryWatchers;
+            _libraryMonitor = libraryMonitor;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
-            _itemRepo = itemRepo;
+            _providerRepo = providerRepo;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -78,11 +83,34 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// </summary>
         /// </summary>
         /// <param name="providers">The providers.</param>
         /// <param name="providers">The providers.</param>
         /// <param name="imageProviders">The image 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();
             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>
         /// <summary>
@@ -91,9 +119,9 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="force">if set to <c>true</c> [force].</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>
         /// <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)
             if (item == null)
             {
             {
@@ -108,7 +136,7 @@ namespace MediaBrowser.Server.Implementations.Providers
 
 
             var providerHistories = item.DateLastSaved == default(DateTime) ?
             var providerHistories = item.DateLastSaved == default(DateTime) ?
                 new List<BaseProviderInfo>() :
                 new List<BaseProviderInfo>() :
-                _itemRepo.GetProviderHistory(item.Id).ToList();
+                _providerRepo.GetProviderHistory(item.Id).ToList();
 
 
             // Run the normal providers sequentially in order of priority
             // Run the normal providers sequentially in order of priority
             foreach (var provider in MetadataProviders)
             foreach (var provider in MetadataProviders)
@@ -126,12 +154,6 @@ namespace MediaBrowser.Server.Implementations.Providers
                     continue;
                     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
                 // 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
                 //  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)
                 if (provider.RequiresInternet && item.DontFetchMeta && provider.EnforceDontFetchMetadata)
@@ -179,7 +201,7 @@ namespace MediaBrowser.Server.Implementations.Providers
 
 
             if (result.HasValue || force)
             if (result.HasValue || force)
             {
             {
-                await _itemRepo.SaveProviderHistory(item.Id, providerHistories, cancellationToken);
+                await _providerRepo.SaveProviderHistory(item.Id, providerHistories, cancellationToken);
             }
             }
 
 
             return result;
             return result;
@@ -293,7 +315,7 @@ namespace MediaBrowser.Server.Implementations.Providers
             }
             }
 
 
             //Tell the watchers to ignore
             //Tell the watchers to ignore
-            _directoryWatchers.TemporarilyIgnore(path);
+            _libraryMonitor.ReportFileSystemChangeBeginning(path);
 
 
             if (dataToSave.CanSeek)
             if (dataToSave.CanSeek)
             {
             {
@@ -316,7 +338,7 @@ namespace MediaBrowser.Server.Implementations.Providers
             finally
             finally
             {
             {
                 //Remove the ignore
                 //Remove the ignore
-                _directoryWatchers.RemoveTempIgnore(path);
+                _libraryMonitor.ReportFileSystemChangeComplete(path, false);
             }
             }
         }
         }
 
 
@@ -358,7 +380,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, string sourceUrl, CancellationToken cancellationToken)
         public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, string sourceUrl, CancellationToken cancellationToken)
         {
         {
-            return new ImageSaver(ConfigurationManager, _directoryWatchers, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, sourceUrl, cancellationToken);
+            return new ImageSaver(ConfigurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, sourceUrl, cancellationToken);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -371,7 +393,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
         public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, CancellationToken cancellationToken, string providerName = null, ImageType? type = null)
         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))
             if (!string.IsNullOrEmpty(providerName))
             {
             {
@@ -396,7 +418,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="preferredLanguage">The preferred language.</param>
         /// <param name="preferredLanguage">The preferred language.</param>
         /// <param name="type">The type.</param>
         /// <param name="type">The type.</param>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
         /// <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
             try
             {
             {
@@ -414,7 +436,7 @@ namespace MediaBrowser.Server.Implementations.Providers
             }
             }
             catch (Exception ex)
             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>();
                 return new List<RemoteImageInfo>();
             }
             }
         }
         }
@@ -430,14 +452,9 @@ namespace MediaBrowser.Server.Implementations.Providers
             return images;
             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
                 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);
                     _logger.ErrorException("{0} failed in Supports for type {1}", ex, i.GetType().Name, item.GetType().Name);
                     return false;
                     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
+
             });
             });
         }
         }
     }
     }

+ 24 - 15
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -64,6 +64,17 @@
     </Reference>
     </Reference>
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
+    <Compile Include="All\LocalImageProvider.cs" />
+    <Compile Include="GameGenres\GameGenreMetadataService.cs" />
+    <Compile Include="Genres\GenreMetadataService.cs" />
+    <Compile Include="LiveTv\ChannelMetadataService.cs" />
+    <Compile Include="LiveTv\ChannelXmlProvider.cs" />
+    <Compile Include="LiveTv\ProgramMetadataService.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="CollectionFolderImageProvider.cs" />
     <Compile Include="FanartBaseProvider.cs" />
     <Compile Include="FanartBaseProvider.cs" />
     <Compile Include="FolderProviderFromXml.cs" />
     <Compile Include="FolderProviderFromXml.cs" />
@@ -72,14 +83,10 @@
     <Compile Include="Games\GameSystemProviderFromXml.cs" />
     <Compile Include="Games\GameSystemProviderFromXml.cs" />
     <Compile Include="ImageFromMediaLocationProvider.cs" />
     <Compile Include="ImageFromMediaLocationProvider.cs" />
     <Compile Include="ImagesByNameProvider.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="ImagesByName\ImageUtils.cs" />
-    <Compile Include="LiveTv\ChannelProviderFromXml.cs" />
     <Compile Include="MediaInfo\AudioImageProvider.cs" />
     <Compile Include="MediaInfo\AudioImageProvider.cs" />
     <Compile Include="MediaInfo\BaseFFProbeProvider.cs" />
     <Compile Include="MediaInfo\BaseFFProbeProvider.cs" />
     <Compile Include="MediaInfo\FFProbeAudioInfoProvider.cs" />
     <Compile Include="MediaInfo\FFProbeAudioInfoProvider.cs" />
@@ -88,8 +95,8 @@
     <Compile Include="Movies\BoxSetProviderFromXml.cs" />
     <Compile Include="Movies\BoxSetProviderFromXml.cs" />
     <Compile Include="Movies\ManualMovieDbImageProvider.cs" />
     <Compile Include="Movies\ManualMovieDbImageProvider.cs" />
     <Compile Include="Movies\ManualFanartMovieImageProvider.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\MovieUpdatesPrescanTask.cs" />
     <Compile Include="Movies\MovieXmlParser.cs" />
     <Compile Include="Movies\MovieXmlParser.cs" />
     <Compile Include="Movies\FanArtMovieProvider.cs" />
     <Compile Include="Movies\FanArtMovieProvider.cs" />
@@ -98,8 +105,6 @@
     <Compile Include="Movies\MovieDbProvider.cs" />
     <Compile Include="Movies\MovieDbProvider.cs" />
     <Compile Include="Movies\MovieProviderFromXml.cs" />
     <Compile Include="Movies\MovieProviderFromXml.cs" />
     <Compile Include="Movies\OpenMovieDatabaseProvider.cs" />
     <Compile Include="Movies\OpenMovieDatabaseProvider.cs" />
-    <Compile Include="Movies\PersonProviderFromXml.cs" />
-    <Compile Include="Movies\MovieDbPersonProvider.cs" />
     <Compile Include="Music\AlbumInfoFromSongProvider.cs" />
     <Compile Include="Music\AlbumInfoFromSongProvider.cs" />
     <Compile Include="Music\AlbumProviderFromXml.cs" />
     <Compile Include="Music\AlbumProviderFromXml.cs" />
     <Compile Include="Music\ArtistInfoFromSongProvider.cs" />
     <Compile Include="Music\ArtistInfoFromSongProvider.cs" />
@@ -118,7 +123,11 @@
     <Compile Include="Music\MusicBrainzAlbumProvider.cs" />
     <Compile Include="Music\MusicBrainzAlbumProvider.cs" />
     <Compile Include="Music\MusicVideoXmlParser.cs" />
     <Compile Include="Music\MusicVideoXmlParser.cs" />
     <Compile Include="Music\SoundtrackPostScanTask.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="Properties\AssemblyInfo.cs" />
+    <Compile Include="ProviderUtils.cs" />
     <Compile Include="RefreshIntrosTask.cs" />
     <Compile Include="RefreshIntrosTask.cs" />
     <Compile Include="Savers\AlbumXmlSaver.cs" />
     <Compile Include="Savers\AlbumXmlSaver.cs" />
     <Compile Include="Savers\ArtistXmlSaver.cs" />
     <Compile Include="Savers\ArtistXmlSaver.cs" />
@@ -133,8 +142,8 @@
     <Compile Include="Savers\SeasonXmlSaver.cs" />
     <Compile Include="Savers\SeasonXmlSaver.cs" />
     <Compile Include="Savers\SeriesXmlSaver.cs" />
     <Compile Include="Savers\SeriesXmlSaver.cs" />
     <Compile Include="Savers\XmlSaverHelpers.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\EpisodeImageFromMediaLocationProvider.cs" />
     <Compile Include="TV\EpisodeIndexNumberProvider.cs" />
     <Compile Include="TV\EpisodeIndexNumberProvider.cs" />
     <Compile Include="TV\EpisodeProviderFromXml.cs" />
     <Compile Include="TV\EpisodeProviderFromXml.cs" />
@@ -145,7 +154,7 @@
     <Compile Include="TV\ManualFanartSeasonProvider.cs" />
     <Compile Include="TV\ManualFanartSeasonProvider.cs" />
     <Compile Include="TV\ManualFanartSeriesProvider.cs" />
     <Compile Include="TV\ManualFanartSeriesProvider.cs" />
     <Compile Include="TV\ManualTvdbEpisodeImageProvider.cs" />
     <Compile Include="TV\ManualTvdbEpisodeImageProvider.cs" />
-    <Compile Include="TV\ManualTvdbPersonImageProvider.cs" />
+    <Compile Include="People\TvdbPersonImageProvider.cs" />
     <Compile Include="TV\ManualTvdbSeasonImageProvider.cs" />
     <Compile Include="TV\ManualTvdbSeasonImageProvider.cs" />
     <Compile Include="TV\ManualTvdbSeriesImageProvider.cs" />
     <Compile Include="TV\ManualTvdbSeriesImageProvider.cs" />
     <Compile Include="TV\SeasonIndexNumberProvider.cs" />
     <Compile Include="TV\SeasonIndexNumberProvider.cs" />
@@ -157,7 +166,6 @@
     <Compile Include="TV\SeriesPostScanTask.cs" />
     <Compile Include="TV\SeriesPostScanTask.cs" />
     <Compile Include="TV\SeriesProviderFromXml.cs" />
     <Compile Include="TV\SeriesProviderFromXml.cs" />
     <Compile Include="TV\SeriesXmlParser.cs" />
     <Compile Include="TV\SeriesXmlParser.cs" />
-    <Compile Include="TV\TvdbPersonImageProvider.cs" />
     <Compile Include="TV\TvdbPrescanTask.cs" />
     <Compile Include="TV\TvdbPrescanTask.cs" />
     <Compile Include="TV\TvdbSeriesImageProvider.cs" />
     <Compile Include="TV\TvdbSeriesImageProvider.cs" />
     <Compile Include="UserRootFolderNameProvider.cs" />
     <Compile Include="UserRootFolderNameProvider.cs" />
@@ -180,6 +188,7 @@
   <ItemGroup>
   <ItemGroup>
     <None Include="packages.config" />
     <None Include="packages.config" />
   </ItemGroup>
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
   <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. 
   <!-- 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.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
@@ -16,14 +17,16 @@ using System.Xml;
 
 
 namespace MediaBrowser.Providers.Movies
 namespace MediaBrowser.Providers.Movies
 {
 {
-    public class ManualFanartMovieImageProvider : IImageProvider
+    public class ManualFanartMovieImageProvider : IRemoteImageProvider
     {
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualFanartMovieImageProvider(IServerConfigurationManager config)
+        public ManualFanartMovieImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -41,6 +44,20 @@ namespace MediaBrowser.Providers.Movies
             return FanArtMovieProvider.SupportsItem(item);
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             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; }
             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.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
@@ -14,15 +15,17 @@ using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Providers.Movies
 namespace MediaBrowser.Providers.Movies
 {
 {
-    class ManualMovieDbImageProvider : IImageProvider
+    class ManualMovieDbImageProvider : IRemoteImageProvider
     {
     {
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualMovieDbImageProvider(IJsonSerializer jsonSerializer, IServerConfigurationManager config)
+        public ManualMovieDbImageProvider(IJsonSerializer jsonSerializer, IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -40,6 +43,15 @@ namespace MediaBrowser.Providers.Movies
             return MovieDbImagesProvider.SupportsItem(item);
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -167,9 +179,19 @@ namespace MediaBrowser.Providers.Movies
             return null;
             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
-    }
-}

+ 2 - 1
MediaBrowser.Providers/Movies/MovieDbProvider.cs

@@ -697,7 +697,8 @@ namespace MediaBrowser.Providers.Movies
             }
             }
             if (!movie.LockedFields.Contains(MetadataFields.Overview))
             if (!movie.LockedFields.Contains(MetadataFields.Overview))
             {
             {
-                movie.Overview = WebUtility.HtmlDecode(movieData.overview);
+                // Bug in Mono: WebUtility.HtmlDecode should return null if the string is null but in Mono it generate an System.ArgumentNullException.
+                movie.Overview = movieData.overview != null ? WebUtility.HtmlDecode(movieData.overview) : null;
                 movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null;
                 movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null;
             }
             }
             movie.HomePageUrl = movieData.homepage;
             movie.HomePageUrl = movieData.homepage;

+ 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;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
@@ -17,14 +18,16 @@ using System.Xml;
 
 
 namespace MediaBrowser.Providers.Music
 namespace MediaBrowser.Providers.Music
 {
 {
-    public class ManualFanartAlbumProvider : IImageProvider
+    public class ManualFanartAlbumProvider : IRemoteImageProvider
     {
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualFanartAlbumProvider(IServerConfigurationManager config)
+        public ManualFanartAlbumProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -42,6 +45,15 @@ namespace MediaBrowser.Providers.Music
             return item is MusicAlbum;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -325,9 +337,19 @@ namespace MediaBrowser.Providers.Music
             list.Add(info);
             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;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
@@ -17,14 +18,16 @@ using System.Xml;
 
 
 namespace MediaBrowser.Providers.Music
 namespace MediaBrowser.Providers.Music
 {
 {
-    public class ManualFanartArtistProvider : IImageProvider
+    public class ManualFanartArtistProvider : IRemoteImageProvider
     {
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualFanartArtistProvider(IServerConfigurationManager config)
+        public ManualFanartArtistProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -42,6 +45,18 @@ namespace MediaBrowser.Providers.Music
             return item is MusicArtist;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -334,9 +349,19 @@ namespace MediaBrowser.Providers.Music
             list.Add(info);
             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.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
@@ -11,8 +12,15 @@ using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Providers.Music
 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
         public string Name
         {
         {
             get { return ProviderName; }
             get { return ProviderName; }
@@ -28,6 +36,14 @@ namespace MediaBrowser.Providers.Music
             return item is MusicAlbum || item is MusicArtist;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -72,7 +88,8 @@ namespace MediaBrowser.Providers.Music
             var info = new RemoteImageInfo
             var info = new RemoteImageInfo
             {
             {
                 ProviderName = Name,
                 ProviderName = Name,
-                Url = url
+                Url = url,
+                Type = ImageType.Primary
             };
             };
 
 
             if (string.Equals(size, "mega", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(size, "mega", StringComparison.OrdinalIgnoreCase))
@@ -95,9 +112,19 @@ namespace MediaBrowser.Providers.Music
             return info;
             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
+            });
         }
         }
     }
     }
 }
 }

+ 10 - 5
MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -20,11 +21,13 @@ namespace MediaBrowser.Providers.Music
         internal static MusicBrainzAlbumProvider Current;
         internal static MusicBrainzAlbumProvider Current;
 
 
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
+        private readonly IApplicationHost _appHost;
 
 
-        public MusicBrainzAlbumProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IHttpClient httpClient)
+        public MusicBrainzAlbumProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IHttpClient httpClient, IApplicationHost appHost)
             : base(logManager, configurationManager)
             : base(logManager, configurationManager)
         {
         {
             _httpClient = httpClient;
             _httpClient = httpClient;
+            _appHost = appHost;
 
 
             Current = this;
             Current = this;
         }
         }
@@ -83,7 +86,7 @@ namespace MediaBrowser.Providers.Music
 
 
         private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken)
         private async Task<ReleaseResult> GetReleaseResult(string albumName, string artistId, CancellationToken cancellationToken)
         {
         {
-            var url = string.Format("http://www.musicbrainz.org/ws/2/release/?query=\"{0}\" and arid:{1}",
+            var url = string.Format("http://www.musicbrainz.org/ws/2/release/?query=\"{0}\" AND arid:{1}",
                 WebUtility.UrlEncode(albumName),
                 WebUtility.UrlEncode(albumName),
                 artistId);
                 artistId);
 
 
@@ -94,7 +97,7 @@ namespace MediaBrowser.Providers.Music
 
 
         private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken)
         private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken)
         {
         {
-            var url = string.Format("http://www.musicbrainz.org/ws/2/release/?query=\"{0}\" and artist:\"{1}\"",
+            var url = string.Format("http://www.musicbrainz.org/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"",
                 WebUtility.UrlEncode(albumName),
                 WebUtility.UrlEncode(albumName),
                 WebUtility.UrlEncode(artistName));
                 WebUtility.UrlEncode(artistName));
 
 
@@ -189,11 +192,13 @@ namespace MediaBrowser.Providers.Music
 
 
                 var doc = new XmlDocument();
                 var doc = new XmlDocument();
 
 
+                var userAgent = _appHost.Name + "/" + _appHost.ApplicationVersion;
+
                 using (var xml = await _httpClient.Get(new HttpRequestOptions
                 using (var xml = await _httpClient.Get(new HttpRequestOptions
                 {
                 {
                     Url = url,
                     Url = url,
                     CancellationToken = cancellationToken,
                     CancellationToken = cancellationToken,
-                    UserAgent = Environment.MachineName
+                    UserAgent = userAgent
 
 
                 }).ConfigureAwait(false))
                 }).ConfigureAwait(false))
                 {
                 {

+ 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.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Genres;
+using MediaBrowser.Providers.ImagesByName;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 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 IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
@@ -22,7 +24,7 @@ namespace MediaBrowser.Providers.ImagesByName
 
 
         private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1);
         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;
             _config = config;
             _httpClient = httpClient;
             _httpClient = httpClient;
@@ -44,6 +46,15 @@ namespace MediaBrowser.Providers.ImagesByName
             return item is MusicGenre;
             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)
         public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, 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);
             return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken);
         }
         }
 
 
-        public int Priority
+        public int Order
         {
         {
             get { return 0; }
             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, IProviderRepository providerRepo, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo)
+        {
+            _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.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
+using MediaBrowser.Providers.Movies;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 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 IServerConfigurationManager _config;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IJsonSerializer _jsonSerializer;
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualMovieDbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer)
+        public MovieDbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
             _jsonSerializer = jsonSerializer;
             _jsonSerializer = jsonSerializer;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -38,6 +42,14 @@ namespace MediaBrowser.Providers.Movies
             return item is Person;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             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();
             return profile.iso_639_1 == null ? null : profile.iso_639_1.ToString();
         }
         }
 
 
-        public int Priority
+        public int Order
         {
         {
             get { return 0; }
             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, IProviderRepository providerRepo, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo)
+        {
+            _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);
+        }
+    }
+}

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

@@ -0,0 +1,59 @@
+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");
+        }
+    }
+}

+ 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;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.TV;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -14,17 +16,19 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using System.Xml;
 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 IServerConfigurationManager _config;
         private readonly ILibraryManager _library;
         private readonly ILibraryManager _library;
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualTvdbPersonImageProvider(IServerConfigurationManager config, ILibraryManager library)
+        public TvdbPersonImageProvider(IServerConfigurationManager config, ILibraryManager library, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
             _library = library;
             _library = library;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -42,6 +46,14 @@ namespace MediaBrowser.Providers.TV
             return item is Person;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
@@ -184,9 +196,19 @@ namespace MediaBrowser.Providers.TV
             return null;
             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, IProviderRepository providerRepo, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo)
+        {
+            _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.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Genres;
+using MediaBrowser.Providers.ImagesByName;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 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 IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
@@ -21,7 +23,7 @@ namespace MediaBrowser.Providers.ImagesByName
 
 
         private readonly SemaphoreSlim _listResourcePool = new SemaphoreSlim(1, 1);
         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;
             _config = config;
             _httpClient = httpClient;
             _httpClient = httpClient;
@@ -43,6 +45,15 @@ namespace MediaBrowser.Providers.ImagesByName
             return item is Studio;
             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)
         public Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             return GetImages(item, imageType == ImageType.Primary, imageType == ImageType.Thumb, 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);
             return ImageUtils.EnsureList(url, file, _httpClient, _fileSystem, _listResourcePool, cancellationToken);
         }
         }
 
 
-        public int Priority
+        public int Order
         {
         {
             get { return 0; }
             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;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
@@ -17,14 +18,16 @@ using System.Xml;
 
 
 namespace MediaBrowser.Providers.TV
 namespace MediaBrowser.Providers.TV
 {
 {
-    public class ManualFanartSeasonImageProvider : IImageProvider
+    public class ManualFanartSeasonImageProvider : IRemoteImageProvider
     {
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualFanartSeasonImageProvider(IServerConfigurationManager config)
+        public ManualFanartSeasonImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -42,6 +45,15 @@ namespace MediaBrowser.Providers.TV
             return item is Season;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             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;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
@@ -17,14 +18,16 @@ using System.Xml;
 
 
 namespace MediaBrowser.Providers.TV
 namespace MediaBrowser.Providers.TV
 {
 {
-    public class ManualFanartSeriesImageProvider : IImageProvider
+    public class ManualFanartSeriesImageProvider : IRemoteImageProvider
     {
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualFanartSeriesImageProvider(IServerConfigurationManager config)
+        public ManualFanartSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -42,6 +45,19 @@ namespace MediaBrowser.Providers.TV
             return item is Series;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             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;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -16,14 +17,16 @@ using System.Xml;
 
 
 namespace MediaBrowser.Providers.TV
 namespace MediaBrowser.Providers.TV
 {
 {
-    public class ManualTvdbEpisodeImageProvider : IImageProvider
+    public class ManualTvdbEpisodeImageProvider : IRemoteImageProvider
     {
     {
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualTvdbEpisodeImageProvider(IServerConfigurationManager config)
+        public ManualTvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -36,6 +39,14 @@ namespace MediaBrowser.Providers.TV
             return item is Episode;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             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; }
             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;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -18,14 +19,16 @@ using System.Xml;
 
 
 namespace MediaBrowser.Providers.TV
 namespace MediaBrowser.Providers.TV
 {
 {
-    public class ManualTvdbSeasonImageProvider : IImageProvider
+    public class ManualTvdbSeasonImageProvider : IRemoteImageProvider
     {
     {
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly IHttpClient _httpClient;
 
 
-        public ManualTvdbSeasonImageProvider(IServerConfigurationManager config)
+        public ManualTvdbSeasonImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -43,6 +46,16 @@ namespace MediaBrowser.Providers.TV
             return item is Season;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             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;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -18,14 +19,16 @@ using System.Xml;
 
 
 namespace MediaBrowser.Providers.TV
 namespace MediaBrowser.Providers.TV
 {
 {
-    public class ManualTvdbSeriesImageProvider : IImageProvider
+    public class ManualTvdbSeriesImageProvider : IRemoteImageProvider
     {
     {
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
 
-        public ManualTvdbSeriesImageProvider(IServerConfigurationManager config)
+        public ManualTvdbSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
         {
             _config = config;
             _config = config;
+            _httpClient = httpClient;
         }
         }
 
 
         public string Name
         public string Name
@@ -43,6 +46,16 @@ namespace MediaBrowser.Providers.TV
             return item is Series;
             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)
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             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)
         public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
         {
         {
             item.ValidateImages();
             item.ValidateImages();
-            item.ValidateBackdrops();
-
-            var hasScreenshots = item as IHasScreenshots;
-
-            if (hasScreenshots != null)
-            {
-                hasScreenshots.ValidateScreenshots();
-            }
 
 
             SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
             SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
             return TrueTaskResult;
             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="image">The image.</param>
         /// <param name="outputFormat">The output format.</param>
         /// <param name="outputFormat">The output format.</param>
         /// <returns>ImageFormat.</returns>
         /// <returns>ImageFormat.</returns>
-        private ImageFormat GetOutputFormat(Image image, ImageOutputFormat outputFormat)
+        private System.Drawing.Imaging.ImageFormat GetOutputFormat(Image image, ImageOutputFormat outputFormat)
         {
         {
             switch (outputFormat)
             switch (outputFormat)
             {
             {
                 case ImageOutputFormat.Bmp:
                 case ImageOutputFormat.Bmp:
-                    return ImageFormat.Bmp;
+                    return System.Drawing.Imaging.ImageFormat.Bmp;
                 case ImageOutputFormat.Gif:
                 case ImageOutputFormat.Gif:
-                    return ImageFormat.Gif;
+                    return System.Drawing.Imaging.ImageFormat.Gif;
                 case ImageOutputFormat.Jpg:
                 case ImageOutputFormat.Jpg:
-                    return ImageFormat.Jpeg;
+                    return System.Drawing.Imaging.ImageFormat.Jpeg;
                 case ImageOutputFormat.Png:
                 case ImageOutputFormat.Png:
-                    return ImageFormat.Png;
+                    return System.Drawing.Imaging.ImageFormat.Png;
                 default:
                 default:
                     return image.RawFormat;
                     return image.RawFormat;
             }
             }
@@ -787,7 +787,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
                                 //And then save it in the cache
                                 //And then save it in the cache
                                 using (var outputStream = _fileSystem.GetFileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
                                 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;
                     dto.SpecialFeatureCount = specialFeatureCount;
                 }
                 }
+
+                if (fields.Contains(ItemFields.TmdbCollectionName))
+                {
+                    dto.TmdbCollectionName = movie.TmdbCollectionName;
+                }
             }
             }
 
 
             // Add EpisodeInfo
             // Add EpisodeInfo

+ 13 - 7
MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 {
 {
     public class EpisodeFileOrganizer
     public class EpisodeFileOrganizer
     {
     {
-        private readonly IDirectoryWatchers _directoryWatchers;
+        private readonly ILibraryMonitor _libraryMonitor;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
@@ -31,14 +31,14 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
 
-        public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, IDirectoryWatchers directoryWatchers)
+        public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor)
         {
         {
             _organizationService = organizationService;
             _organizationService = organizationService;
             _config = config;
             _config = config;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _logger = logger;
             _logger = logger;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
-            _directoryWatchers = directoryWatchers;
+            _libraryMonitor = libraryMonitor;
         }
         }
 
 
         public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting)
         public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting)
@@ -174,6 +174,8 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                 {
                 {
                     _logger.Debug("Removing duplicate episode {0}", path);
                     _logger.Debug("Removing duplicate episode {0}", path);
 
 
+                    _libraryMonitor.ReportFileSystemChangeBeginning(path);
+
                     try
                     try
                     {
                     {
                         File.Delete(path);
                         File.Delete(path);
@@ -182,6 +184,10 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                     {
                     {
                         _logger.ErrorException("Error removing duplicate episode", ex, path);
                         _logger.ErrorException("Error removing duplicate episode", ex, path);
                     }
                     }
+                    finally
+                    {
+                        _libraryMonitor.ReportFileSystemChangeComplete(path, true);
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -232,7 +238,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 
 
         private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result)
         private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result)
         {
         {
-            _directoryWatchers.TemporarilyIgnore(result.TargetPath);
+            _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath);
 
 
             Directory.CreateDirectory(Path.GetDirectoryName(result.TargetPath));
             Directory.CreateDirectory(Path.GetDirectoryName(result.TargetPath));
 
 
@@ -264,7 +270,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
             }
             }
             finally
             finally
             {
             {
-                _directoryWatchers.RemoveTempIgnore(result.TargetPath);
+                _libraryMonitor.ReportFileSystemChangeComplete(result.TargetPath, true);
             }
             }
 
 
             if (copy)
             if (copy)
@@ -376,8 +382,8 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 
 
         private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options)
         private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options)
         {
         {
-            seriesName = _fileSystem.GetValidFilename(seriesName);
-            episodeTitle = _fileSystem.GetValidFilename(episodeTitle);
+            seriesName = _fileSystem.GetValidFilename(seriesName).Trim();
+            episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim();
 
 
             var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.');
             var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.');
 
 

+ 5 - 11
MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs

@@ -21,17 +21,17 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
         private readonly ITaskManager _taskManager;
         private readonly ITaskManager _taskManager;
         private readonly IFileOrganizationRepository _repo;
         private readonly IFileOrganizationRepository _repo;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
-        private readonly IDirectoryWatchers _directoryWatchers;
+        private readonly ILibraryMonitor _libraryMonitor;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
 
 
-        public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, IDirectoryWatchers directoryWatchers, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem)
+        public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem)
         {
         {
             _taskManager = taskManager;
             _taskManager = taskManager;
             _repo = repo;
             _repo = repo;
             _logger = logger;
             _logger = logger;
-            _directoryWatchers = directoryWatchers;
+            _libraryMonitor = libraryMonitor;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _config = config;
             _config = config;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
@@ -91,13 +91,10 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
             }
             }
 
 
             var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
             var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
-                _directoryWatchers);
+                _libraryMonitor);
 
 
             await organizer.OrganizeEpisodeFile(result.OriginalPath, _config.Configuration.TvFileOrganizationOptions, true)
             await organizer.OrganizeEpisodeFile(result.OriginalPath, _config.Configuration.TvFileOrganizationOptions, true)
                     .ConfigureAwait(false);
                     .ConfigureAwait(false);
-
-            await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None)
-                    .ConfigureAwait(false);
         }
         }
 
 
         public Task ClearLog()
         public Task ClearLog()
@@ -108,12 +105,9 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
         public async Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request)
         public async Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request)
         {
         {
             var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
             var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager,
-                _directoryWatchers);
+                _libraryMonitor);
 
 
             await organizer.OrganizeWithCorrection(request, _config.Configuration.TvFileOrganizationOptions).ConfigureAwait(false);
             await organizer.OrganizeWithCorrection(request, _config.Configuration.TvFileOrganizationOptions).ConfigureAwait(false);
-
-            await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None)
-                    .ConfigureAwait(false);
         }
         }
     }
     }
 }
 }

+ 4 - 4
MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs

@@ -14,16 +14,16 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 {
 {
     public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask
     public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask
     {
     {
-        private readonly IDirectoryWatchers _directoryWatchers;
+        private readonly ILibraryMonitor _libraryMonitor;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IFileOrganizationService _organizationService;
         private readonly IFileOrganizationService _organizationService;
 
 
-        public OrganizerScheduledTask(IDirectoryWatchers directoryWatchers, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IFileOrganizationService organizationService)
+        public OrganizerScheduledTask(ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IFileOrganizationService organizationService)
         {
         {
-            _directoryWatchers = directoryWatchers;
+            _libraryMonitor = libraryMonitor;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _logger = logger;
             _logger = logger;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
@@ -48,7 +48,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 
 
         public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
         public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
         {
         {
-            return new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _directoryWatchers, _organizationService, _config)
+            return new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _libraryMonitor, _organizationService, _config)
                 .Organize(_config.Configuration.TvFileOrganizationOptions, cancellationToken, progress);
                 .Organize(_config.Configuration.TvFileOrganizationOptions, cancellationToken, progress);
         }
         }
 
 

+ 4 - 4
MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs

@@ -18,19 +18,19 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
 {
 {
     public class TvFolderOrganizer
     public class TvFolderOrganizer
     {
     {
-        private readonly IDirectoryWatchers _directoryWatchers;
+        private readonly ILibraryMonitor _libraryMonitor;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IFileOrganizationService _organizationService;
         private readonly IFileOrganizationService _organizationService;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
 
 
-        public TvFolderOrganizer(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IDirectoryWatchers directoryWatchers, IFileOrganizationService organizationService, IServerConfigurationManager config)
+        public TvFolderOrganizer(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IFileOrganizationService organizationService, IServerConfigurationManager config)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _logger = logger;
             _logger = logger;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
-            _directoryWatchers = directoryWatchers;
+            _libraryMonitor = libraryMonitor;
             _organizationService = organizationService;
             _organizationService = organizationService;
             _config = config;
             _config = config;
         }
         }
@@ -57,7 +57,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
                 foreach (var file in eligibleFiles)
                 foreach (var file in eligibleFiles)
                 {
                 {
                     var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager,
                     var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager,
-                        _directoryWatchers);
+                        _libraryMonitor);
 
 
                     var result = await organizer.OrganizeEpisodeFile(file.FullName, options, false).ConfigureAwait(false);
                     var result = await organizer.OrganizeEpisodeFile(file.FullName, options, false).ConfigureAwait(false);
 
 

+ 68 - 126
MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs → MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs

@@ -2,7 +2,6 @@
 using MediaBrowser.Common.ScheduledTasks;
 using MediaBrowser.Common.ScheduledTasks;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
@@ -18,10 +17,7 @@ using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Server.Implementations.IO
 namespace MediaBrowser.Server.Implementations.IO
 {
 {
-    /// <summary>
-    /// Class DirectoryWatchers
-    /// </summary>
-    public class DirectoryWatchers : IDirectoryWatchers
+    public class LibraryMonitor : ILibraryMonitor
     {
     {
         /// <summary>
         /// <summary>
         /// The file system watchers
         /// The file system watchers
@@ -55,17 +51,28 @@ namespace MediaBrowser.Server.Implementations.IO
         /// Add the path to our temporary ignore list.  Use when writing to a path within our listening scope.
         /// Add the path to our temporary ignore list.  Use when writing to a path within our listening scope.
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
-        public void TemporarilyIgnore(string path)
+        private void TemporarilyIgnore(string path)
         {
         {
             _tempIgnoredPaths[path] = path;
             _tempIgnoredPaths[path] = path;
         }
         }
 
 
-        /// <summary>
-        /// Removes the temp ignore.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        public async void RemoveTempIgnore(string path)
+        public void ReportFileSystemChangeBeginning(string path)
+        {
+            if (string.IsNullOrEmpty(path))
+            {
+                throw new ArgumentNullException("path");
+            }
+
+            TemporarilyIgnore(path);
+        }
+
+        public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
         {
         {
+            if (string.IsNullOrEmpty(path))
+            {
+                throw new ArgumentNullException("path");
+            }
+
             // This is an arbitraty amount of time, but delay it because file system writes often trigger events after RemoveTempIgnore has been called. 
             // This is an arbitraty amount of time, but delay it because file system writes often trigger events after RemoveTempIgnore has been called. 
             // Seeing long delays in some situations, especially over the network.
             // Seeing long delays in some situations, especially over the network.
             // Seeing delays up to 40 seconds, but not going to ignore changes for that long.
             // Seeing delays up to 40 seconds, but not going to ignore changes for that long.
@@ -73,6 +80,11 @@ namespace MediaBrowser.Server.Implementations.IO
 
 
             string val;
             string val;
             _tempIgnoredPaths.TryRemove(path, out val);
             _tempIgnoredPaths.TryRemove(path, out val);
+
+            if (refreshPath)
+            {
+                ReportFileSystemChanged(path);
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -91,11 +103,11 @@ namespace MediaBrowser.Server.Implementations.IO
         private IServerConfigurationManager ConfigurationManager { get; set; }
         private IServerConfigurationManager ConfigurationManager { get; set; }
 
 
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
-        
+
         /// <summary>
         /// <summary>
-        /// Initializes a new instance of the <see cref="DirectoryWatchers" /> class.
+        /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
         /// </summary>
         /// </summary>
-        public DirectoryWatchers(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem)
+        public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem)
         {
         {
             if (taskManager == null)
             if (taskManager == null)
             {
             {
@@ -104,7 +116,7 @@ namespace MediaBrowser.Server.Implementations.IO
 
 
             LibraryManager = libraryManager;
             LibraryManager = libraryManager;
             TaskManager = taskManager;
             TaskManager = taskManager;
-            Logger = logManager.GetLogger("DirectoryWatchers");
+            Logger = logManager.GetLogger(GetType().Name);
             ConfigurationManager = configurationManager;
             ConfigurationManager = configurationManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
 
 
@@ -328,31 +340,30 @@ namespace MediaBrowser.Server.Implementations.IO
             {
             {
                 OnWatcherChanged(e);
                 OnWatcherChanged(e);
             }
             }
-            catch (IOException ex)
+            catch (Exception ex)
             {
             {
-                Logger.ErrorException("IOException in watcher changed. Path: {0}", ex, e.FullPath);
+                Logger.ErrorException("Exception in watcher changed. Path: {0}", ex, e.FullPath);
             }
             }
         }
         }
 
 
         private void OnWatcherChanged(FileSystemEventArgs e)
         private void OnWatcherChanged(FileSystemEventArgs e)
         {
         {
-            var name = e.Name;
+            Logger.Debug("Watcher sees change of type " + e.ChangeType + " to " + e.FullPath);
 
 
-            // Ignore certain files
-            if (_alwaysIgnoreFiles.Contains(name, StringComparer.OrdinalIgnoreCase))
-            {
-                return;
-            }
+            ReportFileSystemChanged(e.FullPath);
+        }
 
 
-            var nameFromFullPath = Path.GetFileName(e.FullPath);
-            // Ignore certain files
-            if (!string.IsNullOrEmpty(nameFromFullPath) && _alwaysIgnoreFiles.Contains(nameFromFullPath, StringComparer.OrdinalIgnoreCase))
+        public void ReportFileSystemChanged(string path)
+        {
+            if (string.IsNullOrEmpty(path))
             {
             {
-                return;
+                throw new ArgumentNullException("path");
             }
             }
+            
+            var filename = Path.GetFileName(path);
 
 
-            // Ignore when someone manually creates a new folder
-            if (e.ChangeType == WatcherChangeTypes.Created && name == "New folder")
+            // Ignore certain files
+            if (!string.IsNullOrEmpty(filename) && _alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase))
             {
             {
                 return;
                 return;
             }
             }
@@ -362,36 +373,35 @@ namespace MediaBrowser.Server.Implementations.IO
             // If the parent of an ignored path has a change event, ignore that too
             // If the parent of an ignored path has a change event, ignore that too
             if (tempIgnorePaths.Any(i =>
             if (tempIgnorePaths.Any(i =>
             {
             {
-                if (string.Equals(i, e.FullPath, StringComparison.OrdinalIgnoreCase))
+                if (string.Equals(i, path, StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    Logger.Debug("Watcher ignoring change to {0}", e.FullPath);
+                    Logger.Debug("Ignoring change to {0}", path);
                     return true;
                     return true;
                 }
                 }
 
 
-                // Go up a level
-                var parent = Path.GetDirectoryName(i);
-                if (string.Equals(parent, e.FullPath, StringComparison.OrdinalIgnoreCase))
+                if (_fileSystem.ContainsSubPath(i, path))
                 {
                 {
-                    Logger.Debug("Watcher ignoring change to {0}", e.FullPath);
+                    Logger.Debug("Ignoring change to {0}", path);
                     return true;
                     return true;
                 }
                 }
 
 
-                // Go up another level
+                // Go up a level
+                var parent = Path.GetDirectoryName(i);
                 if (!string.IsNullOrEmpty(parent))
                 if (!string.IsNullOrEmpty(parent))
                 {
                 {
-                    parent = Path.GetDirectoryName(i);
-                    if (string.Equals(parent, e.FullPath, StringComparison.OrdinalIgnoreCase))
+                    if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase))
                     {
                     {
-                        Logger.Debug("Watcher ignoring change to {0}", e.FullPath);
+                        Logger.Debug("Ignoring change to {0}", path);
                         return true;
                         return true;
                     }
                     }
-                }
 
 
-                if (i.StartsWith(e.FullPath, StringComparison.OrdinalIgnoreCase) || 
-                    e.FullPath.StartsWith(i, StringComparison.OrdinalIgnoreCase))
-                {
-                    Logger.Debug("Watcher ignoring change to {0}", e.FullPath);
-                    return true;
+                    // Go up another level
+                    parent = Path.GetDirectoryName(i);
+                    if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase))
+                    {
+                        Logger.Debug("Ignoring change to {0}", path);
+                        return true;
+                    }
                 }
                 }
 
 
                 return false;
                 return false;
@@ -401,22 +411,19 @@ namespace MediaBrowser.Server.Implementations.IO
                 return;
                 return;
             }
             }
 
 
-            Logger.Info("Watcher sees change of type " + e.ChangeType + " to " + e.FullPath);
-
-            //Since we're watching created, deleted and renamed we always want the parent of the item to be the affected path
-            var affectedPath = e.FullPath;
-
-            _affectedPaths.AddOrUpdate(affectedPath, affectedPath, (key, oldValue) => affectedPath);
+            // Avoid implicitly captured closure
+            var affectedPath = path;
+            _affectedPaths.AddOrUpdate(path, path, (key, oldValue) => affectedPath);
 
 
             lock (_timerLock)
             lock (_timerLock)
             {
             {
                 if (_updateTimer == null)
                 if (_updateTimer == null)
                 {
                 {
-                    _updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
+                    _updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.RealtimeWatcherDelay), TimeSpan.FromMilliseconds(-1));
                 }
                 }
                 else
                 else
                 {
                 {
-                    _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
+                    _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.RealtimeWatcherDelay), TimeSpan.FromMilliseconds(-1));
                 }
                 }
             }
             }
         }
         }
@@ -427,24 +434,9 @@ namespace MediaBrowser.Server.Implementations.IO
         /// <param name="stateInfo">The state info.</param>
         /// <param name="stateInfo">The state info.</param>
         private async void TimerStopped(object stateInfo)
         private async void TimerStopped(object stateInfo)
         {
         {
-            lock (_timerLock)
-            {
-                // Extend the timer as long as any of the paths are still being written to.
-                if (_affectedPaths.Any(p => IsFileLocked(p.Key)))
-                {
-                    Logger.Info("Timer extended.");
-                    _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1));
-                    return;
-                }
-
-                Logger.Info("Timer stopped.");
+            Logger.Debug("Timer stopped.");
 
 
-                if (_updateTimer != null)
-                {
-                    _updateTimer.Dispose();
-                    _updateTimer = null;
-                }
-            }
+            DisposeTimer();
 
 
             var paths = _affectedPaths.Keys.ToList();
             var paths = _affectedPaths.Keys.ToList();
             _affectedPaths.Clear();
             _affectedPaths.Clear();
@@ -452,59 +444,16 @@ namespace MediaBrowser.Server.Implementations.IO
             await ProcessPathChanges(paths).ConfigureAwait(false);
             await ProcessPathChanges(paths).ConfigureAwait(false);
         }
         }
 
 
-        /// <summary>
-        /// Try and determine if a file is locked
-        /// This is not perfect, and is subject to race conditions, so I'd rather not make this a re-usable library method.
-        /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns><c>true</c> if [is file locked] [the specified path]; otherwise, <c>false</c>.</returns>
-        private bool IsFileLocked(string path)
+        private void DisposeTimer()
         {
         {
-            try
-            {
-                var data = _fileSystem.GetFileSystemInfo(path);
-
-                if (!data.Exists
-                    || data.Attributes.HasFlag(FileAttributes.Directory)
-                    || data.Attributes.HasFlag(FileAttributes.ReadOnly))
-                {
-                    return false;
-                }
-            }
-            catch (IOException)
-            {
-                return false;
-            }
-
-            try
+            lock (_timerLock)
             {
             {
-                using (_fileSystem.GetFileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
+                if (_updateTimer != null)
                 {
                 {
-                    //file is not locked
-                    return false;
+                    _updateTimer.Dispose();
+                    _updateTimer = null;
                 }
                 }
             }
             }
-            catch (DirectoryNotFoundException)
-            {
-                return false;
-            }
-            catch (FileNotFoundException)
-            {
-                return false;
-            }
-            catch (IOException)
-            {
-                //the file is unavailable because it is:
-                //still being written to
-                //or being processed by another thread
-                //or does not exist (has already been processed)
-                Logger.Debug("{0} is locked.", path);
-                return true;
-            }
-            catch
-            {
-                return false;
-            }
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -599,14 +548,7 @@ namespace MediaBrowser.Server.Implementations.IO
                 watcher.Dispose();
                 watcher.Dispose();
             }
             }
 
 
-            lock (_timerLock)
-            {
-                if (_updateTimer != null)
-                {
-                    _updateTimer.Dispose();
-                    _updateTimer = null;
-                }
-            }
+            DisposeTimer();
 
 
             _fileSystemWatchers.Clear();
             _fileSystemWatchers.Clear();
             _affectedPaths.Clear();
             _affectedPaths.Clear();

+ 8 - 8
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -137,7 +137,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
 
         private IEnumerable<IMetadataSaver> _savers;
         private IEnumerable<IMetadataSaver> _savers;
 
 
-        private readonly Func<IDirectoryWatchers> _directoryWatchersFactory;
+        private readonly Func<ILibraryMonitor> _libraryMonitorFactory;
 
 
         /// <summary>
         /// <summary>
         /// The _library items cache
         /// The _library items cache
@@ -180,14 +180,14 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <param name="userManager">The user manager.</param>
         /// <param name="userManager">The user manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="userDataRepository">The user data repository.</param>
         /// <param name="userDataRepository">The user data repository.</param>
-        public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<IDirectoryWatchers> directoryWatchersFactory, IFileSystem fileSystem)
+        public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem)
         {
         {
             _logger = logger;
             _logger = logger;
             _taskManager = taskManager;
             _taskManager = taskManager;
             _userManager = userManager;
             _userManager = userManager;
             ConfigurationManager = configurationManager;
             ConfigurationManager = configurationManager;
             _userDataRepository = userDataRepository;
             _userDataRepository = userDataRepository;
-            _directoryWatchersFactory = directoryWatchersFactory;
+            _libraryMonitorFactory = libraryMonitorFactory;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             ByReferenceItems = new ConcurrentDictionary<Guid, BaseItem>();
             ByReferenceItems = new ConcurrentDictionary<Guid, BaseItem>();
 
 
@@ -934,7 +934,7 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
         public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
         {
         {
-            _directoryWatchersFactory().Stop();
+            _libraryMonitorFactory().Stop();
 
 
             try
             try
             {
             {
@@ -942,7 +942,7 @@ namespace MediaBrowser.Server.Implementations.Library
             }
             }
             finally
             finally
             {
             {
-                _directoryWatchersFactory().Start();
+                _libraryMonitorFactory().Start();
             }
             }
         }
         }
 
 
@@ -1462,13 +1462,13 @@ namespace MediaBrowser.Server.Implementations.Library
 
 
                 var semaphore = _fileLocks.GetOrAdd(path, key => new SemaphoreSlim(1, 1));
                 var semaphore = _fileLocks.GetOrAdd(path, key => new SemaphoreSlim(1, 1));
 
 
-                var directoryWatchers = _directoryWatchersFactory();
+                var libraryMonitor = _libraryMonitorFactory();
 
 
                 await semaphore.WaitAsync().ConfigureAwait(false);
                 await semaphore.WaitAsync().ConfigureAwait(false);
 
 
                 try
                 try
                 {
                 {
-                    directoryWatchers.TemporarilyIgnore(path);
+                    libraryMonitor.ReportFileSystemChangeBeginning(path);
                     saver.Save(item, CancellationToken.None);
                     saver.Save(item, CancellationToken.None);
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
@@ -1477,7 +1477,7 @@ namespace MediaBrowser.Server.Implementations.Library
                 }
                 }
                 finally
                 finally
                 {
                 {
-                    directoryWatchers.RemoveTempIgnore(path);
+                    libraryMonitor.ReportFileSystemChangeComplete(path, false);
                     semaphore.Release();
                     semaphore.Release();
                 }
                 }
             }
             }

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

@@ -46,7 +46,7 @@ namespace MediaBrowser.Server.Implementations.Library
             }
             }
 
 
             // Make sure the item has a name
             // Make sure the item has a name
-            EnsureName(item);
+            EnsureName(item, args);
 
 
             item.DontFetchMeta = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
             item.DontFetchMeta = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
                 item.Parents.Any(i => i.DontFetchMeta);
                 item.Parents.Any(i => i.DontFetchMeta);
@@ -59,13 +59,13 @@ namespace MediaBrowser.Server.Implementations.Library
         /// Ensures the name.
         /// Ensures the name.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <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 the subclass didn't supply a name, add it here
             if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
             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
                 //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.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -192,7 +193,11 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         public Task RefreshUsersMetadata(CancellationToken cancellationToken, bool force = false)
         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);
             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.
         /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
         /// </summary>
         /// </summary>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="libraryManager">The library manager.</param>
-        /// <param name="userManager">The user manager.</param>
         public GenresPostScanTask(ILibraryManager libraryManager)
         public GenresPostScanTask(ILibraryManager libraryManager)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;

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

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -88,7 +89,14 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
 
 
                     var itemByName = _libraryManager.GetPerson(name);
                     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)
                     foreach (var libraryId in counts.Keys)
                     {
                     {

+ 43 - 86
MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs

@@ -1,154 +1,111 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Net;
 using System;
 using System;
-using System.IO;
+using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
-using System.Net;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Server.Implementations.LiveTv
 namespace MediaBrowser.Server.Implementations.LiveTv
 {
 {
-    public class ChannelImageProvider : BaseMetadataProvider
+    public class ChannelImageProvider : IDynamicImageProvider, IHasChangeMonitor
     {
     {
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILiveTvManager _liveTvManager;
-        private readonly IProviderManager _providerManager;
-        private readonly IFileSystem _fileSystem;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
+        private readonly ILogger _logger;
 
 
-        public ChannelImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem, IHttpClient httpClient)
-            : base(logManager, configurationManager)
+        public ChannelImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger)
         {
         {
             _liveTvManager = liveTvManager;
             _liveTvManager = liveTvManager;
-            _providerManager = providerManager;
-            _fileSystem = fileSystem;
             _httpClient = httpClient;
             _httpClient = httpClient;
+            _logger = logger;
         }
         }
 
 
-        public override bool Supports(BaseItem item)
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
         {
         {
-            return item is LiveTvChannel;
-        }
-
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            return !item.HasImage(ImageType.Primary);
+            return new[] { ImageType.Primary };
         }
         }
 
 
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
+        public async Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken)
         {
         {
-            if (item.HasImage(ImageType.Primary))
-            {
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-                return true;
-            }
-
-            var changed = true;
-
-            try
-            {
-                changed = await DownloadImage((LiveTvChannel)item, cancellationToken).ConfigureAwait(false);
-            }
-            catch (HttpException ex)
-            {
-                // Don't fail the provider on a 404
-                if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
-                {
-                    throw;
-                }
-            }
-
-            if (changed)
-            {
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            }
-
-            return changed;
-        }
+            var liveTvItem = (LiveTvChannel)item;
 
 
-        private async Task<bool> DownloadImage(LiveTvChannel item, CancellationToken cancellationToken)
-        {
-            Stream imageStream = null;
-            string contentType = null;
+            var imageResponse = new DynamicImageResponse();
 
 
-            if (!string.IsNullOrEmpty(item.ProviderImagePath))
+            if (!string.IsNullOrEmpty(liveTvItem.ProviderImagePath))
             {
             {
-                contentType = "image/" + Path.GetExtension(item.ProviderImagePath).ToLower();
-                imageStream = _fileSystem.GetFileStream(item.ProviderImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true);
+                imageResponse.Path = liveTvItem.ProviderImagePath;
+                imageResponse.HasImage = true;
             }
             }
-            else if (!string.IsNullOrEmpty(item.ProviderImageUrl))
+            else if (!string.IsNullOrEmpty(liveTvItem.ProviderImageUrl))
             {
             {
                 var options = new HttpRequestOptions
                 var options = new HttpRequestOptions
                 {
                 {
                     CancellationToken = cancellationToken,
                     CancellationToken = cancellationToken,
-                    Url = item.ProviderImageUrl
+                    Url = liveTvItem.ProviderImageUrl
                 };
                 };
 
 
                 var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
                 var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
 
 
-                if (!response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+                if (response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    Logger.Error("Provider did not return an image content type.");
-                    return false;
+                    imageResponse.HasImage = true;
+                    imageResponse.Stream = response.Content;
+                    imageResponse.SetFormatFromMimeType(response.ContentType);
+                }
+                else
+                {
+                    _logger.Error("Provider did not return an image content type.");
                 }
                 }
-
-                imageStream = response.Content;
-                contentType = response.ContentType;
             }
             }
-            else if (item.HasProviderImage ?? true)
+            else if (liveTvItem.HasProviderImage ?? true)
             {
             {
-                var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase));
+                var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase));
 
 
                 if (service != null)
                 if (service != null)
                 {
                 {
                     try
                     try
                     {
                     {
-                        var response = await service.GetChannelImageAsync(item.ExternalId, cancellationToken).ConfigureAwait(false);
+                        var response = await service.GetChannelImageAsync(liveTvItem.ExternalId, cancellationToken).ConfigureAwait(false);
 
 
                         if (response != null)
                         if (response != null)
                         {
                         {
-                            imageStream = response.Stream;
-                            contentType = response.MimeType;
+                            imageResponse.HasImage = true;
+                            imageResponse.Stream = response.Stream;
+                            imageResponse.Format = response.Format;
                         }
                         }
                     }
                     }
                     catch (NotImplementedException)
                     catch (NotImplementedException)
                     {
                     {
-                        return false;
                     }
                     }
                 }
                 }
             }
             }
 
 
-            if (imageStream != null)
-            {
-                // Dummy up the original url
-                var url = item.ServiceName + item.ExternalId;
+            return imageResponse;
+        }
 
 
-                await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false);
-                return true;
-            }
+        public string Name
+        {
+            get { return "Live TV Service Provider"; }
+        }
 
 
-            return false;
+        public bool Supports(IHasImages item)
+        {
+            return item is LiveTvChannel;
         }
         }
 
 
-        public override MetadataProviderPriority Priority
+        public int Order
         {
         {
-            get { return MetadataProviderPriority.Second; }
+            get { return 0; }
         }
         }
 
 
-        public override ItemUpdateType ItemUpdateType
+        public bool HasChanged(IHasMetadata item, DateTime date)
         {
         {
-            get
-            {
-                return ItemUpdateType.ImageUpdate;
-            }
+            return !item.HasImage(ImageType.Primary) && (DateTime.UtcNow - date).TotalDays >= 1;
         }
         }
     }
     }
 }
 }

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

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

+ 43 - 86
MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs

@@ -1,154 +1,111 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Net;
 using System;
 using System;
-using System.IO;
+using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
-using System.Net;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
 namespace MediaBrowser.Server.Implementations.LiveTv
 namespace MediaBrowser.Server.Implementations.LiveTv
 {
 {
-    public class ProgramImageProvider : BaseMetadataProvider
+    public class ProgramImageProvider : IDynamicImageProvider, IHasChangeMonitor
     {
     {
         private readonly ILiveTvManager _liveTvManager;
         private readonly ILiveTvManager _liveTvManager;
-        private readonly IProviderManager _providerManager;
-        private readonly IFileSystem _fileSystem;
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
+        private readonly ILogger _logger;
 
 
-        public ProgramImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, ILiveTvManager liveTvManager, IProviderManager providerManager, IFileSystem fileSystem, IHttpClient httpClient)
-            : base(logManager, configurationManager)
+        public ProgramImageProvider(ILiveTvManager liveTvManager, IHttpClient httpClient, ILogger logger)
         {
         {
             _liveTvManager = liveTvManager;
             _liveTvManager = liveTvManager;
-            _providerManager = providerManager;
-            _fileSystem = fileSystem;
             _httpClient = httpClient;
             _httpClient = httpClient;
+            _logger = logger;
         }
         }
 
 
-        public override bool Supports(BaseItem item)
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
         {
         {
-            return item is LiveTvProgram;
-        }
-
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            return !item.HasImage(ImageType.Primary);
+            return new[] { ImageType.Primary };
         }
         }
 
 
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
+        public async Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken)
         {
         {
-            if (item.HasImage(ImageType.Primary))
-            {
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-                return true;
-            }
-
-            var changed = true;
-
-            try
-            {
-                changed = await DownloadImage((LiveTvProgram)item, cancellationToken).ConfigureAwait(false);
-            }
-            catch (HttpException ex)
-            {
-                // Don't fail the provider on a 404
-                if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
-                {
-                    throw;
-                }
-            }
-
-            if (changed)
-            {
-                SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            }
-
-            return changed;
-        }
+            var liveTvItem = (LiveTvProgram)item;
 
 
-        private async Task<bool> DownloadImage(LiveTvProgram item, CancellationToken cancellationToken)
-        {
-            Stream imageStream = null;
-            string contentType = null;
+            var imageResponse = new DynamicImageResponse();
 
 
-            if (!string.IsNullOrEmpty(item.ProviderImagePath))
+            if (!string.IsNullOrEmpty(liveTvItem.ProviderImagePath))
             {
             {
-                contentType = "image/" + Path.GetExtension(item.ProviderImagePath).ToLower();
-                imageStream = _fileSystem.GetFileStream(item.ProviderImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, true);
+                imageResponse.Path = liveTvItem.ProviderImagePath;
+                imageResponse.HasImage = true;
             }
             }
-            else if (!string.IsNullOrEmpty(item.ProviderImageUrl))
+            else if (!string.IsNullOrEmpty(liveTvItem.ProviderImageUrl))
             {
             {
                 var options = new HttpRequestOptions
                 var options = new HttpRequestOptions
                 {
                 {
                     CancellationToken = cancellationToken,
                     CancellationToken = cancellationToken,
-                    Url = item.ProviderImageUrl
+                    Url = liveTvItem.ProviderImageUrl
                 };
                 };
 
 
                 var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
                 var response = await _httpClient.GetResponse(options).ConfigureAwait(false);
 
 
-                if (!response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
+                if (response.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    Logger.Error("Provider did not return an image content type.");
-                    return false;
+                    imageResponse.HasImage = true;
+                    imageResponse.Stream = response.Content;
+                    imageResponse.SetFormatFromMimeType(response.ContentType);
+                }
+                else
+                {
+                    _logger.Error("Provider did not return an image content type.");
                 }
                 }
-
-                imageStream = response.Content;
-                contentType = response.ContentType;
             }
             }
-            else if (item.HasProviderImage ?? true)
+            else if (liveTvItem.HasProviderImage ?? true)
             {
             {
-                var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, item.ServiceName, StringComparison.OrdinalIgnoreCase));
+                var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase));
 
 
                 if (service != null)
                 if (service != null)
                 {
                 {
                     try
                     try
                     {
                     {
-                        var response = await service.GetProgramImageAsync(item.ExternalId, item.ExternalChannelId, cancellationToken).ConfigureAwait(false);
+                        var response = await service.GetProgramImageAsync(liveTvItem.ExternalId, liveTvItem.ExternalChannelId, cancellationToken).ConfigureAwait(false);
 
 
                         if (response != null)
                         if (response != null)
                         {
                         {
-                            imageStream = response.Stream;
-                            contentType = response.MimeType;
+                            imageResponse.HasImage = true;
+                            imageResponse.Stream = response.Stream;
+                            imageResponse.Format = response.Format;
                         }
                         }
                     }
                     }
                     catch (NotImplementedException)
                     catch (NotImplementedException)
                     {
                     {
-                        return false;
                     }
                     }
                 }
                 }
             }
             }
 
 
-            if (imageStream != null)
-            {
-                // Dummy up the original url
-                var url = item.ServiceName + item.ExternalId;
+            return imageResponse;
+        }
 
 
-                await _providerManager.SaveImage(item, imageStream, contentType, ImageType.Primary, null, url, cancellationToken).ConfigureAwait(false);
-                return true;
-            }
+        public string Name
+        {
+            get { return "Live TV Service Provider"; }
+        }
 
 
-            return false;
+        public bool Supports(IHasImages item)
+        {
+            return item is LiveTvProgram;
         }
         }
 
 
-        public override MetadataProviderPriority Priority
+        public int Order
         {
         {
-            get { return MetadataProviderPriority.Second; }
+            get { return 0; }
         }
         }
 
 
-        public override ItemUpdateType ItemUpdateType
+        public bool HasChanged(IHasMetadata item, DateTime date)
         {
         {
-            get
-            {
-                return ItemUpdateType.ImageUpdate;
-            }
+            return !item.HasImage(ImageType.Primary) && (DateTime.UtcNow - date).TotalHours >= 12;
         }
         }
     }
     }
 }
 }

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

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

+ 1 - 3
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -137,7 +137,7 @@
     <Compile Include="HttpServer\StreamWriter.cs" />
     <Compile Include="HttpServer\StreamWriter.cs" />
     <Compile Include="HttpServer\SwaggerService.cs" />
     <Compile Include="HttpServer\SwaggerService.cs" />
     <Compile Include="Drawing\ImageProcessor.cs" />
     <Compile Include="Drawing\ImageProcessor.cs" />
-    <Compile Include="IO\DirectoryWatchers.cs" />
+    <Compile Include="IO\LibraryMonitor.cs" />
     <Compile Include="Library\CoreResolutionIgnoreRule.cs" />
     <Compile Include="Library\CoreResolutionIgnoreRule.cs" />
     <Compile Include="Library\LibraryManager.cs" />
     <Compile Include="Library\LibraryManager.cs" />
     <Compile Include="Library\SearchEngine.cs" />
     <Compile Include="Library\SearchEngine.cs" />
@@ -189,8 +189,6 @@
     <Compile Include="Persistence\SqliteShrinkMemoryTimer.cs" />
     <Compile Include="Persistence\SqliteShrinkMemoryTimer.cs" />
     <Compile Include="Persistence\TypeMapper.cs" />
     <Compile Include="Persistence\TypeMapper.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="Providers\ImageSaver.cs" />
-    <Compile Include="Providers\ProviderManager.cs" />
     <Compile Include="Roku\RokuControllerFactory.cs" />
     <Compile Include="Roku\RokuControllerFactory.cs" />
     <Compile Include="ScheduledTasks\PeopleValidationTask.cs" />
     <Compile Include="ScheduledTasks\PeopleValidationTask.cs" />
     <Compile Include="ScheduledTasks\ChapterImagesTask.cs" />
     <Compile Include="ScheduledTasks\ChapterImagesTask.cs" />

Some files were not shown because too many files changed in this diff