فهرست منبع

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

Sven Van den brande 11 سال پیش
والد
کامیت
96d4b9c43b
45فایلهای تغییر یافته به همراه1649 افزوده شده و 488 حذف شده
  1. 1 29
      MediaBrowser.Api/LibraryService.cs
  2. 1 0
      MediaBrowser.Api/MediaBrowser.Api.csproj
  3. 8 7
      MediaBrowser.Api/Playback/Hls/VideoHlsService.cs
  4. 16 15
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  5. 132 0
      MediaBrowser.Api/RemoteImageService.cs
  6. 3 3
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  7. 17 4
      MediaBrowser.Controller/Providers/IImageProvider.cs
  8. 10 2
      MediaBrowser.Controller/Providers/IProviderManager.cs
  9. 3 0
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  10. 3 0
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  11. 4 1
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  12. 6 0
      MediaBrowser.Model/Dto/IItemDto.cs
  13. 1 0
      MediaBrowser.Model/MediaBrowser.Model.csproj
  14. 8 1
      MediaBrowser.Model/Providers/RemoteImageInfo.cs
  15. 28 0
      MediaBrowser.Model/Providers/RemoteImageResult.cs
  16. 4 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  17. 59 138
      MediaBrowser.Providers/Movies/FanArtMovieProvider.cs
  18. 5 14
      MediaBrowser.Providers/Movies/FanArtMovieUpdatesPrescanTask.cs
  19. 301 0
      MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs
  20. 24 14
      MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs
  21. 5 18
      MediaBrowser.Providers/Movies/MovieDbImagesProvider.cs
  22. 25 16
      MediaBrowser.Providers/Movies/MovieDbProvider.cs
  23. 1 3
      MediaBrowser.Providers/Movies/MovieUpdatesPrescanTask.cs
  24. 2 2
      MediaBrowser.Providers/Savers/XmlSaverHelpers.cs
  25. 15 51
      MediaBrowser.Providers/TV/FanArtSeasonProvider.cs
  26. 57 114
      MediaBrowser.Providers/TV/FanArtTVProvider.cs
  27. 5 15
      MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs
  28. 251 0
      MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs
  29. 309 0
      MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs
  30. 166 0
      MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs
  31. 4 1
      MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs
  32. 1 1
      MediaBrowser.Providers/TV/RemoteSeasonProvider.cs
  33. 27 1
      MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs
  34. 3 1
      MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs
  35. 1 1
      MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs
  36. 4 1
      MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs
  37. 2 2
      MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs
  38. 1 1
      MediaBrowser.Server.Implementations/Persistence/SqliteNotificationsRepository.cs
  39. 1 1
      MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs
  40. 1 1
      MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs
  41. 27 8
      MediaBrowser.Server.Implementations/Providers/ImageSaver.cs
  42. 40 13
      MediaBrowser.Server.Implementations/Providers/ProviderManager.cs
  43. 8 1
      MediaBrowser.Server.Implementations/Sorting/PremiereDateComparer.cs
  44. 58 7
      MediaBrowser.WebDashboard/ApiClient.js
  45. 1 1
      MediaBrowser.WebDashboard/packages.config

+ 1 - 29
MediaBrowser.Api/LibraryService.cs

@@ -5,10 +5,8 @@ using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using ServiceStack.ServiceHost;
 using ServiceStack.ServiceHost;
 using System;
 using System;
@@ -34,21 +32,6 @@ namespace MediaBrowser.Api
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
 
 
-    [Route("/Items/{Id}/RemoteImages/{Type}", "GET")]
-    [Api(Description = "Gets available remote images for an item")]
-    public class GetRemoteImages : IReturn<List<RemoteImageInfo>>
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public ImageType Type { get; set; }
-    }
-    
     /// <summary>
     /// <summary>
     /// Class GetCriticReviews
     /// Class GetCriticReviews
     /// </summary>
     /// </summary>
@@ -225,7 +208,6 @@ namespace MediaBrowser.Api
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
-        private readonly IProviderManager _providerManager;
 
 
         private readonly IDtoService _dtoService;
         private readonly IDtoService _dtoService;
 
 
@@ -233,14 +215,13 @@ namespace MediaBrowser.Api
         /// Initializes a new instance of the <see cref="LibraryService" /> class.
         /// Initializes a new instance of the <see cref="LibraryService" /> class.
         /// </summary>
         /// </summary>
         public LibraryService(IItemRepository itemRepo, ILibraryManager libraryManager, IUserManager userManager,
         public LibraryService(IItemRepository itemRepo, ILibraryManager libraryManager, IUserManager userManager,
-                              IDtoService dtoService, IUserDataManager userDataManager, IProviderManager providerManager)
+                              IDtoService dtoService, IUserDataManager userDataManager)
         {
         {
             _itemRepo = itemRepo;
             _itemRepo = itemRepo;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _userManager = userManager;
             _dtoService = dtoService;
             _dtoService = dtoService;
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
-            _providerManager = providerManager;
         }
         }
 
 
         public object Get(GetFile request)
         public object Get(GetFile request)
@@ -259,15 +240,6 @@ namespace MediaBrowser.Api
             return ToStaticFileResult(item.Path);
             return ToStaticFileResult(item.Path);
         }
         }
 
 
-        public object Get(GetRemoteImages request)
-        {
-            var item = _dtoService.GetItemByDtoId(request.Id);
-
-            var result = _providerManager.GetAvailableRemoteImages(item, request.Type, CancellationToken.None).Result;
-
-            return ToOptimizedResult(result);
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the specified request.
         /// Gets the specified request.
         /// </summary>
         /// </summary>

+ 1 - 0
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -100,6 +100,7 @@
     <Compile Include="Playback\StreamState.cs" />
     <Compile Include="Playback\StreamState.cs" />
     <Compile Include="Playback\Progressive\VideoService.cs" />
     <Compile Include="Playback\Progressive\VideoService.cs" />
     <Compile Include="PluginService.cs" />
     <Compile Include="PluginService.cs" />
+    <Compile Include="RemoteImageService.cs" />
     <Compile Include="ScheduledTasks\ScheduledTaskService.cs" />
     <Compile Include="ScheduledTasks\ScheduledTaskService.cs" />
     <Compile Include="ScheduledTasks\ScheduledTasksWebSocketListener.cs" />
     <Compile Include="ScheduledTasks\ScheduledTasksWebSocketListener.cs" />
     <Compile Include="ApiEntryPoint.cs" />
     <Compile Include="ApiEntryPoint.cs" />

+ 8 - 7
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -1,4 +1,4 @@
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
@@ -80,11 +80,6 @@ namespace MediaBrowser.Api.Playback.Hls
                     args += " -ac " + channels.Value;
                     args += " -ac " + channels.Value;
                 }
                 }
 
 
-                if (state.Request.AudioSampleRate.HasValue)
-                {
-                    args += " -ar " + state.Request.AudioSampleRate.Value;
-                }
-
                 var bitrate = GetAudioBitrateParam(state);
                 var bitrate = GetAudioBitrateParam(state);
 
 
                 if (bitrate.HasValue)
                 if (bitrate.HasValue)
@@ -93,14 +88,20 @@ namespace MediaBrowser.Api.Playback.Hls
                 }
                 }
 
 
                 var volParam = string.Empty;
                 var volParam = string.Empty;
+                var AudioSampleRate = string.Empty;
 
 
                 // Boost volume to 200% when downsampling from 6ch to 2ch
                 // Boost volume to 200% when downsampling from 6ch to 2ch
                 if (channels.HasValue && channels.Value <= 2 && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
                 if (channels.HasValue && channels.Value <= 2 && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
                 {
                 {
                     volParam = ",volume=2.000000";
                     volParam = ",volume=2.000000";
                 }
                 }
+                
+                if (state.Request.AudioSampleRate.HasValue)
+                {
+                    AudioSampleRate= state.Request.AudioSampleRate.Value + ":";
+                }
 
 
-                args += string.Format(" -af \"adelay=1,aresample=async=1000{0}\"", volParam);
+                args += string.Format(" -af \"adelay=1,aresample={0}async=1000{1}\"",AudioSampleRate, volParam);
 
 
                 return args;
                 return args;
             }
             }

+ 16 - 15
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -1,4 +1,4 @@
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
@@ -220,11 +220,6 @@ namespace MediaBrowser.Api.Playback.Progressive
                 args += " -ac " + channels.Value;
                 args += " -ac " + channels.Value;
             }
             }
 
 
-            if (request.AudioSampleRate.HasValue)
-            {
-                args += " -ar " + request.AudioSampleRate.Value;
-            }
-
             var bitrate = GetAudioBitrateParam(state);
             var bitrate = GetAudioBitrateParam(state);
 
 
             if (bitrate.HasValue)
             if (bitrate.HasValue)
@@ -232,18 +227,24 @@ namespace MediaBrowser.Api.Playback.Progressive
                 args += " -ab " + bitrate.Value.ToString(UsCulture);
                 args += " -ab " + bitrate.Value.ToString(UsCulture);
             }
             }
 
 
-            var volParam = string.Empty;
+                var volParam = string.Empty;
+                var AudioSampleRate = string.Empty;
 
 
-            // Boost volume to 200% when downsampling from 6ch to 2ch
-            if (channels.HasValue && channels.Value <= 2 && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
-            {
-                volParam = ",volume=2.000000";
-            }
+                // Boost volume to 200% when downsampling from 6ch to 2ch
+                if (channels.HasValue && channels.Value <= 2 && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5)
+                {
+                    volParam = ",volume=2.000000";
+                }
+                
+                if (state.Request.AudioSampleRate.HasValue)
+                {
+                    AudioSampleRate= state.Request.AudioSampleRate.Value + ":";
+                }
 
 
-            args += string.Format(" -af \"aresample=async=1000{0}\"", volParam);
+                args += string.Format(" -af \"aresample={0}async=1000{1}\"",AudioSampleRate, volParam);
 
 
-            return args;
-        }
+                return args;
+            }
 
 
         /// <summary>
         /// <summary>
         /// Gets the video bitrate to specify on the command line
         /// Gets the video bitrate to specify on the command line

+ 132 - 0
MediaBrowser.Api/RemoteImageService.cs

@@ -0,0 +1,132 @@
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using ServiceStack.ServiceHost;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api
+{
+    [Route("/Items/{Id}/RemoteImages", "GET")]
+    [Api(Description = "Gets available remote images for an item")]
+    public class GetRemoteImages : IReturn<RemoteImageResult>
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public ImageType? Type { get; set; }
+
+        /// <summary>
+        /// Skips over a given number of items within the results. Use for paging.
+        /// </summary>
+        /// <value>The start index.</value>
+        [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? StartIndex { get; set; }
+
+        /// <summary>
+        /// The maximum number of items to return
+        /// </summary>
+        /// <value>The limit.</value>
+        [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? Limit { get; set; }
+
+        [ApiMember(Name = "ProviderName", Description = "Optional. The image provider to use", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string ProviderName { get; set; }
+    }
+
+    [Route("/Items/{Id}/RemoteImages/Download", "POST")]
+    [Api(Description = "Downloads a remote image for an item")]
+    public class DownloadRemoteImage : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public ImageType Type { get; set; }
+
+        [ApiMember(Name = "ProviderName", Description = "The image provider", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string ProviderName { get; set; }
+
+        [ApiMember(Name = "ImageUrl", Description = "The image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public string ImageUrl { get; set; }
+    }
+
+    public class RemoteImageService : BaseApiService
+    {
+        private readonly IProviderManager _providerManager;
+
+        private readonly IDtoService _dtoService;
+
+        public RemoteImageService(IProviderManager providerManager, IDtoService dtoService)
+        {
+            _providerManager = providerManager;
+            _dtoService = dtoService;
+        }
+
+        public object Get(GetRemoteImages request)
+        {
+            var item = _dtoService.GetItemByDtoId(request.Id);
+
+            var images = _providerManager.GetAvailableRemoteImages(item, CancellationToken.None, request.ProviderName, request.Type).Result;
+
+            var imagesList = images.ToList();
+
+            var result = new RemoteImageResult
+            {
+                TotalRecordCount = imagesList.Count,
+                Providers = _providerManager.GetImageProviders(item).Select(i => i.Name).ToList()
+            };
+
+            if (request.StartIndex.HasValue)
+            {
+                imagesList = imagesList.Skip(request.StartIndex.Value)
+                    .ToList();
+            }
+
+            if (request.Limit.HasValue)
+            {
+                imagesList = imagesList.Take(request.Limit.Value)
+                    .ToList();
+            }
+
+            result.Images = imagesList;
+
+            return ToOptimizedResult(result);
+        }
+
+        public void Post(DownloadRemoteImage request)
+        {
+            var task = DownloadRemoteImage(request);
+
+            Task.WaitAll(task);
+        }
+
+        private async Task DownloadRemoteImage(DownloadRemoteImage request)
+        {
+            var item = _dtoService.GetItemByDtoId(request.Id);
+
+            int? index = null;
+
+            if (request.Type == ImageType.Backdrop)
+            {
+                index = item.BackdropImagePaths.Count;
+            }
+
+            await _providerManager.SaveImage(item, request.ImageUrl, null, request.Type, index, CancellationToken.None).ConfigureAwait(false);
+
+            await item.RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false)
+                    .ConfigureAwait(false);
+        }
+    }
+}

+ 3 - 3
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -492,12 +492,12 @@ namespace MediaBrowser.Api.UserLibrary
 
 
         private IEnumerable<BaseItem> FilterVirtualSeasons(GetItems request, IEnumerable<BaseItem> items, User user)
         private IEnumerable<BaseItem> FilterVirtualSeasons(GetItems request, IEnumerable<BaseItem> items, User user)
         {
         {
-            if (request.IsMissing.HasValue && request.IsUnaired.HasValue)
+            if (request.IsMissing.HasValue && request.IsVirtualUnaired.HasValue)
             {
             {
                 var isMissing = request.IsMissing.Value;
                 var isMissing = request.IsMissing.Value;
-                var isUnaired = request.IsUnaired.Value;
+                var isVirtualUnaired = request.IsVirtualUnaired.Value;
 
 
-                if (!isMissing && !isUnaired)
+                if (!isMissing && !isVirtualUnaired)
                 {
                 {
                     return items.Where(i =>
                     return items.Where(i =>
                     {
                     {

+ 17 - 4
MediaBrowser.Controller/Providers/IImageProvider.cs

@@ -22,17 +22,30 @@ namespace MediaBrowser.Controller.Providers
         /// Supportses the specified item.
         /// Supportses the specified item.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <param name="imageType">Type of the image.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        bool Supports(BaseItem item, ImageType imageType);
+        bool Supports(BaseItem item);
 
 
         /// <summary>
         /// <summary>
-        /// Gets the available images.
+        /// Gets the images.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="imageType">Type of the image.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        Task<IEnumerable<RemoteImageInfo>> GetAvailableImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken);
+        Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem 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(BaseItem item, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the priority.
+        /// </summary>
+        /// <value>The priority.</value>
+        int Priority { get; }
     }
     }
 }
 }

+ 10 - 2
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -60,9 +60,17 @@ namespace MediaBrowser.Controller.Providers
         /// Gets the available remote images.
         /// Gets the available remote images.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <param name="type">The type.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="providerName">Name of the provider.</param>
+        /// <param name="type">The type.</param>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, ImageType type, CancellationToken cancellationToken);
+        Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, CancellationToken cancellationToken, string providerName = null, ImageType? type = null);
+
+        /// <summary>
+        /// Gets the image providers.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>IEnumerable{IImageProvider}.</returns>
+        IEnumerable<IImageProvider> GetImageProviders(BaseItem item);
     }
     }
 }
 }

+ 3 - 0
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -299,6 +299,9 @@
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteImageInfo.cs">
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteImageInfo.cs">
       <Link>Providers\RemoteImageInfo.cs</Link>
       <Link>Providers\RemoteImageInfo.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Providers\RemoteImageResult.cs">
+      <Link>Providers\RemoteImageResult.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
       <Link>Querying\ArtistsQuery.cs</Link>
       <Link>Querying\ArtistsQuery.cs</Link>
     </Compile>
     </Compile>

+ 3 - 0
MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj

@@ -286,6 +286,9 @@
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteImageInfo.cs">
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteImageInfo.cs">
       <Link>Providers\RemoteImageInfo.cs</Link>
       <Link>Providers\RemoteImageInfo.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Providers\RemoteImageResult.cs">
+      <Link>Providers\RemoteImageResult.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
       <Link>Querying\ArtistsQuery.cs</Link>
       <Link>Querying\ArtistsQuery.cs</Link>
     </Compile>
     </Compile>

+ 4 - 1
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -275,7 +275,10 @@ namespace MediaBrowser.Model.Configuration
             MetadataCountryCode = "US";
             MetadataCountryCode = "US";
             DownloadMovieImages = new ImageDownloadOptions();
             DownloadMovieImages = new ImageDownloadOptions();
             DownloadSeriesImages = new ImageDownloadOptions();
             DownloadSeriesImages = new ImageDownloadOptions();
-            DownloadSeasonImages = new ImageDownloadOptions();
+            DownloadSeasonImages = new ImageDownloadOptions
+            {
+                Backdrops = false
+            };
             DownloadMusicArtistImages = new ImageDownloadOptions();
             DownloadMusicArtistImages = new ImageDownloadOptions();
             DownloadMusicAlbumImages = new ImageDownloadOptions();
             DownloadMusicAlbumImages = new ImageDownloadOptions();
             MaxBackdrops = 3;
             MaxBackdrops = 3;

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

@@ -18,4 +18,10 @@ namespace MediaBrowser.Model.Dto
         /// <value>The original primary image aspect ratio.</value>
         /// <value>The original primary image aspect ratio.</value>
         double? OriginalPrimaryImageAspectRatio { get; set; }
         double? OriginalPrimaryImageAspectRatio { get; set; }
     }
     }
+
+    public enum RatingType
+    {
+        Score,
+        Likes
+    }
 }
 }

+ 1 - 0
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -85,6 +85,7 @@
     <Compile Include="Notifications\NotificationQuery.cs" />
     <Compile Include="Notifications\NotificationQuery.cs" />
     <Compile Include="Notifications\NotificationResult.cs" />
     <Compile Include="Notifications\NotificationResult.cs" />
     <Compile Include="Notifications\NotificationsSummary.cs" />
     <Compile Include="Notifications\NotificationsSummary.cs" />
+    <Compile Include="Providers\RemoteImageResult.cs" />
     <Compile Include="Querying\ArtistsQuery.cs" />
     <Compile Include="Querying\ArtistsQuery.cs" />
     <Compile Include="Querying\ItemCountsQuery.cs" />
     <Compile Include="Querying\ItemCountsQuery.cs" />
     <Compile Include="Querying\ItemReviewsResult.cs" />
     <Compile Include="Querying\ItemReviewsResult.cs" />

+ 8 - 1
MediaBrowser.Model/Providers/RemoteImageInfo.cs

@@ -1,4 +1,4 @@
-
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
 namespace MediaBrowser.Model.Providers
 namespace MediaBrowser.Model.Providers
@@ -55,5 +55,12 @@ namespace MediaBrowser.Model.Providers
         /// </summary>
         /// </summary>
         /// <value>The type.</value>
         /// <value>The type.</value>
         public ImageType Type { get; set; }
         public ImageType Type { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the rating.
+        /// </summary>
+        /// <value>The type of the rating.</value>
+        public RatingType RatingType { get; set; }
     }
     }
+
 }
 }

+ 28 - 0
MediaBrowser.Model/Providers/RemoteImageResult.cs

@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Model.Providers
+{
+    /// <summary>
+    /// Class RemoteImageResult.
+    /// </summary>
+    public class RemoteImageResult
+    {
+        /// <summary>
+        /// Gets or sets the images.
+        /// </summary>
+        /// <value>The images.</value>
+        public List<RemoteImageInfo> Images { get; set; }
+
+        /// <summary>
+        /// Gets or sets the total record count.
+        /// </summary>
+        /// <value>The total record count.</value>
+        public int TotalRecordCount { get; set; }
+
+        /// <summary>
+        /// Gets or sets the providers.
+        /// </summary>
+        /// <value>The providers.</value>
+        public List<string> Providers { get; set; }
+    }
+}

+ 4 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -61,6 +61,7 @@
     <Compile Include="MediaInfo\VideoImageProvider.cs" />
     <Compile Include="MediaInfo\VideoImageProvider.cs" />
     <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\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" />
@@ -109,6 +110,9 @@
     <Compile Include="TV\FanArtSeasonProvider.cs" />
     <Compile Include="TV\FanArtSeasonProvider.cs" />
     <Compile Include="TV\FanArtTVProvider.cs" />
     <Compile Include="TV\FanArtTVProvider.cs" />
     <Compile Include="TV\FanArtTvUpdatesPrescanTask.cs" />
     <Compile Include="TV\FanArtTvUpdatesPrescanTask.cs" />
+    <Compile Include="TV\ManualFanartSeasonProvider.cs" />
+    <Compile Include="TV\ManualFanartSeriesProvider.cs" />
+    <Compile Include="TV\ManualTvdbEpisodeImageProvider.cs" />
     <Compile Include="TV\RemoteEpisodeProvider.cs" />
     <Compile Include="TV\RemoteEpisodeProvider.cs" />
     <Compile Include="TV\RemoteSeasonProvider.cs" />
     <Compile Include="TV\RemoteSeasonProvider.cs" />
     <Compile Include="TV\RemoteSeriesProvider.cs" />
     <Compile Include="TV\RemoteSeriesProvider.cs" />

+ 59 - 138
MediaBrowser.Providers/Movies/FanArtMovieProvider.cs

@@ -4,18 +4,17 @@ 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.Entities.Movies;
-using MediaBrowser.Controller.IO;
 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.Logging;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Providers;
 using System;
 using System;
-using System.Globalization;
+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 System.Xml;
 
 
 namespace MediaBrowser.Providers.Movies
 namespace MediaBrowser.Providers.Movies
 {
 {
@@ -35,11 +34,6 @@ namespace MediaBrowser.Providers.Movies
         /// </summary>
         /// </summary>
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
 
 
-        /// <summary>
-        /// The us culture
-        /// </summary>
-        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
         internal static FanArtMovieProvider Current { get; private set; }
         internal static FanArtMovieProvider Current { get; private set; }
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
 
 
@@ -139,28 +133,6 @@ namespace MediaBrowser.Providers.Movies
                 return false;
                 return false;
             }
             }
 
 
-            if (!ConfigurationManager.Configuration.DownloadMovieImages.Art &&
-                !ConfigurationManager.Configuration.DownloadMovieImages.Logo &&
-                !ConfigurationManager.Configuration.DownloadMovieImages.Disc &&
-                !ConfigurationManager.Configuration.DownloadMovieImages.Backdrops &&
-                !ConfigurationManager.Configuration.DownloadMovieImages.Banner &&
-                !ConfigurationManager.Configuration.DownloadMovieImages.Thumb &&
-                !ConfigurationManager.Configuration.DownloadMovieImages.Primary)
-            {
-                return false;
-            }
-
-            if (item.HasImage(ImageType.Primary) &&
-                item.HasImage(ImageType.Art) &&
-                item.HasImage(ImageType.Logo) &&
-                item.HasImage(ImageType.Disc) &&
-                item.HasImage(ImageType.Banner) &&
-                item.HasImage(ImageType.Thumb) &&
-                item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops)
-            {
-                return false;
-            }
-
             return base.NeedsRefreshInternal(item, providerInfo);
             return base.NeedsRefreshInternal(item, providerInfo);
         }
         }
 
 
@@ -171,24 +143,11 @@ namespace MediaBrowser.Providers.Movies
             if (!string.IsNullOrEmpty(id))
             if (!string.IsNullOrEmpty(id))
             {
             {
                 // Process images
                 // Process images
-                var path = GetMovieDataPath(ConfigurationManager.ApplicationPaths, id);
+                var xmlPath = GetFanartXmlPath(id);
 
 
-                try
-                {
-                    var files = new DirectoryInfo(path)
-                        .EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)
-                        .Select(i => _fileSystem.GetLastWriteTimeUtc(i))
-                        .ToList();
-
-                    if (files.Count > 0)
-                    {
-                        return files.Max() > providerInfo.LastRefreshed;
-                    }
-                }
-                catch (DirectoryNotFoundException)
-                {
-                    return true;
-                }
+                var fileInfo = new FileInfo(xmlPath);
+
+                return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed;
             }
             }
 
 
             return base.NeedsRefreshBasedOnCompareDate(item, providerInfo);
             return base.NeedsRefreshBasedOnCompareDate(item, providerInfo);
@@ -234,41 +193,44 @@ namespace MediaBrowser.Providers.Movies
 
 
             if (!string.IsNullOrEmpty(movieId))
             if (!string.IsNullOrEmpty(movieId))
             {
             {
-                var movieDataPath = GetMovieDataPath(ConfigurationManager.ApplicationPaths, movieId);
-                var xmlPath = Path.Combine(movieDataPath, "fanart.xml");
+                var xmlPath = GetFanartXmlPath(movieId);
 
 
                 // Only download the xml if it doesn't already exist. The prescan task will take care of getting updates
                 // Only download the xml if it doesn't already exist. The prescan task will take care of getting updates
                 if (!File.Exists(xmlPath))
                 if (!File.Exists(xmlPath))
                 {
                 {
-                    await DownloadMovieXml(movieDataPath, movieId, cancellationToken).ConfigureAwait(false);
+                    await DownloadMovieXml(movieId, cancellationToken).ConfigureAwait(false);
                 }
                 }
 
 
-                if (File.Exists(xmlPath))
-                {
-                    await FetchFromXml(item, xmlPath, cancellationToken).ConfigureAwait(false);
-                }
+                var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartMovieImageProvider.ProviderName).ConfigureAwait(false);
+
+                await FetchImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
             }
             }
 
 
             SetLastRefreshed(item, DateTime.UtcNow);
             SetLastRefreshed(item, DateTime.UtcNow);
             return true;
             return true;
         }
         }
 
 
+        public string GetFanartXmlPath(string tmdbId)
+        {
+            var movieDataPath = GetMovieDataPath(ConfigurationManager.ApplicationPaths, tmdbId);
+            return Path.Combine(movieDataPath, "fanart.xml");
+        }
+
         /// <summary>
         /// <summary>
         /// Downloads the movie XML.
         /// Downloads the movie XML.
         /// </summary>
         /// </summary>
-        /// <param name="movieDataPath">The movie data path.</param>
         /// <param name="tmdbId">The TMDB id.</param>
         /// <param name="tmdbId">The TMDB id.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        internal async Task DownloadMovieXml(string movieDataPath, string tmdbId, CancellationToken cancellationToken)
+        internal async Task DownloadMovieXml(string tmdbId, CancellationToken cancellationToken)
         {
         {
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
-            string url = string.Format(FanArtBaseUrl, ApiKey, tmdbId);
+            var url = string.Format(FanArtBaseUrl, ApiKey, tmdbId);
 
 
-            var xmlPath = Path.Combine(movieDataPath, "fanart.xml");
+            var xmlPath = GetFanartXmlPath(tmdbId);
 
 
-            Directory.CreateDirectory(movieDataPath);
+            Directory.CreateDirectory(Path.GetDirectoryName(xmlPath));
 
 
             using (var response = await HttpClient.Get(new HttpRequestOptions
             using (var response = await HttpClient.Get(new HttpRequestOptions
             {
             {
@@ -285,82 +247,53 @@ namespace MediaBrowser.Providers.Movies
             }
             }
         }
         }
 
 
-        /// <summary>
-        /// Fetches from XML.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="xmlFilePath">The XML file path.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        private async Task FetchFromXml(BaseItem item, string xmlFilePath, CancellationToken cancellationToken)
+        private async Task FetchImages(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
         {
         {
-            var doc = new XmlDocument();
-            doc.Load(xmlFilePath);
-
-            var language = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower();
-
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
-            string path;
-
             if (ConfigurationManager.Configuration.DownloadMovieImages.Primary && !item.HasImage(ImageType.Primary))
             if (ConfigurationManager.Configuration.DownloadMovieImages.Primary && !item.HasImage(ImageType.Primary))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/movie/movieposters/movieposter[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/movie/movieposters/movieposter/@url");
-                path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Primary);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Primary, null, cancellationToken)
-                                        .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
-            
+
             if (ConfigurationManager.Configuration.DownloadMovieImages.Logo && !item.HasImage(ImageType.Logo))
             if (ConfigurationManager.Configuration.DownloadMovieImages.Logo && !item.HasImage(ImageType.Logo))
             {
             {
-                var node =
-                    doc.SelectSingleNode("//fanart/movie/hdmovielogos/hdmovielogo[@lang = \"" + language + "\"]/@url") ??
-                    doc.SelectSingleNode("//fanart/movie/movielogos/movielogo[@lang = \"" + language + "\"]/@url");
-                if (node == null && language != "en")
-                {
-                    //maybe just couldn't find language - try just first one
-                    node = doc.SelectSingleNode("//fanart/movie/hdmovielogos/hdmovielogo/@url") ??
-                        doc.SelectSingleNode("//fanart/movie/movielogos/movielogo/@url");
-                }
-                path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Logo);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Logo, null, cancellationToken).ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Logo, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
+
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
             if (ConfigurationManager.Configuration.DownloadMovieImages.Art && !item.HasImage(ImageType.Art))
             if (ConfigurationManager.Configuration.DownloadMovieImages.Art && !item.HasImage(ImageType.Art))
             {
             {
-                var node =
-                    doc.SelectSingleNode("//fanart/movie/hdmoviecleararts/hdmovieclearart[@lang = \"" + language + "\"]/@url") ??
-                    doc.SelectSingleNode("//fanart/movie/hdmoviecleararts/hdmovieclearart/@url") ??
-                    doc.SelectSingleNode("//fanart/movie/moviearts/movieart[@lang = \"" + language + "\"]/@url") ??
-                    doc.SelectSingleNode("//fanart/movie/moviearts/movieart/@url");
-                path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Art);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Art, null, cancellationToken)
-                                        .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Art, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
+
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
             if (ConfigurationManager.Configuration.DownloadMovieImages.Disc && !item.HasImage(ImageType.Disc))
             if (ConfigurationManager.Configuration.DownloadMovieImages.Disc && !item.HasImage(ImageType.Disc))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/movie/moviediscs/moviedisc[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/movie/moviediscs/moviedisc/@url");
-                path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Disc);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Disc, null, cancellationToken)
-                                        .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Disc, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
@@ -368,13 +301,11 @@ namespace MediaBrowser.Providers.Movies
 
 
             if (ConfigurationManager.Configuration.DownloadMovieImages.Banner && !item.HasImage(ImageType.Banner))
             if (ConfigurationManager.Configuration.DownloadMovieImages.Banner && !item.HasImage(ImageType.Banner))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/movie/moviebanners/moviebanner[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/movie/moviebanners/moviebanner/@url");
-                path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Banner);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Banner, null, cancellationToken)
-                                        .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Banner, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
@@ -382,40 +313,30 @@ namespace MediaBrowser.Providers.Movies
 
 
             if (ConfigurationManager.Configuration.DownloadMovieImages.Thumb && !item.HasImage(ImageType.Thumb))
             if (ConfigurationManager.Configuration.DownloadMovieImages.Thumb && !item.HasImage(ImageType.Thumb))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/movie/moviethumbs/moviethumb[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/movie/moviethumbs/moviethumb/@url");
-                path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Thumb);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Thumb, null, cancellationToken)
-                                        .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Thumb, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
+            cancellationToken.ThrowIfCancellationRequested();
+
             var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
             var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
-            if (ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit)
+            if (ConfigurationManager.Configuration.DownloadMovieImages.Backdrops &&
+                item.BackdropImagePaths.Count < backdropLimit)
             {
             {
-                var nodes = doc.SelectNodes("//fanart/movie/moviebackgrounds//@url");
+                var numBackdrops = item.BackdropImagePaths.Count;
 
 
-                if (nodes != null)
+                foreach (var image in images.Where(i => i.Type == ImageType.Backdrop))
                 {
                 {
-                    var numBackdrops = item.BackdropImagePaths.Count;
-
-                    foreach (XmlNode node in nodes)
-                    {
-                        path = node.Value;
-
-                        if (!string.IsNullOrEmpty(path) && !item.ContainsImageWithSourceUrl(path))
-                        {
-                            await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
-                                                .ConfigureAwait(false);
-
-                            numBackdrops++;
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
+                                        .ConfigureAwait(false);
 
 
-                            if (item.BackdropImagePaths.Count >= backdropLimit) break;
-                        }
-                    }
+                    numBackdrops++;
 
 
+                    if (item.BackdropImagePaths.Count >= backdropLimit) break;
                 }
                 }
             }
             }
         }
         }

+ 5 - 14
MediaBrowser.Providers/Movies/FanArtMovieUpdatesPrescanTask.cs

@@ -86,7 +86,7 @@ namespace MediaBrowser.Providers.Movies
 
 
                 progress.Report(5);
                 progress.Report(5);
 
 
-                await UpdateMovies(moviesToUpdate, path, progress, cancellationToken).ConfigureAwait(false);
+                await UpdateMovies(moviesToUpdate, progress, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
             var newUpdateTime = Convert.ToInt64(DateTimeToUnixTimestamp(DateTime.UtcNow)).ToString(UsCulture);
             var newUpdateTime = Convert.ToInt64(DateTimeToUnixTimestamp(DateTime.UtcNow)).ToString(UsCulture);
@@ -127,14 +127,16 @@ namespace MediaBrowser.Providers.Movies
             }
             }
         }
         }
 
 
-        private async Task UpdateMovies(IEnumerable<string> idList, string moviesDataPath, IProgress<double> progress, CancellationToken cancellationToken)
+        private async Task UpdateMovies(IEnumerable<string> idList, IProgress<double> progress, CancellationToken cancellationToken)
         {
         {
             var list = idList.ToList();
             var list = idList.ToList();
             var numComplete = 0;
             var numComplete = 0;
 
 
             foreach (var id in list)
             foreach (var id in list)
             {
             {
-                await UpdateMovie(id, moviesDataPath, cancellationToken).ConfigureAwait(false);
+                _logger.Info("Updating movie " + id);
+
+                await FanArtMovieProvider.Current.DownloadMovieXml(id, cancellationToken).ConfigureAwait(false);
 
 
                 numComplete++;
                 numComplete++;
                 double percent = numComplete;
                 double percent = numComplete;
@@ -145,17 +147,6 @@ namespace MediaBrowser.Providers.Movies
             }
             }
         }
         }
 
 
-        private Task UpdateMovie(string tmdbId, string movieDataPath, CancellationToken cancellationToken)
-        {
-            _logger.Info("Updating movie " + tmdbId);
-
-            movieDataPath = Path.Combine(movieDataPath, tmdbId);
-
-            Directory.CreateDirectory(movieDataPath);
-
-            return FanArtMovieProvider.Current.DownloadMovieXml(movieDataPath, tmdbId, cancellationToken);
-        }
-
         /// <summary>
         /// <summary>
         /// Dates the time to unix timestamp.
         /// Dates the time to unix timestamp.
         /// </summary>
         /// </summary>

+ 301 - 0
MediaBrowser.Providers/Movies/ManualFanartMovieImageProvider.cs

@@ -0,0 +1,301 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Providers.Movies
+{
+    public class ManualFanartMovieImageProvider : IImageProvider
+    {
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly IServerConfigurationManager _config;
+
+        public ManualFanartMovieImageProvider(IServerConfigurationManager config)
+        {
+            _config = config;
+        }
+
+        public string Name
+        {
+            get { return ProviderName; }
+        }
+
+        public static string ProviderName
+        {
+            get { return "FanArt"; }
+        }
+
+        public bool Supports(BaseItem item)
+        {
+            return FanArtMovieProvider.Current.Supports(item);
+        }
+
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken)
+        {
+            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
+
+            return images.Where(i => i.Type == imageType);
+        }
+
+        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken)
+        {
+            var list = new List<RemoteImageInfo>();
+
+            var movieId = item.GetProviderId(MetadataProviders.Tmdb);
+
+            if (!string.IsNullOrEmpty(movieId))
+            {
+                var xmlPath = FanArtMovieProvider.Current.GetFanartXmlPath(movieId);
+
+                try
+                {
+                    AddImages(list, xmlPath, cancellationToken);
+                }
+                catch (FileNotFoundException)
+                {
+                    // No biggie. Don't blow up
+                }
+            }
+
+            var language = _config.Configuration.PreferredMetadataLanguage;
+
+            var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+            
+            // Sort first by width to prioritize HD versions
+            list = list.OrderByDescending(i => i.Width ?? 0)
+                .ThenByDescending(i =>
+                {
+                    if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return 3;
+                    }
+                    if (!isLanguageEn)
+                    {
+                        if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+                        {
+                            return 2;
+                        }
+                    }
+                    if (string.IsNullOrEmpty(i.Language))
+                    {
+                        return isLanguageEn ? 3 : 2;
+                    }
+                    return 0;
+                })
+                .ThenByDescending(i => i.CommunityRating ?? 0)
+                .ToList();
+
+            return Task.FromResult<IEnumerable<RemoteImageInfo>>(list);
+        }
+
+        private void AddImages(List<RemoteImageInfo> list, string xmlPath, CancellationToken cancellationToken)
+        {
+            using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
+            {
+                // Use XmlReader for best performance
+                using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
+                {
+                    CheckCharacters = false,
+                    IgnoreProcessingInstructions = true,
+                    IgnoreComments = true,
+                    ValidationType = ValidationType.None
+                }))
+                {
+                    reader.MoveToContent();
+
+                    // Loop through each element
+                    while (reader.Read())
+                    {
+                        cancellationToken.ThrowIfCancellationRequested();
+
+                        if (reader.NodeType == XmlNodeType.Element)
+                        {
+                            switch (reader.Name)
+                            {
+                                case "movie":
+                                    {
+                                        using (var subReader = reader.ReadSubtree())
+                                        {
+                                            AddImages(list, subReader, cancellationToken);
+                                        }
+                                        break;
+                                    }
+
+                                default:
+                                    reader.Skip();
+                                    break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private void AddImages(List<RemoteImageInfo> list, XmlReader reader, CancellationToken cancellationToken)
+        {
+            reader.MoveToContent();
+
+            while (reader.Read())
+            {
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "hdmoviecleararts":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Art, 1000, 562);
+                                }
+                                break;
+                            }
+                        case "hdmovielogos":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Logo, 800, 310);
+                                }
+                                break;
+                            }
+                        case "moviediscs":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Disc, 1000, 1000);
+                                }
+                                break;
+                            }
+                        case "movieposters":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Primary, 1000, 1426);
+                                }
+                                break;
+                            }
+                        case "movielogos":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Logo, 400, 155);
+                                }
+                                break;
+                            }
+                        case "moviearts":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Art, 500, 281);
+                                }
+                                break;
+                            }
+                        case "moviethumbs":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Thumb, 1000, 562);
+                                }
+                                break;
+                            }
+                        case "moviebanners":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Banner, 1000, 185);
+                                }
+                                break;
+                            }
+                        case "moviebackgrounds":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Backdrop, 1920, 1080);
+                                }
+                                break;
+                            }
+                        default:
+                            {
+                                using (reader.ReadSubtree())
+                                {
+                                }
+                                break;
+                            }
+                    }
+                }
+            }
+        }
+
+        private void PopulateImageCategory(List<RemoteImageInfo> list, XmlReader reader, CancellationToken cancellationToken, ImageType type, int width, int height)
+        {
+            reader.MoveToContent();
+
+            while (reader.Read())
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "hdmovielogo":
+                        case "moviedisc":
+                        case "hdmovieclearart":
+                        case "movieposter":
+                        case "movielogo":
+                        case "movieart":
+                        case "moviethumb":
+                        case "moviebanner":
+                        case "moviebackground":
+                            {
+                                var url = reader.GetAttribute("url");
+
+                                if (!string.IsNullOrEmpty(url))
+                                {
+                                    var likesString = reader.GetAttribute("likes");
+                                    int likes;
+
+                                    var info = new RemoteImageInfo
+                                    {
+                                        RatingType = RatingType.Likes,
+                                        Type = type,
+                                        Width = width,
+                                        Height = height,
+                                        ProviderName = Name,
+                                        Url = url,
+                                        Language = reader.GetAttribute("lang")
+                                    };
+
+                                    if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Any, _usCulture, out likes))
+                                    {
+                                        info.CommunityRating = likes;
+                                    }
+
+                                    list.Add(info);
+                                }
+                                break;
+                            }
+                        default:
+                            reader.Skip();
+                            break;
+                    }
+                }
+            }
+        }
+
+        public int Priority
+        {
+            get { return 1; }
+        }
+    }
+}

+ 24 - 14
MediaBrowser.Providers/Movies/ManualMovieDbImageProvider.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Controller.Configuration;
 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.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
@@ -26,20 +27,20 @@ namespace MediaBrowser.Providers.Movies
 
 
         public string Name
         public string Name
         {
         {
-            get { return "TheMovieDB"; }
+            get { return ProviderName; }
         }
         }
 
 
-        public bool Supports(BaseItem item, ImageType imageType)
+        public static string ProviderName
         {
         {
-            if (MovieDbImagesProvider.SupportsItem(item))
-            {
-                return imageType == ImageType.Primary || imageType == ImageType.Backdrop;
-            }
+            get { return "TheMovieDb"; }
+        }
 
 
-            return false;
+        public bool Supports(BaseItem item)
+        {
+            return MovieDbImagesProvider.SupportsItem(item);
         }
         }
 
 
-        public async Task<IEnumerable<RemoteImageInfo>> GetAvailableImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken)
         {
         {
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
             var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
 
 
@@ -70,7 +71,8 @@ namespace MediaBrowser.Providers.Movies
                 Height = i.height,
                 Height = i.height,
                 Language = i.iso_639_1,
                 Language = i.iso_639_1,
                 ProviderName = Name,
                 ProviderName = Name,
-                Type = ImageType.Primary
+                Type = ImageType.Primary,
+                RatingType = RatingType.Score
             }));
             }));
 
 
             list.AddRange(GetBackdrops(results, item).Select(i => new RemoteImageInfo
             list.AddRange(GetBackdrops(results, item).Select(i => new RemoteImageInfo
@@ -81,7 +83,8 @@ namespace MediaBrowser.Providers.Movies
                 Width = i.width,
                 Width = i.width,
                 Height = i.height,
                 Height = i.height,
                 ProviderName = Name,
                 ProviderName = Name,
-                Type = ImageType.Backdrop
+                Type = ImageType.Backdrop,
+                RatingType = RatingType.Score
             }));
             }));
             
             
             return list;
             return list;
@@ -101,7 +104,7 @@ namespace MediaBrowser.Providers.Movies
 
 
             var eligiblePosters = images.posters == null ?
             var eligiblePosters = images.posters == null ?
                 new List<MovieDbProvider.Poster>() :
                 new List<MovieDbProvider.Poster>() :
-                images.posters.Where(i => i.width >= _config.Configuration.MinMoviePosterWidth)
+                images.posters
                 .ToList();
                 .ToList();
 
 
             return eligiblePosters.OrderByDescending(i =>
             return eligiblePosters.OrderByDescending(i =>
@@ -124,6 +127,7 @@ namespace MediaBrowser.Providers.Movies
                     return 0;
                     return 0;
                 })
                 })
                 .ThenByDescending(i => i.vote_average)
                 .ThenByDescending(i => i.vote_average)
+                .ThenByDescending(i => i.vote_count)
                 .ToList();
                 .ToList();
         }
         }
 
 
@@ -136,10 +140,11 @@ namespace MediaBrowser.Providers.Movies
         private IEnumerable<MovieDbProvider.Backdrop> GetBackdrops(MovieDbProvider.Images images, BaseItem item)
         private IEnumerable<MovieDbProvider.Backdrop> GetBackdrops(MovieDbProvider.Images images, BaseItem item)
         {
         {
             var eligibleBackdrops = images.backdrops == null ? new List<MovieDbProvider.Backdrop>() :
             var eligibleBackdrops = images.backdrops == null ? new List<MovieDbProvider.Backdrop>() :
-                images.backdrops.Where(i => i.width >= _config.Configuration.MinMovieBackdropWidth)
+                images.backdrops
                 .ToList();
                 .ToList();
 
 
-            return eligibleBackdrops.OrderByDescending(i => i.vote_average);
+            return eligibleBackdrops.OrderByDescending(i => i.vote_average)
+                .ThenByDescending(i => i.vote_count);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -150,7 +155,7 @@ namespace MediaBrowser.Providers.Movies
         /// <returns>Task{MovieImages}.</returns>
         /// <returns>Task{MovieImages}.</returns>
         private MovieDbProvider.Images FetchImages(BaseItem item, IJsonSerializer jsonSerializer)
         private MovieDbProvider.Images FetchImages(BaseItem item, IJsonSerializer jsonSerializer)
         {
         {
-            var path = MovieDbProvider.Current.GetDataFilePath(item, "default");
+            var path = MovieDbProvider.Current.GetImagesDataFilePath(item);
 
 
             if (!string.IsNullOrEmpty(path))
             if (!string.IsNullOrEmpty(path))
             {
             {
@@ -164,5 +169,10 @@ namespace MediaBrowser.Providers.Movies
 
 
             return null;
             return null;
         }
         }
+
+        public int Priority
+        {
+            get { return 2; }
+        }
     }
     }
 }
 }

+ 5 - 18
MediaBrowser.Providers/Movies/MovieDbImagesProvider.cs

@@ -8,7 +8,6 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -28,10 +27,6 @@ namespace MediaBrowser.Providers.Movies
         /// </summary>
         /// </summary>
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
 
 
-        /// <summary>
-        /// The _json serializer
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
 
 
         /// <summary>
         /// <summary>
@@ -40,12 +35,10 @@ namespace MediaBrowser.Providers.Movies
         /// <param name="logManager">The log manager.</param>
         /// <param name="logManager">The log manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="providerManager">The provider manager.</param>
         /// <param name="providerManager">The provider manager.</param>
-        /// <param name="jsonSerializer">The json serializer.</param>
-        public MovieDbImagesProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
+        public MovieDbImagesProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem)
             : base(logManager, configurationManager)
             : base(logManager, configurationManager)
         {
         {
             _providerManager = providerManager;
             _providerManager = providerManager;
-            _jsonSerializer = jsonSerializer;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
         }
         }
 
 
@@ -149,12 +142,7 @@ namespace MediaBrowser.Providers.Movies
 
 
         protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
         protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
         {
         {
-            if (string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)))
-            {
-                return false;
-            }
-
-            var path = MovieDbProvider.Current.GetDataFilePath(item, "default");
+            var path = MovieDbProvider.Current.GetImagesDataFilePath(item);
 
 
             if (!string.IsNullOrEmpty(path))
             if (!string.IsNullOrEmpty(path))
             {
             {
@@ -182,8 +170,7 @@ namespace MediaBrowser.Providers.Movies
 
 
             if (!string.IsNullOrEmpty(id))
             if (!string.IsNullOrEmpty(id))
             {
             {
-                var images = await new ManualMovieDbImageProvider(_jsonSerializer, ConfigurationManager).GetAllImages(item,
-                            cancellationToken).ConfigureAwait(false);
+                var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualMovieDbImageProvider.ProviderName).ConfigureAwait(false);
 
 
                 await ProcessImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
                 await ProcessImages(item, images.ToList(), cancellationToken).ConfigureAwait(false);
             }
             }
@@ -204,7 +191,7 @@ namespace MediaBrowser.Providers.Movies
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
             var eligiblePosters = images
             var eligiblePosters = images
-                .Where(i => i.Type == ImageType.Primary)
+                .Where(i => i.Type == ImageType.Primary && i.Width.HasValue && i.Width.Value >= ConfigurationManager.Configuration.MinMoviePosterWidth)
                 .ToList();
                 .ToList();
 
 
             //        poster
             //        poster
@@ -228,7 +215,7 @@ namespace MediaBrowser.Providers.Movies
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
             var eligibleBackdrops = images
             var eligibleBackdrops = images
-                .Where(i => i.Type == ImageType.Backdrop)
+                .Where(i => i.Type == ImageType.Backdrop && i.Width.HasValue && i.Width.Value >= ConfigurationManager.Configuration.MinMovieBackdropWidth)
                 .ToList();
                 .ToList();
 
 
             var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
             var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;

+ 25 - 16
MediaBrowser.Providers/Movies/MovieDbProvider.cs

@@ -198,8 +198,7 @@ namespace MediaBrowser.Providers.Movies
 
 
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
         {
         {
-            // Boxsets require two passes because we need the children to be refreshed
-            if (item is BoxSet && string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)))
+            if (string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tmdb)))
             {
             {
                 return true;
                 return true;
             }
             }
@@ -209,20 +208,17 @@ namespace MediaBrowser.Providers.Movies
 
 
         protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
         protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
         {
         {
-            var language = ConfigurationManager.Configuration.PreferredMetadataLanguage;
-
-            var path = GetDataFilePath(item, language);
+            var path = GetDataFilePath(item);
 
 
             if (!string.IsNullOrEmpty(path))
             if (!string.IsNullOrEmpty(path))
             {
             {
-                var fileInfo = new FileInfo(path);
+                var imagesFilePath = GetImagesDataFilePath(item);
 
 
-                if (fileInfo.Exists)
-                {
-                    return _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed;
-                }
+                var fileInfo = new FileInfo(path);
+                var imagesFileInfo = new FileInfo(imagesFilePath);
 
 
-                return true;
+                return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed ||
+                    !imagesFileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(imagesFileInfo) > providerInfo.LastRefreshed;
             }
             }
 
 
             return base.NeedsRefreshBasedOnCompareDate(item, providerInfo);
             return base.NeedsRefreshBasedOnCompareDate(item, providerInfo);
@@ -508,9 +504,9 @@ namespace MediaBrowser.Providers.Movies
 
 
             var language = ConfigurationManager.Configuration.PreferredMetadataLanguage;
             var language = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 
 
-            var dataFilePath = GetDataFilePath(item, language);
+            var dataFilePath = GetDataFilePath(item);
 
 
-            if (string.IsNullOrEmpty(dataFilePath) || !File.Exists(dataFilePath))
+            if (string.IsNullOrEmpty(dataFilePath) || !File.Exists(dataFilePath) || !File.Exists(GetImagesDataFilePath(item)))
             {
             {
                 var isBoxSet = item is BoxSet;
                 var isBoxSet = item is BoxSet;
 
 
@@ -538,7 +534,7 @@ namespace MediaBrowser.Providers.Movies
 
 
             if (isForcedRefresh || ConfigurationManager.Configuration.EnableTmdbUpdates || !HasAltMeta(item))
             if (isForcedRefresh || ConfigurationManager.Configuration.EnableTmdbUpdates || !HasAltMeta(item))
             {
             {
-                dataFilePath = GetDataFilePath(item, language);
+                dataFilePath = GetDataFilePath(item);
 
 
                 var mainResult = JsonSerializer.DeserializeFromFile<CompleteMovieData>(dataFilePath);
                 var mainResult = JsonSerializer.DeserializeFromFile<CompleteMovieData>(dataFilePath);
 
 
@@ -580,10 +576,11 @@ namespace MediaBrowser.Providers.Movies
         /// Gets the data file path.
         /// Gets the data file path.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <param name="language">The language.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        internal string GetDataFilePath(BaseItem item, string language)
+        internal string GetDataFilePath(BaseItem item)
         {
         {
+            var language = ConfigurationManager.Configuration.PreferredMetadataLanguage;
+            
             var id = item.GetProviderId(MetadataProviders.Tmdb);
             var id = item.GetProviderId(MetadataProviders.Tmdb);
 
 
             if (string.IsNullOrEmpty(id))
             if (string.IsNullOrEmpty(id))
@@ -598,6 +595,18 @@ namespace MediaBrowser.Providers.Movies
             return path;
             return path;
         }
         }
 
 
+        internal string GetImagesDataFilePath(BaseItem item)
+        {
+            var path = GetDataFilePath(item);
+
+            if (!string.IsNullOrEmpty(path))
+            {
+                path = Path.Combine(Path.GetDirectoryName(path), "default.json");
+            }
+
+            return path;
+        }
+
         /// <summary>
         /// <summary>
         /// Fetches the main result.
         /// Fetches the main result.
         /// </summary>
         /// </summary>

+ 1 - 3
MediaBrowser.Providers/Movies/MovieUpdatesPrescanTask.cs

@@ -100,10 +100,8 @@ namespace MediaBrowser.Providers.Movies
 
 
             var timestampFileInfo = new FileInfo(timestampFile);
             var timestampFileInfo = new FileInfo(timestampFile);
 
 
-            var refreshDays = _config.Configuration.EnableTmdbUpdates ? 1 : 7;
-
             // Don't check for tvdb updates anymore frequently than 24 hours
             // Don't check for tvdb updates anymore frequently than 24 hours
-            if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < refreshDays)
+            if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 1)
             {
             {
                 return;
                 return;
             }
             }

+ 2 - 2
MediaBrowser.Providers/Savers/XmlSaverHelpers.cs

@@ -562,8 +562,8 @@ namespace MediaBrowser.Providers.Savers
                     {
                     {
                         var timespan = TimeSpan.FromTicks(item.RunTimeTicks.Value);
                         var timespan = TimeSpan.FromTicks(item.RunTimeTicks.Value);
 
 
-                        builder.Append("<Duration>" + Convert.ToInt32(timespan.TotalMinutes).ToString(UsCulture) + "</Duration>");
-                        builder.Append("<DurationSeconds>" + Convert.ToInt32(timespan.TotalSeconds).ToString(UsCulture) + "</DurationSeconds>");
+                        builder.Append("<Duration>" + Convert.ToInt64(timespan.TotalMinutes).ToString(UsCulture) + "</Duration>");
+                        builder.Append("<DurationSeconds>" + Convert.ToInt64(timespan.TotalSeconds).ToString(UsCulture) + "</DurationSeconds>");
                     }
                     }
 
 
                     if (video != null && video.Video3DFormat.HasValue)
                     if (video != null && video.Video3DFormat.HasValue)

+ 15 - 51
MediaBrowser.Providers/TV/FanArtSeasonProvider.cs

@@ -6,11 +6,13 @@ using MediaBrowser.Controller.Library;
 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.Providers;
 using System;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.IO;
+using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using System.Xml;
 
 
 namespace MediaBrowser.Providers.TV
 namespace MediaBrowser.Providers.TV
 {
 {
@@ -73,7 +75,7 @@ namespace MediaBrowser.Providers.TV
             if (!string.IsNullOrEmpty(seriesId))
             if (!string.IsNullOrEmpty(seriesId))
             {
             {
                 // Process images
                 // Process images
-                var imagesXmlPath = Path.Combine(FanArtTvProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "fanart.xml");
+                var imagesXmlPath = FanArtTvProvider.Current.GetFanartXmlPath(seriesId);
 
 
                 var imagesFileInfo = new FileInfo(imagesXmlPath);
                 var imagesFileInfo = new FileInfo(imagesXmlPath);
 
 
@@ -99,69 +101,31 @@ namespace MediaBrowser.Providers.TV
 
 
             var season = (Season)item;
             var season = (Season)item;
 
 
-            var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null;
-
-            if (!string.IsNullOrEmpty(seriesId))
-            {
-                // Process images
-                var imagesXmlPath = Path.Combine(FanArtTvProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "fanart.xml");
-
-                var imagesFileInfo = new FileInfo(imagesXmlPath);
-
-                if (imagesFileInfo.Exists)
-                {
-                    if (!season.HasImage(ImageType.Thumb))
-                    {
-                        var xmlDoc = new XmlDocument();
-                        xmlDoc.Load(imagesXmlPath);
+            // Process images
+            var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartSeasonImageProvider.ProviderName).ConfigureAwait(false);
 
 
-                        await FetchImages(season, xmlDoc, cancellationToken).ConfigureAwait(false);
-                    }
-                }
+            await FetchImages(season, images.ToList(), cancellationToken).ConfigureAwait(false);
 
 
-                BaseProviderInfo data;
-                if (!item.ProviderData.TryGetValue(Id, out data))
-                {
-                    data = new BaseProviderInfo();
-                    item.ProviderData[Id] = data;
-                }
-
-                SetLastRefreshed(item, DateTime.UtcNow);
-                return true;
-            }
-
-            return false;
+            SetLastRefreshed(item, DateTime.UtcNow);
+            return true;
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Fetches the images.
         /// Fetches the images.
         /// </summary>
         /// </summary>
         /// <param name="season">The season.</param>
         /// <param name="season">The season.</param>
-        /// <param name="doc">The doc.</param>
+        /// <param name="images">The images.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        private async Task FetchImages(Season season, XmlDocument doc, CancellationToken cancellationToken)
+        private async Task FetchImages(Season season, List<RemoteImageInfo> images, CancellationToken cancellationToken)
         {
         {
-            var seasonNumber = season.IndexNumber ?? -1;
-
-            if (seasonNumber == -1)
-            {
-                return;
-            }
-
-            var language = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower();
-
             if (ConfigurationManager.Configuration.DownloadSeasonImages.Thumb && !season.HasImage(ImageType.Thumb))
             if (ConfigurationManager.Configuration.DownloadSeasonImages.Thumb && !season.HasImage(ImageType.Thumb))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/series/seasonthumbs/seasonthumb[@lang = \"" + language + "\"][@season = \"" + seasonNumber + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/series/seasonthumbs/seasonthumb[@season = \"" + seasonNumber + "\"]/@url");
-                
-                var path = node != null ? node.Value : null;
-                
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Thumb);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(season, path, FanArtResourcePool, ImageType.Thumb, null, cancellationToken)
-                                        .ConfigureAwait(false);
+                    await _providerManager.SaveImage(season, image.Url, FanArtResourcePool, ImageType.Thumb, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
         }
         }

+ 57 - 114
MediaBrowser.Providers/TV/FanArtTVProvider.cs

@@ -4,18 +4,18 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 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.IO;
 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.Logging;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Providers;
 using System;
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 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 System.Xml;
 
 
 namespace MediaBrowser.Providers.TV
 namespace MediaBrowser.Providers.TV
 {
 {
@@ -82,26 +82,6 @@ namespace MediaBrowser.Providers.TV
                 return false;
                 return false;
             }
             }
 
 
-            if (!ConfigurationManager.Configuration.DownloadSeriesImages.Art &&
-                !ConfigurationManager.Configuration.DownloadSeriesImages.Logo &&
-                !ConfigurationManager.Configuration.DownloadSeriesImages.Thumb &&
-                !ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops &&
-                !ConfigurationManager.Configuration.DownloadSeriesImages.Banner &&
-                !ConfigurationManager.Configuration.DownloadSeriesImages.Primary)
-            {
-                return false;
-            }
-
-            if (item.HasImage(ImageType.Primary) &&
-                item.HasImage(ImageType.Art) &&
-                item.HasImage(ImageType.Logo) &&
-                item.HasImage(ImageType.Banner) &&
-                item.HasImage(ImageType.Thumb) &&
-                item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops)
-            {
-                return false;
-            }
-
             return base.NeedsRefreshInternal(item, providerInfo);
             return base.NeedsRefreshInternal(item, providerInfo);
         }
         }
 
 
@@ -112,28 +92,14 @@ namespace MediaBrowser.Providers.TV
             if (!string.IsNullOrEmpty(id))
             if (!string.IsNullOrEmpty(id))
             {
             {
                 // Process images
                 // Process images
-                var path = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, id);
+                var xmlPath = GetFanartXmlPath(id);
 
 
-                try
-                {
-                    var files = new DirectoryInfo(path)
-                        .EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)
-                        .Select(i => _fileSystem.GetLastWriteTimeUtc(i))
-                        .ToList();
-
-                    if (files.Count > 0)
-                    {
-                        return files.Max() > providerInfo.LastRefreshed;
-                    }
-                }
-                catch (DirectoryNotFoundException)
-                {
-                    // Don't blow up
-                    return true;
-                }
+                var fileInfo = new FileInfo(xmlPath);
+
+                return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed;
             }
             }
-            
-            return false;
+
+            return base.NeedsRefreshBasedOnCompareDate(item, providerInfo);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -184,6 +150,12 @@ namespace MediaBrowser.Providers.TV
 
 
             return dataPath;
             return dataPath;
         }
         }
+
+        public string GetFanartXmlPath(string tvdbId)
+        {
+            var dataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, tvdbId);
+            return Path.Combine(dataPath, "fanart.xml");
+        }
         
         
         protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
         protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
         
         
@@ -195,19 +167,17 @@ namespace MediaBrowser.Providers.TV
 
 
             if (!string.IsNullOrEmpty(seriesId))
             if (!string.IsNullOrEmpty(seriesId))
             {
             {
-                var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId);
-                var xmlPath = Path.Combine(seriesDataPath, "fanart.xml");
+                var xmlPath = GetFanartXmlPath(seriesId);
 
 
                 // Only download the xml if it doesn't already exist. The prescan task will take care of getting updates
                 // Only download the xml if it doesn't already exist. The prescan task will take care of getting updates
                 if (!File.Exists(xmlPath))
                 if (!File.Exists(xmlPath))
                 {
                 {
-                    await DownloadSeriesXml(seriesDataPath, seriesId, cancellationToken).ConfigureAwait(false);
+                    await DownloadSeriesXml(seriesId, cancellationToken).ConfigureAwait(false);
                 }
                 }
 
 
-                if (File.Exists(xmlPath))
-                {
-                    await FetchFromXml(item, xmlPath, cancellationToken).ConfigureAwait(false);
-                }
+                var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartSeriesImageProvider.ProviderName).ConfigureAwait(false);
+
+                await FetchFromXml(item, images.ToList(), cancellationToken).ConfigureAwait(false);
             }
             }
 
 
             SetLastRefreshed(item, DateTime.UtcNow);
             SetLastRefreshed(item, DateTime.UtcNow);
@@ -219,27 +189,20 @@ namespace MediaBrowser.Providers.TV
         /// Fetches from XML.
         /// Fetches from XML.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <param name="xmlFilePath">The XML file path.</param>
+        /// <param name="images">The images.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        private async Task FetchFromXml(BaseItem item, string xmlFilePath, CancellationToken cancellationToken)
+        private async Task FetchFromXml(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
         {
         {
-            var doc = new XmlDocument();
-            doc.Load(xmlFilePath);
-
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
-            var language = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower();
-
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Primary && !item.HasImage(ImageType.Primary))
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Primary && !item.HasImage(ImageType.Primary))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/series/tvposters/tvposter[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/series/tvposters/tvposter/@url");
-                var path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Primary);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Primary, null, cancellationToken)
-                          .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
@@ -247,15 +210,11 @@ namespace MediaBrowser.Providers.TV
 
 
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Logo && !item.HasImage(ImageType.Logo))
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Logo && !item.HasImage(ImageType.Logo))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/series/hdtvlogos/hdtvlogo[@lang = \"" + language + "\"]/@url") ??
-                            doc.SelectSingleNode("//fanart/series/clearlogos/clearlogo[@lang = \"" + language + "\"]/@url") ??
-                            doc.SelectSingleNode("//fanart/series/hdtvlogos/hdtvlogo/@url") ??
-                            doc.SelectSingleNode("//fanart/series/clearlogos/clearlogo/@url");
-                var path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Logo);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Logo, null, cancellationToken)
-                          .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Logo, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
@@ -263,15 +222,11 @@ namespace MediaBrowser.Providers.TV
 
 
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Art && !item.HasImage(ImageType.Art))
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Art && !item.HasImage(ImageType.Art))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/series/hdcleararts/hdclearart[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/series/cleararts/clearart[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/series/hdcleararts/hdclearart/@url") ??
-                           doc.SelectSingleNode("//fanart/series/cleararts/clearart/@url");
-                var path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Art);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Art, null, cancellationToken)
-                          .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Art, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
@@ -279,53 +234,42 @@ namespace MediaBrowser.Providers.TV
 
 
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Thumb && !item.HasImage(ImageType.Thumb))
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Thumb && !item.HasImage(ImageType.Thumb))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/series/tvthumbs/tvthumb[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/series/tvthumbs/tvthumb/@url");
-                var path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Thumb);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Thumb, null, cancellationToken)
-                          .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Thumb, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
+            cancellationToken.ThrowIfCancellationRequested();
+
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !item.HasImage(ImageType.Banner))
             if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !item.HasImage(ImageType.Banner))
             {
             {
-                var node = doc.SelectSingleNode("//fanart/series/tbbanners/tvbanner[@lang = \"" + language + "\"]/@url") ??
-                           doc.SelectSingleNode("//fanart/series/tbbanners/tvbanner/@url");
-                var path = node != null ? node.Value : null;
-                if (!string.IsNullOrEmpty(path))
+                var image = images.FirstOrDefault(i => i.Type == ImageType.Banner);
+
+                if (image != null)
                 {
                 {
-                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Banner, null, cancellationToken)
-                          .ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Banner, null, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
-            var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
+            cancellationToken.ThrowIfCancellationRequested();
 
 
-            if (ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit)
+            var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
+            if (ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops &&
+                item.BackdropImagePaths.Count < backdropLimit)
             {
             {
-                var nodes = doc.SelectNodes("//fanart/series/showbackgrounds//@url");
+                var numBackdrops = item.BackdropImagePaths.Count;
 
 
-                if (nodes != null)
+                foreach (var image in images.Where(i => i.Type == ImageType.Backdrop))
                 {
                 {
-                    var numBackdrops = item.BackdropImagePaths.Count;
-
-                    foreach (XmlNode node in nodes)
-                    {
-                        var path = node.Value;
-
-                        if (!string.IsNullOrEmpty(path) && !item.ContainsImageWithSourceUrl(path))
-                        {
-                            await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
-                                  .ConfigureAwait(false);
-
-                            numBackdrops++;
+                    await _providerManager.SaveImage(item, image.Url, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
+                                        .ConfigureAwait(false);
 
 
-                            if (item.BackdropImagePaths.Count >= backdropLimit) break;
-                        }
-                    }
+                    numBackdrops++;
 
 
+                    if (item.BackdropImagePaths.Count >= backdropLimit) break;
                 }
                 }
             }
             }
 
 
@@ -334,19 +278,18 @@ namespace MediaBrowser.Providers.TV
         /// <summary>
         /// <summary>
         /// Downloads the series XML.
         /// Downloads the series XML.
         /// </summary>
         /// </summary>
-        /// <param name="seriesDataPath">The series data path.</param>
         /// <param name="tvdbId">The TVDB id.</param>
         /// <param name="tvdbId">The TVDB id.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        internal async Task DownloadSeriesXml(string seriesDataPath, string tvdbId, CancellationToken cancellationToken)
+        internal async Task DownloadSeriesXml(string tvdbId, CancellationToken cancellationToken)
         {
         {
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
-            string url = string.Format(FanArtBaseUrl, ApiKey, tvdbId);
+            var url = string.Format(FanArtBaseUrl, ApiKey, tvdbId);
 
 
-            var xmlPath = Path.Combine(seriesDataPath, "fanart.xml");
+            var xmlPath = GetFanartXmlPath(tvdbId);
 
 
-            Directory.CreateDirectory(seriesDataPath);
+            Directory.CreateDirectory(Path.GetDirectoryName(xmlPath));
 
 
             using (var response = await HttpClient.Get(new HttpRequestOptions
             using (var response = await HttpClient.Get(new HttpRequestOptions
             {
             {

+ 5 - 15
MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs

@@ -86,7 +86,7 @@ namespace MediaBrowser.Providers.TV
 
 
                 progress.Report(5);
                 progress.Report(5);
 
 
-                await UpdateSeries(seriesToUpdate, path, progress, cancellationToken).ConfigureAwait(false);
+                await UpdateSeries(seriesToUpdate, progress, cancellationToken).ConfigureAwait(false);
             }
             }
 
 
             var newUpdateTime = Convert.ToInt64(DateTimeToUnixTimestamp(DateTime.UtcNow)).ToString(UsCulture);
             var newUpdateTime = Convert.ToInt64(DateTimeToUnixTimestamp(DateTime.UtcNow)).ToString(UsCulture);
@@ -138,18 +138,19 @@ namespace MediaBrowser.Providers.TV
         /// Updates the series.
         /// Updates the series.
         /// </summary>
         /// </summary>
         /// <param name="idList">The id list.</param>
         /// <param name="idList">The id list.</param>
-        /// <param name="seriesDataPath">The artists data path.</param>
         /// <param name="progress">The progress.</param>
         /// <param name="progress">The progress.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
-        private async Task UpdateSeries(IEnumerable<string> idList, string seriesDataPath, IProgress<double> progress, CancellationToken cancellationToken)
+        private async Task UpdateSeries(IEnumerable<string> idList, IProgress<double> progress, CancellationToken cancellationToken)
         {
         {
             var list = idList.ToList();
             var list = idList.ToList();
             var numComplete = 0;
             var numComplete = 0;
 
 
             foreach (var id in list)
             foreach (var id in list)
             {
             {
-                await UpdateSeries(id, seriesDataPath, cancellationToken).ConfigureAwait(false);
+                _logger.Info("Updating series " + id);
+                
+                await FanArtTvProvider.Current.DownloadSeriesXml(id, cancellationToken).ConfigureAwait(false);
 
 
                 numComplete++;
                 numComplete++;
                 double percent = numComplete;
                 double percent = numComplete;
@@ -160,17 +161,6 @@ namespace MediaBrowser.Providers.TV
             }
             }
         }
         }
 
 
-        private Task UpdateSeries(string tvdbId, string seriesDataPath, CancellationToken cancellationToken)
-        {
-            _logger.Info("Updating series " + tvdbId);
-
-            seriesDataPath = Path.Combine(seriesDataPath, tvdbId);
-
-            Directory.CreateDirectory(seriesDataPath);
-
-            return FanArtTvProvider.Current.DownloadSeriesXml(seriesDataPath, tvdbId, cancellationToken);
-        }
-
         /// <summary>
         /// <summary>
         /// Dates the time to unix timestamp.
         /// Dates the time to unix timestamp.
         /// </summary>
         /// </summary>

+ 251 - 0
MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs

@@ -0,0 +1,251 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Providers.TV
+{
+    public class ManualFanartSeasonImageProvider : IImageProvider
+    {
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly IServerConfigurationManager _config;
+
+        public ManualFanartSeasonImageProvider(IServerConfigurationManager config)
+        {
+            _config = config;
+        }
+
+        public string Name
+        {
+            get { return ProviderName; }
+        }
+
+        public static string ProviderName
+        {
+            get { return "FanArt"; }
+        }
+
+        public bool Supports(BaseItem item)
+        {
+            return item is Season;
+        }
+
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken)
+        {
+            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
+
+            return images.Where(i => i.Type == imageType);
+        }
+
+        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken)
+        {
+            var list = new List<RemoteImageInfo>();
+
+            var series = ((Season)item).Series;
+
+            if (series != null)
+            {
+                var id = series.GetProviderId(MetadataProviders.Tvdb);
+
+                if (!string.IsNullOrEmpty(id) && item.IndexNumber.HasValue)
+                {
+                    var xmlPath = FanArtTvProvider.Current.GetFanartXmlPath(id);
+
+                    try
+                    {
+                        AddImages(list, item.IndexNumber.Value, xmlPath, cancellationToken);
+                    }
+                    catch (FileNotFoundException)
+                    {
+                        // No biggie. Don't blow up
+                    }
+                }
+            }
+
+            var language = _config.Configuration.PreferredMetadataLanguage;
+
+            var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+
+            // Sort first by width to prioritize HD versions
+            list = list.OrderByDescending(i => i.Width ?? 0)
+                .ThenByDescending(i =>
+                {
+                    if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return 3;
+                    }
+                    if (!isLanguageEn)
+                    {
+                        if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+                        {
+                            return 2;
+                        }
+                    }
+                    if (string.IsNullOrEmpty(i.Language))
+                    {
+                        return isLanguageEn ? 3 : 2;
+                    }
+                    return 0;
+                })
+                .ThenByDescending(i => i.CommunityRating ?? 0)
+                .ToList();
+
+            return Task.FromResult<IEnumerable<RemoteImageInfo>>(list);
+        }
+
+        private void AddImages(List<RemoteImageInfo> list, int seasonNumber, string xmlPath, CancellationToken cancellationToken)
+        {
+            using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
+            {
+                // Use XmlReader for best performance
+                using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
+                {
+                    CheckCharacters = false,
+                    IgnoreProcessingInstructions = true,
+                    IgnoreComments = true,
+                    ValidationType = ValidationType.None
+                }))
+                {
+                    reader.MoveToContent();
+
+                    // Loop through each element
+                    while (reader.Read())
+                    {
+                        cancellationToken.ThrowIfCancellationRequested();
+
+                        if (reader.NodeType == XmlNodeType.Element)
+                        {
+                            switch (reader.Name)
+                            {
+                                case "series":
+                                    {
+                                        using (var subReader = reader.ReadSubtree())
+                                        {
+                                            AddImages(list, subReader, seasonNumber, cancellationToken);
+                                        }
+                                        break;
+                                    }
+
+                                default:
+                                    reader.Skip();
+                                    break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private void AddImages(List<RemoteImageInfo> list, XmlReader reader, int seasonNumber, CancellationToken cancellationToken)
+        {
+            reader.MoveToContent();
+
+            while (reader.Read())
+            {
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "seasonthumbs":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Thumb, 500, 281, seasonNumber);
+                                }
+                                break;
+                            }
+                        case "showbackgrounds":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Backdrop, 1920, 1080, seasonNumber);
+                                }
+                                break;
+                            }
+                        default:
+                            {
+                                using (reader.ReadSubtree())
+                                {
+                                }
+                                break;
+                            }
+                    }
+                }
+            }
+        }
+
+        private void PopulateImageCategory(List<RemoteImageInfo> list, XmlReader reader, CancellationToken cancellationToken, ImageType type, int width, int height, int seasonNumber)
+        {
+            reader.MoveToContent();
+
+            while (reader.Read())
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "seasonthumb":
+                        case "showbackground":
+                            {
+                                var url = reader.GetAttribute("url");
+                                var season = reader.GetAttribute("season");
+
+                                int imageSeasonNumber;
+
+                                if (!string.IsNullOrEmpty(url) &&
+                                    !string.IsNullOrEmpty(season) &&
+                                    int.TryParse(season, NumberStyles.Any, _usCulture, out imageSeasonNumber) &&
+                                    seasonNumber == imageSeasonNumber)
+                                {
+                                    var likesString = reader.GetAttribute("likes");
+                                    int likes;
+
+                                    var info = new RemoteImageInfo
+                                    {
+                                        RatingType = RatingType.Likes,
+                                        Type = type,
+                                        Width = width,
+                                        Height = height,
+                                        ProviderName = Name,
+                                        Url = url,
+                                        Language = reader.GetAttribute("lang")
+                                    };
+
+                                    if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Any, _usCulture, out likes))
+                                    {
+                                        info.CommunityRating = likes;
+                                    }
+
+                                    list.Add(info);
+                                }
+
+                                break;
+                            }
+                        default:
+                            reader.Skip();
+                            break;
+                    }
+                }
+            }
+        }
+
+        public int Priority
+        {
+            get { return 1; }
+        }
+    }
+}

+ 309 - 0
MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs

@@ -0,0 +1,309 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Providers.TV
+{
+    public class ManualFanartSeriesImageProvider : IImageProvider
+    {
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        private readonly IServerConfigurationManager _config;
+
+        public ManualFanartSeriesImageProvider(IServerConfigurationManager config)
+        {
+            _config = config;
+        }
+
+        public string Name
+        {
+            get { return ProviderName; }
+        }
+
+        public static string ProviderName
+        {
+            get { return "FanArt"; }
+        }
+
+        public bool Supports(BaseItem item)
+        {
+            return item is Series;
+        }
+
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken)
+        {
+            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
+
+            return images.Where(i => i.Type == imageType);
+        }
+
+        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken)
+        {
+            var list = new List<RemoteImageInfo>();
+
+            var series = (Series)item;
+
+            var id = series.GetProviderId(MetadataProviders.Tvdb);
+
+            if (!string.IsNullOrEmpty(id))
+            {
+                var xmlPath = FanArtTvProvider.Current.GetFanartXmlPath(id);
+
+                try
+                {
+                    AddImages(list, xmlPath, cancellationToken);
+                }
+                catch (FileNotFoundException)
+                {
+                    // No biggie. Don't blow up
+                }
+            }
+
+            var language = _config.Configuration.PreferredMetadataLanguage;
+
+            var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
+
+            // Sort first by width to prioritize HD versions
+            list = list.OrderByDescending(i => i.Width ?? 0)
+                .ThenByDescending(i =>
+                {
+                    if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return 3;
+                    }
+                    if (!isLanguageEn)
+                    {
+                        if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+                        {
+                            return 2;
+                        }
+                    }
+                    if (string.IsNullOrEmpty(i.Language))
+                    {
+                        return isLanguageEn ? 3 : 2;
+                    }
+                    return 0;
+                })
+                .ThenByDescending(i => i.CommunityRating ?? 0)
+                .ToList();
+
+            return Task.FromResult<IEnumerable<RemoteImageInfo>>(list);
+        }
+
+        private void AddImages(List<RemoteImageInfo> list, string xmlPath, CancellationToken cancellationToken)
+        {
+            using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
+            {
+                // Use XmlReader for best performance
+                using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
+                {
+                    CheckCharacters = false,
+                    IgnoreProcessingInstructions = true,
+                    IgnoreComments = true,
+                    ValidationType = ValidationType.None
+                }))
+                {
+                    reader.MoveToContent();
+
+                    // Loop through each element
+                    while (reader.Read())
+                    {
+                        cancellationToken.ThrowIfCancellationRequested();
+
+                        if (reader.NodeType == XmlNodeType.Element)
+                        {
+                            switch (reader.Name)
+                            {
+                                case "series":
+                                    {
+                                        using (var subReader = reader.ReadSubtree())
+                                        {
+                                            AddImages(list, subReader, cancellationToken);
+                                        }
+                                        break;
+                                    }
+
+                                default:
+                                    reader.Skip();
+                                    break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        private void AddImages(List<RemoteImageInfo> list, XmlReader reader, CancellationToken cancellationToken)
+        {
+            reader.MoveToContent();
+
+            while (reader.Read())
+            {
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "hdtvlogos":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Logo, 800, 310);
+                                }
+                                break;
+                            }
+                        case "hdcleararts":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Art, 1000, 562);
+                                }
+                                break;
+                            }
+                        case "clearlogos":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Logo, 400, 155);
+                                }
+                                break;
+                            }
+                        case "cleararts":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Art, 500, 281);
+                                }
+                                break;
+                            }
+                        case "showbackgrounds":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Backdrop, 1920, 1080, true);
+                                }
+                                break;
+                            }
+                        case "seasonthumbs":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Thumb, 500, 281);
+                                }
+                                break;
+                            }
+                        case "tvthumbs":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Thumb, 500, 281);
+                                }
+                                break;
+                            }
+                        case "tvbanners":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Banner, 1000, 185);
+                                }
+                                break;
+                            }
+                        case "tvposters":
+                            {
+                                using (var subReader = reader.ReadSubtree())
+                                {
+                                    PopulateImageCategory(list, subReader, cancellationToken, ImageType.Primary, 1000, 1426);
+                                }
+                                break;
+                            }
+                        default:
+                            {
+                                using (reader.ReadSubtree())
+                                {
+                                }
+                                break;
+                            }
+                    }
+                }
+            }
+        }
+
+        private void PopulateImageCategory(List<RemoteImageInfo> list, XmlReader reader, CancellationToken cancellationToken, ImageType type, int width, int height, bool allowSeasonAll = false)
+        {
+            reader.MoveToContent();
+
+            while (reader.Read())
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
+                    {
+                        case "hdtvlogo":
+                        case "hdclearart":
+                        case "clearlogo":
+                        case "clearart":
+                        case "showbackground":
+                        case "seasonthumb":
+                        case "tvthumb":
+                        case "tvbanner":
+                        case "tvposter":
+                            {
+                                var url = reader.GetAttribute("url");
+                                var season = reader.GetAttribute("season");
+
+                                var isSeasonValid = string.IsNullOrEmpty(season) ||
+                                    (allowSeasonAll && string.Equals(season, "all", StringComparison.OrdinalIgnoreCase));
+
+                                if (!string.IsNullOrEmpty(url) && isSeasonValid)
+                                {
+                                    var likesString = reader.GetAttribute("likes");
+                                    int likes;
+
+                                    var info = new RemoteImageInfo
+                                    {
+                                        RatingType = RatingType.Likes,
+                                        Type = type,
+                                        Width = width,
+                                        Height = height,
+                                        ProviderName = Name,
+                                        Url = url,
+                                        Language = reader.GetAttribute("lang")
+                                    };
+
+                                    if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Any, _usCulture, out likes))
+                                    {
+                                        info.CommunityRating = likes;
+                                    }
+
+                                    list.Add(info);
+                                }
+
+                                break;
+                            }
+                        default:
+                            reader.Skip();
+                            break;
+                    }
+                }
+            }
+        }
+
+        public int Priority
+        {
+            get { return 1; }
+        }
+    }
+}

+ 166 - 0
MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs

@@ -0,0 +1,166 @@
+using System.Globalization;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Providers.TV
+{
+    public class ManualTvdbEpisodeImageProvider : IImageProvider
+    {
+        private readonly IServerConfigurationManager _config;
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        public ManualTvdbEpisodeImageProvider(IServerConfigurationManager config)
+        {
+            _config = config;
+        }
+
+        public string Name
+        {
+            get { return "TvDb"; }
+        }
+
+        public bool Supports(BaseItem item)
+        {
+            return item is Episode;
+        }
+
+        public Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken)
+        {
+            return GetAllImages(item, cancellationToken);
+        }
+
+        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(BaseItem item, CancellationToken cancellationToken)
+        {
+            var episode = (Episode)item;
+
+            var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+            if (!string.IsNullOrEmpty(seriesId))
+            {
+                // Process images
+                var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId);
+
+                var files = RemoteEpisodeProvider.Current.GetEpisodeXmlFiles(episode, seriesDataPath);
+
+                var result = files.Select(i => GetImageInfo(i, cancellationToken)).Where(i => i != null);
+
+                return Task.FromResult(result);
+            }
+
+            return Task.FromResult<IEnumerable<RemoteImageInfo>>(new RemoteImageInfo[] { });
+        }
+
+        private RemoteImageInfo GetImageInfo(FileInfo xmlFile, CancellationToken cancellationToken)
+        {
+            var height = 225;
+            var width = 400;
+            var url = string.Empty;
+
+            using (var streamReader = new StreamReader(xmlFile.FullName, Encoding.UTF8))
+            {
+                // Use XmlReader for best performance
+                using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings
+                {
+                    CheckCharacters = false,
+                    IgnoreProcessingInstructions = true,
+                    IgnoreComments = true,
+                    ValidationType = ValidationType.None
+                }))
+                {
+                    reader.MoveToContent();
+
+                    // Loop through each element
+                    while (reader.Read())
+                    {
+                        cancellationToken.ThrowIfCancellationRequested();
+
+                        if (reader.NodeType == XmlNodeType.Element)
+                        {
+                            switch (reader.Name)
+                            {
+                                case "thumb_width":
+                                    {
+                                        var val = reader.ReadElementContentAsString();
+
+                                        if (!string.IsNullOrWhiteSpace(val))
+                                        {
+                                            int rval;
+
+                                            // int.TryParse is local aware, so it can be probamatic, force us culture
+                                            if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
+                                            {
+                                                width = rval;
+                                            }
+                                        }
+                                        break;
+                                    }
+
+                                case "thumb_height":
+                                    {
+                                        var val = reader.ReadElementContentAsString();
+
+                                        if (!string.IsNullOrWhiteSpace(val))
+                                        {
+                                            int rval;
+
+                                            // int.TryParse is local aware, so it can be probamatic, force us culture
+                                            if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
+                                            {
+                                                height = rval;
+                                            }
+                                        }
+                                        break;
+                                    }
+
+                                case "filename":
+                                    {
+                                        var val = reader.ReadElementContentAsString();
+                                        if (!string.IsNullOrWhiteSpace(val))
+                                        {
+                                            url = TVUtils.BannerUrl + val;
+                                        }
+                                        break;
+                                    }
+
+                                default:
+                                    reader.Skip();
+                                    break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (string.IsNullOrEmpty(url))
+            {
+                return null;
+            }
+
+            return new RemoteImageInfo
+            {
+                Width = width,
+                Height = height,
+                ProviderName = Name,
+                Url = url,
+                Type = ImageType.Primary
+            };
+        }
+
+        public int Priority
+        {
+            get { return 0; }
+        }
+    }
+}

+ 4 - 1
MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs

@@ -39,6 +39,8 @@ namespace MediaBrowser.Providers.TV
         protected IHttpClient HttpClient { get; private set; }
         protected IHttpClient HttpClient { get; private set; }
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
 
 
+        internal static RemoteEpisodeProvider Current;
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="RemoteEpisodeProvider" /> class.
         /// Initializes a new instance of the <see cref="RemoteEpisodeProvider" /> class.
         /// </summary>
         /// </summary>
@@ -52,6 +54,7 @@ namespace MediaBrowser.Providers.TV
             HttpClient = httpClient;
             HttpClient = httpClient;
             _providerManager = providerManager;
             _providerManager = providerManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
+            Current = this;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -165,7 +168,7 @@ namespace MediaBrowser.Providers.TV
         /// <param name="episode">The episode.</param>
         /// <param name="episode">The episode.</param>
         /// <param name="seriesDataPath">The series data path.</param>
         /// <param name="seriesDataPath">The series data path.</param>
         /// <returns>List{FileInfo}.</returns>
         /// <returns>List{FileInfo}.</returns>
-        private List<FileInfo> GetEpisodeXmlFiles(Episode episode, string seriesDataPath)
+        internal List<FileInfo> GetEpisodeXmlFiles(Episode episode, string seriesDataPath)
         {
         {
             var files = new List<FileInfo>();
             var files = new List<FileInfo>();
 
 

+ 1 - 1
MediaBrowser.Providers/TV/RemoteSeasonProvider.cs

@@ -158,7 +158,7 @@ namespace MediaBrowser.Providers.TV
                 try
                 try
                 {
                 {
                     var fanartData = FetchFanartXmlData(imagesXmlPath, seasonNumber.Value, cancellationToken);
                     var fanartData = FetchFanartXmlData(imagesXmlPath, seasonNumber.Value, cancellationToken);
-                    await DownloadImages(item, fanartData, 1, cancellationToken).ConfigureAwait(false);
+                    await DownloadImages(item, fanartData, ConfigurationManager.Configuration.MaxBackdrops, cancellationToken).ConfigureAwait(false);
                 }
                 }
                 catch (FileNotFoundException)
                 catch (FileNotFoundException)
                 {
                 {

+ 27 - 1
MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs

@@ -361,7 +361,33 @@ namespace MediaBrowser.Server.Implementations.IO
             if (e.ChangeType == WatcherChangeTypes.Changed)
             if (e.ChangeType == WatcherChangeTypes.Changed)
             {
             {
                 // 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 => string.Equals(Path.GetDirectoryName(i), e.FullPath, StringComparison.OrdinalIgnoreCase) || string.Equals(i, e.FullPath, StringComparison.OrdinalIgnoreCase)))
+                if (tempIgnorePaths.Any(i =>
+                {
+                    if (string.Equals(i, e.FullPath, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return true;
+                    }
+
+                    // Go up a level
+                    var parent = Path.GetDirectoryName(i);
+                    if (string.Equals(parent, e.FullPath, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return true;
+                    }
+
+                    // Go up another level
+                    if (!string.IsNullOrEmpty(parent))
+                    {
+                        parent = Path.GetDirectoryName(i);
+                        if (string.Equals(parent, e.FullPath, StringComparison.OrdinalIgnoreCase))
+                        {
+                            return true;
+                        }
+                    }
+
+                    return false;
+
+                }))
                 {
                 {
                     return;
                     return;
                 }
                 }

+ 3 - 1
MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs

@@ -1,4 +1,4 @@
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Common.MediaInfo;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.IO;
@@ -602,6 +602,8 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
                     return "-sub_charenc windows-1251";
                     return "-sub_charenc windows-1251";
                 case "vie":
                 case "vie":
                     return "-sub_charenc windows-1258";
                     return "-sub_charenc windows-1258";
+                case "kor":
+                		return "-sub_charenc cp949";
                 default:
                 default:
                     return "-sub_charenc windows-1252";
                     return "-sub_charenc windows-1252";
             }
             }

+ 1 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs

@@ -80,7 +80,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
         {
         {
             var dbFile = Path.Combine(_appPaths.DataPath, "displaypreferences.db");
             var dbFile = Path.Combine(_appPaths.DataPath, "displaypreferences.db");
 
 
-            _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false);
+            _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false);
 
 
             string[] queries = {
             string[] queries = {
 
 

+ 4 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs

@@ -128,15 +128,18 @@ namespace MediaBrowser.Server.Implementations.Persistence
         /// Connects to db.
         /// Connects to db.
         /// </summary>
         /// </summary>
         /// <param name="dbPath">The db path.</param>
         /// <param name="dbPath">The db path.</param>
+        /// <param name="logger">The logger.</param>
         /// <returns>Task{IDbConnection}.</returns>
         /// <returns>Task{IDbConnection}.</returns>
         /// <exception cref="System.ArgumentNullException">dbPath</exception>
         /// <exception cref="System.ArgumentNullException">dbPath</exception>
-        public static async Task<IDbConnection> ConnectToDb(string dbPath)
+        public static async Task<IDbConnection> ConnectToDb(string dbPath, ILogger logger)
         {
         {
             if (string.IsNullOrEmpty(dbPath))
             if (string.IsNullOrEmpty(dbPath))
             {
             {
                 throw new ArgumentNullException("dbPath");
                 throw new ArgumentNullException("dbPath");
             }
             }
 
 
+            logger.Info("Opening {0}", dbPath);
+
 			#if __MonoCS__
 			#if __MonoCS__
 			var connectionstr = new SqliteConnectionStringBuilder
 			var connectionstr = new SqliteConnectionStringBuilder
 			{
 			{

+ 2 - 2
MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs

@@ -91,7 +91,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
 
             var chapterDbFile = Path.Combine(_appPaths.DataPath, "chapters.db");
             var chapterDbFile = Path.Combine(_appPaths.DataPath, "chapters.db");
 
 
-            var chapterConnection = SqliteExtensions.ConnectToDb(chapterDbFile).Result;
+            var chapterConnection = SqliteExtensions.ConnectToDb(chapterDbFile, _logger).Result;
 
 
             _chapterRepository = new SqliteChapterRepository(chapterConnection, logManager);
             _chapterRepository = new SqliteChapterRepository(chapterConnection, logManager);
         }
         }
@@ -104,7 +104,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
         {
         {
             var dbFile = Path.Combine(_appPaths.DataPath, "library.db");
             var dbFile = Path.Combine(_appPaths.DataPath, "library.db");
 
 
-            _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false);
+            _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false);
 
 
             string[] queries = {
             string[] queries = {
 
 

+ 1 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteNotificationsRepository.cs

@@ -37,7 +37,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
         {
         {
             var dbFile = Path.Combine(_appPaths.DataPath, "notifications.db");
             var dbFile = Path.Combine(_appPaths.DataPath, "notifications.db");
 
 
-            _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false);
+            _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false);
             
             
             string[] queries = {
             string[] queries = {
 
 

+ 1 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs

@@ -73,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
         {
         {
             var dbFile = Path.Combine(_appPaths.DataPath, "userdata_v2.db");
             var dbFile = Path.Combine(_appPaths.DataPath, "userdata_v2.db");
 
 
-            _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false);
+            _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false);
 
 
             string[] queries = {
             string[] queries = {
 
 

+ 1 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs

@@ -70,7 +70,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
         {
         {
             var dbFile = Path.Combine(_appPaths.DataPath, "users.db");
             var dbFile = Path.Combine(_appPaths.DataPath, "users.db");
 
 
-            _connection = await SqliteExtensions.ConnectToDb(dbFile).ConfigureAwait(false);
+            _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false);
             
             
             string[] queries = {
             string[] queries = {
 
 

+ 27 - 8
MediaBrowser.Server.Implementations/Providers/ImageSaver.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using MediaBrowser.Common.IO;
 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;
@@ -69,11 +70,11 @@ namespace MediaBrowser.Server.Implementations.Providers
                 throw new ArgumentNullException("mimeType");
                 throw new ArgumentNullException("mimeType");
             }
             }
 
 
-            var saveLocally = _config.Configuration.SaveLocalMeta || item is IItemByName || item is User;
+            var saveLocally = _config.Configuration.SaveLocalMeta && item.Parent != null && !(item is Audio);
 
 
-            if (item is Audio || item.Parent == null)
+            if (item is IItemByName || item is User)
             {
             {
-                saveLocally = false;
+                saveLocally = true;
             }
             }
 
 
             if (type != ImageType.Primary && item is Episode)
             if (type != ImageType.Primary && item is Episode)
@@ -268,7 +269,7 @@ namespace MediaBrowser.Server.Implementations.Providers
                     {
                     {
                         item.ScreenshotImagePaths[imageIndex.Value] = path;
                         item.ScreenshotImagePaths[imageIndex.Value] = path;
                     }
                     }
-                    else
+                    else if (!item.ScreenshotImagePaths.Contains(path, StringComparer.OrdinalIgnoreCase))
                     {
                     {
                         item.ScreenshotImagePaths.Add(path);
                         item.ScreenshotImagePaths.Add(path);
                     }
                     }
@@ -282,7 +283,7 @@ namespace MediaBrowser.Server.Implementations.Providers
                     {
                     {
                         item.BackdropImagePaths[imageIndex.Value] = path;
                         item.BackdropImagePaths[imageIndex.Value] = path;
                     }
                     }
-                    else
+                    else if (!item.BackdropImagePaths.Contains(path, StringComparer.OrdinalIgnoreCase))
                     {
                     {
                         item.BackdropImagePaths.Add(path);
                         item.BackdropImagePaths.Add(path);
                     }
                     }
@@ -333,14 +334,14 @@ namespace MediaBrowser.Server.Implementations.Providers
                     {
                     {
                         throw new ArgumentNullException("imageIndex");
                         throw new ArgumentNullException("imageIndex");
                     }
                     }
-                    filename = imageIndex.Value == 0 ? "backdrop" : "backdrop" + imageIndex.Value.ToString(UsCulture);
+                    filename = GetBackdropSaveFilename(item.BackdropImagePaths, "backdrop", "backdrop", imageIndex.Value);
                     break;
                     break;
                 case ImageType.Screenshot:
                 case ImageType.Screenshot:
                     if (!imageIndex.HasValue)
                     if (!imageIndex.HasValue)
                     {
                     {
                         throw new ArgumentNullException("imageIndex");
                         throw new ArgumentNullException("imageIndex");
                     }
                     }
-                    filename = imageIndex.Value == 0 ? "screenshot" : "screenshot" + imageIndex.Value.ToString(UsCulture);
+                    filename = GetBackdropSaveFilename(item.ScreenshotImagePaths, "screenshot", "screenshot", imageIndex.Value);
                     break;
                     break;
                 default:
                 default:
                     filename = type.ToString().ToLower();
                     filename = type.ToString().ToLower();
@@ -380,6 +381,24 @@ namespace MediaBrowser.Server.Implementations.Providers
             return path;
             return path;
         }
         }
 
 
+        private string GetBackdropSaveFilename(List<string> images, string zeroIndexFilename, string numberedIndexPrefix, int index)
+        {
+            var filesnames = images.Select(Path.GetFileNameWithoutExtension).ToList();
+
+            if (index == 0)
+            {
+                return zeroIndexFilename;
+            }
+
+            var current = index;
+            while (filesnames.Contains(numberedIndexPrefix + current.ToString(UsCulture), StringComparer.OrdinalIgnoreCase))
+            {
+                current++;
+            }
+
+            return numberedIndexPrefix + current.ToString(UsCulture);
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the compatible save paths.
         /// Gets the compatible save paths.
         /// </summary>
         /// </summary>

+ 40 - 13
MediaBrowser.Server.Implementations/Providers/ProviderManager.cs

@@ -7,13 +7,13 @@ using MediaBrowser.Controller.Library;
 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.Providers;
 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 MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Server.Implementations.Providers
 namespace MediaBrowser.Server.Implementations.Providers
 {
 {
@@ -77,7 +77,7 @@ namespace MediaBrowser.Server.Implementations.Providers
         {
         {
             MetadataProviders = providers.OrderBy(e => e.Priority).ToArray();
             MetadataProviders = providers.OrderBy(e => e.Priority).ToArray();
 
 
-            ImageProviders = imageProviders.ToArray();
+            ImageProviders = imageProviders.OrderByDescending(i => i.Priority).ToArray();
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -356,52 +356,79 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// Gets the available remote images.
         /// Gets the available remote images.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <param name="type">The type.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
+        /// <param name="providerName">Name of the provider.</param>
+        /// <param name="type">The type.</param>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
         /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
-        public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, ImageType type, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, CancellationToken cancellationToken, string providerName = null, ImageType? type = null)
         {
         {
-            var providers = GetSupportedImageProviders(item, type);
+            var providers = GetImageProviders(item);
+
+            if (!string.IsNullOrEmpty(providerName))
+            {
+                providers = providers.Where(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
+            }
+
+            var preferredLanguage = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 
 
             var tasks = providers.Select(i => Task.Run(async () =>
             var tasks = providers.Select(i => Task.Run(async () =>
             {
             {
                 try
                 try
                 {
                 {
-                    var result = await i.GetAvailableImages(item, type, cancellationToken).ConfigureAwait(false);
-                    return result.ToList();
+                    if (type.HasValue)
+                    {
+                        var result = await i.GetImages(item, type.Value, cancellationToken).ConfigureAwait(false);
+
+                        return FilterImages(result, preferredLanguage);
+                    }
+                    else
+                    {
+                        var result = await i.GetAllImages(item, cancellationToken).ConfigureAwait(false);
+                        return FilterImages(result, preferredLanguage);
+                    }
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
-                    _logger.ErrorException("{0} failed in GetAvailableImages for type {1}", ex, i.GetType().Name, item.GetType().Name);
+                    _logger.ErrorException("{0} failed in GetImages for type {1}", ex, i.GetType().Name, item.GetType().Name);
                     return new List<RemoteImageInfo>();
                     return new List<RemoteImageInfo>();
                 }
                 }
-            }));
+
+            }, cancellationToken));
 
 
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
             var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 
 
             return results.SelectMany(i => i);
             return results.SelectMany(i => i);
         }
         }
 
 
+        private IEnumerable<RemoteImageInfo> FilterImages(IEnumerable<RemoteImageInfo> images, string preferredLanguage)
+        {
+            if (string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase))
+            {
+                images = images.Where(i => string.IsNullOrEmpty(i.Language) ||
+                                           string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase));
+            }
+
+            return images;
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the supported image providers.
         /// Gets the supported image providers.
         /// </summary>
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="item">The item.</param>
-        /// <param name="type">The type.</param>
         /// <returns>IEnumerable{IImageProvider}.</returns>
         /// <returns>IEnumerable{IImageProvider}.</returns>
-        private IEnumerable<IImageProvider> GetSupportedImageProviders(BaseItem item, ImageType type)
+        public IEnumerable<IImageProvider> GetImageProviders(BaseItem item)
         {
         {
             return ImageProviders.Where(i =>
             return ImageProviders.Where(i =>
             {
             {
                 try
                 try
                 {
                 {
-                    return i.Supports(item, type);
+                    return i.Supports(item);
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
                     _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;
                 }
                 }
-
             });
             });
         }
         }
     }
     }

+ 8 - 1
MediaBrowser.Server.Implementations/Sorting/PremiereDateComparer.cs

@@ -35,7 +35,14 @@ namespace MediaBrowser.Server.Implementations.Sorting
             
             
             if (x.ProductionYear.HasValue)
             if (x.ProductionYear.HasValue)
             {
             {
-                return new DateTime(x.ProductionYear.Value, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+                try
+                {
+                    return new DateTime(x.ProductionYear.Value, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+                }
+                catch (ArgumentOutOfRangeException)
+                {
+                    // Don't blow up if the item has a bad ProductionYear, just return MinValue
+                }
             }
             }
             return DateTime.MinValue;
             return DateTime.MinValue;
         }
         }

+ 58 - 7
MediaBrowser.WebDashboard/ApiClient.js

@@ -305,17 +305,52 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
                 url: url
                 url: url
             });
             });
         };
         };
+        
+        function getRemoteImagePrefix(options) {
+            
+            var urlPrefix;
 
 
-        self.getAvailableRemoteImages = function (itemId, imageType) {
-
-            if (!itemId) {
-                throw new Error("null itemId");
+            if (options.artist) {
+                urlPrefix = "Artists/" + encodeName(options.artist);
+                delete options.artist;
             }
             }
-            if (!imageType) {
-                throw new Error("null imageType");
+            else if (options.person) {
+                urlPrefix = "Persons/" + encodeName(options.person);
+                delete options.person;
+            }
+            else if (options.genre) {
+                urlPrefix = "Genres/" + encodeName(options.genre);
+                delete options.genre;
+            }
+            else if (options.musicGenre) {
+                urlPrefix = "MusicGenres/" + encodeName(options.musicGenre);
+                delete options.musicGenre;
+            }
+            else if (options.gameGenre) {
+                urlPrefix = "GameGenres/" + encodeName(options.gameGenre);
+                delete options.gameGenre;
+            }
+            else if (options.studio) {
+                urlPrefix = "Studios/" + encodeName(options.studio);
+                delete options.studio;
+            }
+            else {
+                urlPrefix = "Items/" + options.itemId;
+                delete options.itemId;
+            }
+
+            return urlPrefix;
+        }
+
+        self.getAvailableRemoteImages = function (options) {
+
+            if (!options) {
+                throw new Error("null options");
             }
             }
 
 
-            var url = self.getUrl("Items/" + itemId + "/RemoteImages/" + imageType);
+            var urlPrefix = getRemoteImagePrefix(options);
+
+            var url = self.getUrl(urlPrefix + "/RemoteImages", options);
 
 
             return self.ajax({
             return self.ajax({
                 type: "GET",
                 type: "GET",
@@ -324,6 +359,22 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi
             });
             });
         };
         };
 
 
+        self.downloadRemoteImage = function (options) {
+
+            if (!options) {
+                throw new Error("null options");
+            }
+
+            var urlPrefix = getRemoteImagePrefix(options);
+
+            var url = self.getUrl(urlPrefix + "/RemoteImages/Download", options);
+
+            return self.ajax({
+                type: "POST",
+                url: url
+            });
+        };
+
         /**
         /**
          * Gets the current server status
          * Gets the current server status
          */
          */

+ 1 - 1
MediaBrowser.WebDashboard/packages.config

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
 <packages>
-  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.183" targetFramework="net45" />
+  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.187" targetFramework="net45" />
   <package id="ServiceStack.Common" version="3.9.62" targetFramework="net45" />
   <package id="ServiceStack.Common" version="3.9.62" targetFramework="net45" />
   <package id="ServiceStack.Text" version="3.9.62" targetFramework="net45" />
   <package id="ServiceStack.Text" version="3.9.62" targetFramework="net45" />
 </packages>
 </packages>