Browse Source

tightened up image saving to reduce knowledge of file names

Luke Pulverenti 12 years ago
parent
commit
8a1b12b7d8
25 changed files with 576 additions and 402 deletions
  1. 16 116
      MediaBrowser.Api/Images/ImageService.cs
  2. 86 0
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
  3. 0 46
      MediaBrowser.Common.Implementations/HttpClientManager/HttpResponseInfo.cs
  4. 0 1
      MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj
  5. 1 0
      MediaBrowser.Common/MediaBrowser.Common.csproj
  6. 29 0
      MediaBrowser.Common/Net/HttpResponseInfo.cs
  7. 7 0
      MediaBrowser.Common/Net/IHttpClient.cs
  8. 3 0
      MediaBrowser.Controller/Entities/Folder.cs
  9. 21 37
      MediaBrowser.Controller/Providers/IProviderManager.cs
  10. 11 9
      MediaBrowser.Providers/Movies/FanArtMovieProvider.cs
  11. 8 5
      MediaBrowser.Providers/Movies/MovieDbImagesProvider.cs
  12. 20 38
      MediaBrowser.Providers/Movies/MovieDbProvider.cs
  13. 7 16
      MediaBrowser.Providers/Movies/TmdbPersonProvider.cs
  14. 4 2
      MediaBrowser.Providers/Music/FanArtAlbumProvider.cs
  15. 10 5
      MediaBrowser.Providers/Music/FanArtArtistProvider.cs
  16. 9 6
      MediaBrowser.Providers/Savers/SeriesXmlSaver.cs
  17. 2 1
      MediaBrowser.Providers/TV/FanArtSeasonProvider.cs
  18. 10 5
      MediaBrowser.Providers/TV/FanArtTVProvider.cs
  19. 4 1
      MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs
  20. 15 11
      MediaBrowser.Providers/TV/RemoteSeasonProvider.cs
  21. 0 7
      MediaBrowser.Providers/TV/RemoteSeriesProvider.cs
  22. 11 6
      MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs
  23. 1 0
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  24. 255 0
      MediaBrowser.Server.Implementations/Providers/ImageSaver.cs
  25. 46 90
      MediaBrowser.Server.Implementations/Providers/ProviderManager.cs

+ 16 - 116
MediaBrowser.Api/Images/ImageService.cs

@@ -760,145 +760,45 @@ namespace MediaBrowser.Api.Images
                 var bytes = Convert.FromBase64String(text);
 
                 // Validate first
-                using (var memoryStream = new MemoryStream(bytes))
+                using (var validationStream = new MemoryStream(bytes))
                 {
-                    using (var image = Image.FromStream(memoryStream))
+                    using (var image = Image.FromStream(validationStream))
                     {
                         Logger.Info("New image is {0}x{1}", image.Width, image.Height);
                     }
                 }
 
-                string filename;
+                var memoryStream = new MemoryStream(bytes);
 
-                switch (imageType)
-                {
-                    case ImageType.Art:
-                        filename = "clearart";
-                        break;
-                    case ImageType.Primary:
-                        filename = entity is Episode ? Path.GetFileNameWithoutExtension(entity.Path) : "folder";
-                        break;
-                    case ImageType.Backdrop:
-                        filename = GetBackdropFilenameToSave(entity);
-                        break;
-                    case ImageType.Screenshot:
-                        filename = GetScreenshotFilenameToSave(entity);
-                        break;
-                    default:
-                        filename = imageType.ToString().ToLower();
-                        break;
-                }
+                memoryStream.Position = 0;
 
+                var imageIndex = 0;
 
-                var extension = mimeType.Split(';').First().Split('/').Last();
-
-                string oldImagePath;
-                switch (imageType)
+                if (imageType == ImageType.Screenshot)
                 {
-                    case ImageType.Backdrop:
-                    case ImageType.Screenshot:
-                        oldImagePath = null;
-                        break;
-                    default:
-                        oldImagePath = entity.GetImage(imageType);
-                        break;
+                    imageIndex = entity.ScreenshotImagePaths.Count;
                 }
-
-                // Don't save locally if there's no parent (special feature, trailer, etc)
-                var saveLocally = !(entity is Audio) && entity.Parent != null && !string.IsNullOrEmpty(entity.MetaLocation) || entity is User;
-
-                if (imageType != ImageType.Primary)
+                else if (imageType == ImageType.Backdrop)
                 {
-                    if (entity is Episode)
-                    {
-                        saveLocally = false;
-                    }
+                    imageIndex = entity.BackdropImagePaths.Count;
                 }
 
-                if (entity.LocationType != LocationType.FileSystem)
-                {
-                    saveLocally = false;
-                }
+                await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, imageIndex, CancellationToken.None).ConfigureAwait(false);
 
-                var imagePath = _providerManager.GetSavePath(entity, filename + "." + extension, saveLocally);
+                var user = entity as User;
 
-                // Save to file system
-                using (var fs = new FileStream(imagePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true))
+                if (user != null)
                 {
-                    await fs.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
-                }
-
-                if (imageType == ImageType.Screenshot)
-                {
-                    entity.ScreenshotImagePaths.Add(imagePath);
-                }
-                else if (imageType == ImageType.Backdrop)
-                {
-                    entity.BackdropImagePaths.Add(imagePath);
+                    await _userManager.UpdateUser(user).ConfigureAwait(false);
                 }
                 else
                 {
-                    // Set the image
-                    entity.SetImage(imageType, imagePath);
-                }
-
-                // If the new and old paths are different, delete the old one
-                if (!string.IsNullOrEmpty(oldImagePath) && !oldImagePath.Equals(imagePath, StringComparison.OrdinalIgnoreCase))
-                {
-                    File.Delete(oldImagePath);
+                    await _libraryManager.UpdateItem(entity, ItemUpdateType.ImageUpdate, CancellationToken.None)
+                                       .ConfigureAwait(false);
                 }
 
-                // Directory watchers should repeat this, but do a quick refresh first
-                await entity.RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false).ConfigureAwait(false);
+                await entity.RefreshMetadata(CancellationToken.None, allowSlowProviders: false).ConfigureAwait(false);
             }
         }
-
-        /// <summary>
-        /// Gets the backdrop filename to save.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>System.String.</returns>
-        private string GetBackdropFilenameToSave(BaseItem item)
-        {
-            var paths = item.BackdropImagePaths.ToList();
-
-            if (!paths.Any(i => string.Equals(Path.GetFileNameWithoutExtension(i), "backdrop", StringComparison.OrdinalIgnoreCase)))
-            {
-                return "screenshot";
-            }
-
-            var index = 1;
-
-            while (paths.Any(i => string.Equals(Path.GetFileNameWithoutExtension(i), "backdrop" + index, StringComparison.OrdinalIgnoreCase)))
-            {
-                index++;
-            }
-
-            return "backdrop" + index;
-        }
-
-        /// <summary>
-        /// Gets the screenshot filename to save.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>System.String.</returns>
-        private string GetScreenshotFilenameToSave(BaseItem item)
-        {
-            var paths = item.ScreenshotImagePaths.ToList();
-
-            if (!paths.Any(i => string.Equals(Path.GetFileNameWithoutExtension(i), "screenshot", StringComparison.OrdinalIgnoreCase)))
-            {
-                return "screenshot";
-            }
-
-            var index = 1;
-
-            while (paths.Any(i => string.Equals(Path.GetFileNameWithoutExtension(i), "screenshot" + index, StringComparison.OrdinalIgnoreCase)))
-            {
-                index++;
-            }
-
-            return "screenshot" + index;
-        }
     }
 }

+ 86 - 0
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -104,6 +104,92 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             return client;
         }
 
+        public async Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
+        {
+            ValidateParams(options.Url, options.CancellationToken);
+
+            options.CancellationToken.ThrowIfCancellationRequested();
+
+            var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
+
+            if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < 30)
+            {
+                throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url)) { IsTimedOut = true };
+            }
+
+            using (var message = GetHttpRequestMessage(options))
+            {
+                if (options.ResourcePool != null)
+                {
+                    await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
+                }
+
+                if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < 30)
+                {
+                    if (options.ResourcePool != null)
+                    {
+                        options.ResourcePool.Release();
+                    }
+
+                    throw new HttpException(string.Format("Connection to {0} timed out", options.Url)) { IsTimedOut = true };
+                }
+
+                _logger.Info("HttpClientManager.Get url: {0}", options.Url);
+
+                try
+                {
+                    options.CancellationToken.ThrowIfCancellationRequested();
+
+                    var response = await client.HttpClient.SendAsync(message, HttpCompletionOption.ResponseContentRead, options.CancellationToken).ConfigureAwait(false);
+
+                    EnsureSuccessStatusCode(response);
+
+                    options.CancellationToken.ThrowIfCancellationRequested();
+
+                    return new HttpResponseInfo
+                    {
+                        Content = await response.Content.ReadAsStreamAsync().ConfigureAwait(false),
+
+                        StatusCode = response.StatusCode,
+
+                        ContentType = response.Content.Headers.ContentType.MediaType
+                    };
+                }
+                catch (OperationCanceledException ex)
+                {
+                    var exception = GetCancellationException(options.Url, options.CancellationToken, ex);
+
+                    var httpException = exception as HttpException;
+
+                    if (httpException != null && httpException.IsTimedOut)
+                    {
+                        client.LastTimeout = DateTime.UtcNow;
+                    }
+
+                    throw exception;
+                }
+                catch (HttpRequestException ex)
+                {
+                    _logger.ErrorException("Error getting response from " + options.Url, ex);
+
+                    throw new HttpException(ex.Message, ex);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error getting response from " + options.Url, ex);
+
+                    throw;
+                }
+                finally
+                {
+                    if (options.ResourcePool != null)
+                    {
+                        options.ResourcePool.Release();
+                    }
+                }
+            }
+        }
+
         /// <summary>
         /// Performs a GET request and returns the resulting stream
         /// </summary>

+ 0 - 46
MediaBrowser.Common.Implementations/HttpClientManager/HttpResponseInfo.cs

@@ -1,46 +0,0 @@
-using System;
-
-namespace MediaBrowser.Common.Implementations.HttpClientManager
-{
-    /// <summary>
-    /// Class HttpResponseOutput
-    /// </summary>
-    public class HttpResponseInfo
-    {
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-
-        /// <summary>
-        /// Gets or sets the etag.
-        /// </summary>
-        /// <value>The etag.</value>
-        public string Etag { get; set; }
-
-        /// <summary>
-        /// Gets or sets the last modified.
-        /// </summary>
-        /// <value>The last modified.</value>
-        public DateTime? LastModified { get; set; }
-
-        /// <summary>
-        /// Gets or sets the expires.
-        /// </summary>
-        /// <value>The expires.</value>
-        public DateTime? Expires { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether [must revalidate].
-        /// </summary>
-        /// <value><c>true</c> if [must revalidate]; otherwise, <c>false</c>.</value>
-        public bool MustRevalidate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the request date.
-        /// </summary>
-        /// <value>The request date.</value>
-        public DateTime RequestDate { get; set; }
-    }
-}

+ 0 - 1
MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj

@@ -66,7 +66,6 @@
     <Compile Include="Configuration\BaseConfigurationManager.cs" />
     <Compile Include="HttpClientManager\HttpClientInfo.cs" />
     <Compile Include="HttpClientManager\HttpClientManager.cs" />
-    <Compile Include="HttpClientManager\HttpResponseInfo.cs" />
     <Compile Include="Logging\LogHelper.cs" />
     <Compile Include="Logging\NLogger.cs" />
     <Compile Include="Logging\NlogManager.cs" />

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

@@ -74,6 +74,7 @@
     <Compile Include="Net\BasePeriodicWebSocketListener.cs" />
     <Compile Include="Configuration\IApplicationPaths.cs" />
     <Compile Include="Net\HttpRequestOptions.cs" />
+    <Compile Include="Net\HttpResponseInfo.cs" />
     <Compile Include="Net\IHasResultFactory.cs" />
     <Compile Include="Net\IHttpResultFactory.cs" />
     <Compile Include="Net\IServerManager.cs" />

+ 29 - 0
MediaBrowser.Common/Net/HttpResponseInfo.cs

@@ -0,0 +1,29 @@
+using System.IO;
+using System.Net;
+
+namespace MediaBrowser.Common.Net
+{
+    /// <summary>
+    /// Class HttpResponseInfo
+    /// </summary>
+    public class HttpResponseInfo
+    {
+        /// <summary>
+        /// Gets or sets the type of the content.
+        /// </summary>
+        /// <value>The type of the content.</value>
+        public string ContentType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the content.
+        /// </summary>
+        /// <value>The content.</value>
+        public Stream Content { get; set; }
+
+        /// <summary>
+        /// Gets or sets the status code.
+        /// </summary>
+        /// <value>The status code.</value>
+        public HttpStatusCode StatusCode { get; set; }
+    }
+}

+ 7 - 0
MediaBrowser.Common/Net/IHttpClient.cs

@@ -11,6 +11,13 @@ namespace MediaBrowser.Common.Net
     /// </summary>
     public interface IHttpClient : IDisposable
     {
+        /// <summary>
+        /// Gets the response.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <returns>Task{HttpResponseInfo}.</returns>
+        Task<HttpResponseInfo> GetResponse(HttpRequestOptions options);
+
         /// <summary>
         /// Performs a GET request and returns the resulting stream
         /// </summary>

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

@@ -798,6 +798,9 @@ namespace MediaBrowser.Controller.Entities
                         });
 
                         await ((Folder)child).ValidateChildren(innerProgress, cancellationToken, recursive, forceRefreshMetadata).ConfigureAwait(false);
+
+                        // Some folder providers are unable to refresh until children have been refreshed.
+                        await child.RefreshMetadata(cancellationToken, resetResolveArgs: false).ConfigureAwait(false);
                     }
                     else
                     {

+ 21 - 37
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading;
@@ -12,64 +13,47 @@ namespace MediaBrowser.Controller.Providers
     /// </summary>
     public interface IProviderManager
     {
-        /// <summary>
-        /// Downloads the and save image.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="source">The source.</param>
-        /// <param name="targetName">Name of the target.</param>
-        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
-        /// <param name="resourcePool">The resource pool.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{System.String}.</returns>
-        /// <exception cref="System.ArgumentNullException">item</exception>
-        Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, bool saveLocally, SemaphoreSlim resourcePool, CancellationToken cancellationToken);
+        Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, bool saveLocally,
+                                          SemaphoreSlim resourcePool, CancellationToken cancellationToken);
 
         /// <summary>
-        /// Saves the image.
+        /// Executes the metadata providers.
         /// </summary>
         /// <param name="item">The item.</param>
-        /// <param name="source">The source.</param>
-        /// <param name="targetName">Name of the target.</param>
-        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{System.String}.</returns>
-        Task<string> SaveImage(BaseItem item, Stream source, string targetName, bool saveLocally, CancellationToken cancellationToken);
+        /// <param name="force">if set to <c>true</c> [force].</param>
+        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+        /// <returns>Task{System.Boolean}.</returns>
+        Task<ItemUpdateType?> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false, bool allowSlowProviders = true);
 
         /// <summary>
-        /// Saves to library filesystem.
+        /// Saves the image.
         /// </summary>
         /// <param name="item">The item.</param>
-        /// <param name="path">The path.</param>
-        /// <param name="dataToSave">The data to save.</param>
+        /// <param name="url">The URL.</param>
+        /// <param name="resourcePool">The resource pool.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        /// <exception cref="System.ArgumentNullException"></exception>
-        Task SaveToLibraryFilesystem(BaseItem item, string path, Stream dataToSave, CancellationToken cancellationToken);
+        Task SaveImage(BaseItem item, string url, SemaphoreSlim resourcePool, ImageType type, int? imageIndex, CancellationToken cancellationToken);
 
         /// <summary>
-        /// Executes the metadata providers.
+        /// Saves the image.
         /// </summary>
         /// <param name="item">The item.</param>
+        /// <param name="source">The source.</param>
+        /// <param name="mimeType">Type of the MIME.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <param name="force">if set to <c>true</c> [force].</param>
-        /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
-        /// <returns>Task{System.Boolean}.</returns>
-        Task<ItemUpdateType?> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false, bool allowSlowProviders = true);
+        /// <returns>Task.</returns>
+        Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken);
 
         /// <summary>
         /// Adds the metadata providers.
         /// </summary>
         /// <param name="providers">The providers.</param>
         void AddParts(IEnumerable<BaseMetadataProvider> providers);
-
-        /// <summary>
-        /// Gets the save path.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="targetFileName">Name of the target file.</param>
-        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
-        /// <returns>System.String.</returns>
-        string GetSavePath(BaseItem item, string targetFileName, bool saveLocally);
     }
 }

+ 11 - 9
MediaBrowser.Providers/Movies/FanArtMovieProvider.cs

@@ -303,9 +303,6 @@ namespace MediaBrowser.Providers.Movies
             
             cancellationToken.ThrowIfCancellationRequested();
 
-            var saveLocal = ConfigurationManager.Configuration.SaveLocalMeta &&
-                            item.LocationType == LocationType.FileSystem;
-
             string path;
             var hd = ConfigurationManager.Configuration.DownloadHDFanArt ? "hd" : "";
 
@@ -322,7 +319,7 @@ namespace MediaBrowser.Providers.Movies
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(item, path, LogoFile, saveLocal, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Logo, null, cancellationToken).ConfigureAwait(false);
                 }
             }
             cancellationToken.ThrowIfCancellationRequested();
@@ -337,7 +334,8 @@ namespace MediaBrowser.Providers.Movies
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(item, path, ArtFile, saveLocal, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Art, null, cancellationToken)
+                                        .ConfigureAwait(false);
                 }
             }
             cancellationToken.ThrowIfCancellationRequested();
@@ -349,7 +347,8 @@ namespace MediaBrowser.Providers.Movies
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Disc, await _providerManager.DownloadAndSaveImage(item, path, DiscFile, saveLocal, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Disc, null, cancellationToken)
+                                        .ConfigureAwait(false);
                 }
             }
 
@@ -362,7 +361,8 @@ namespace MediaBrowser.Providers.Movies
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(item, path, BannerFile, saveLocal, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Banner, null, cancellationToken)
+                                        .ConfigureAwait(false);
                 }
             }
 
@@ -375,7 +375,8 @@ namespace MediaBrowser.Providers.Movies
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Thumb, await _providerManager.DownloadAndSaveImage(item, path, ThumbFile, saveLocal, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Thumb, null, cancellationToken)
+                                        .ConfigureAwait(false);
                 }
             }
 
@@ -393,7 +394,8 @@ namespace MediaBrowser.Providers.Movies
 
                         if (!string.IsNullOrEmpty(path))
                         {
-                            item.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(item, path, ("backdrop" + (numBackdrops > 0 ? numBackdrops.ToString(UsCulture) : "") + ".jpg"), saveLocal, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                            await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
+                                                .ConfigureAwait(false);
 
                             numBackdrops++;
 

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

@@ -93,7 +93,7 @@ namespace MediaBrowser.Providers.Movies
                 return ItemUpdateType.ImageUpdate;
             }
         }
-        
+
         /// <summary>
         /// Gets a value indicating whether [requires internet].
         /// </summary>
@@ -148,7 +148,7 @@ namespace MediaBrowser.Providers.Movies
             {
                 return false;
             }
-            
+
             return base.NeedsRefreshInternal(item, providerInfo);
         }
 
@@ -168,7 +168,7 @@ namespace MediaBrowser.Providers.Movies
                 data = new BaseProviderInfo();
                 item.ProviderData[Id] = data;
             }
-            
+
             var images = await FetchImages(item, item.GetProviderId(MetadataProviders.Tmdb), cancellationToken).ConfigureAwait(false);
 
             var status = await ProcessImages(item, images, cancellationToken).ConfigureAwait(false);
@@ -246,7 +246,9 @@ namespace MediaBrowser.Providers.Movies
 
                     }).ConfigureAwait(false);
 
-                    item.PrimaryImagePath = await _providerManager.SaveImage(item, img, "folder" + Path.GetExtension(poster.file_path), ConfigurationManager.Configuration.SaveLocalMeta && item.LocationType == LocationType.FileSystem, cancellationToken).ConfigureAwait(false);
+                    await _providerManager.SaveImage(item, img, MimeTypes.GetMimeType(poster.file_path), ImageType.Primary, null, cancellationToken)
+                                        .ConfigureAwait(false);
+
                 }
             }
 
@@ -274,7 +276,8 @@ namespace MediaBrowser.Providers.Movies
 
                         }).ConfigureAwait(false);
 
-                        item.BackdropImagePaths.Add(await _providerManager.SaveImage(item, img, bdName + Path.GetExtension(images.backdrops[i].file_path), ConfigurationManager.Configuration.SaveLocalMeta && item.LocationType == LocationType.FileSystem, cancellationToken).ConfigureAwait(false));
+                        await _providerManager.SaveImage(item, img, MimeTypes.GetMimeType(images.backdrops[i].file_path), ImageType.Backdrop, item.BackdropImagePaths.Count, cancellationToken)
+                          .ConfigureAwait(false);
                     }
 
                     if (item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops)

+ 20 - 38
MediaBrowser.Providers/Movies/MovieDbProvider.cs

@@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Movies
         /// <summary>
         /// The movie db
         /// </summary>
-        private readonly SemaphoreSlim _movieDbResourcePool = new SemaphoreSlim(1,1);
+        private readonly SemaphoreSlim _movieDbResourcePool = new SemaphoreSlim(1, 1);
 
         internal static MovieDbProvider Current { get; private set; }
 
@@ -158,7 +158,7 @@ namespace MediaBrowser.Providers.Movies
                 _tmdbSettingsSemaphore.Release();
                 return _tmdbSettings;
             }
-            
+
             try
             {
                 using (var json = await GetMovieDbResponse(new HttpRequestOptions
@@ -199,7 +199,13 @@ namespace MediaBrowser.Providers.Movies
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
         {
             if (HasAltMeta(item))
-                return false; 
+                return false;
+
+            // Boxsets require two passes because we need the children to be refreshed
+            if (item is BoxSet && string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.TmdbCollection)))
+            {
+                return true;
+            }
 
             return base.NeedsRefreshInternal(item, providerInfo);
         }
@@ -291,19 +297,6 @@ namespace MediaBrowser.Providers.Movies
         /// <returns>Task{System.String}.</returns>
         public async Task<string> FindId(BaseItem item, int? productionYear, CancellationToken cancellationToken)
         {
-            string id = null;
-
-            if (item.LocationType == LocationType.FileSystem)
-            {
-                string justName = item.Path != null ? item.Path.Substring(item.Path.LastIndexOf(Path.DirectorySeparatorChar)) : string.Empty;
-                id = justName.GetAttributeValue("tmdbid");
-                if (id != null)
-                {
-                    Logger.Debug("Using tmdb id specified in path.");
-                    return id;
-                }
-            }
-
             int? year;
             string name = item.Name;
             ParseName(name, out name, out year);
@@ -320,25 +313,14 @@ namespace MediaBrowser.Providers.Movies
             var boxset = item as BoxSet;
             if (boxset != null)
             {
-                var firstChild = boxset.Children.FirstOrDefault();
-
-                if (firstChild != null)
-                {
-                    Logger.Debug("MovieDbProvider - Attempting to find boxset ID from: " + firstChild.Name);
-                    string childName;
-                    int? childYear;
-                    ParseName(firstChild.Name, out childName, out childYear);
-                    id = await GetBoxsetIdFromMovie(childName, childYear, language, cancellationToken).ConfigureAwait(false);
-                    if (id != null)
-                    {
-                        Logger.Info("MovieDbProvider - Found Boxset ID: " + id);
-                    }
-                }
-
-                return id;
+               // See if any movies have a collection id already
+                return boxset.Children.OfType<Video>()
+                    .Select(i => i.GetProviderId(MetadataProviders.TmdbCollection))
+                   .FirstOrDefault(i => i != null);
             }
+
             //nope - search for it
-            id = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
+            var id = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
             if (id == null)
             {
                 //try in english if wasn't before
@@ -509,7 +491,7 @@ namespace MediaBrowser.Providers.Movies
                             DateTime r;
 
                             //These dates are always in this exact format
-                            if (DateTime.TryParseExact(possible.release_date, "yyyy-MM-dd", EnUs, DateTimeStyles.None,  out r))
+                            if (DateTime.TryParseExact(possible.release_date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out r))
                             {
                                 if (Math.Abs(r.Year - year.Value) > 1) // allow a 1 year tolerance on release date
                                 {
@@ -708,7 +690,7 @@ namespace MediaBrowser.Providers.Movies
                     var ourRelease = movieData.releases.countries.FirstOrDefault(c => c.iso_3166_1.Equals(ConfigurationManager.Configuration.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)) ?? new Country();
                     var usRelease = movieData.releases.countries.FirstOrDefault(c => c.iso_3166_1.Equals("US", StringComparison.OrdinalIgnoreCase)) ?? new Country();
                     var minimunRelease = movieData.releases.countries.OrderBy(c => c.release_date).FirstOrDefault() ?? new Country();
-                    var ratingPrefix = ConfigurationManager.Configuration.MetadataCountryCode.Equals("us", StringComparison.OrdinalIgnoreCase) ? "" : ConfigurationManager.Configuration.MetadataCountryCode +"-";
+                    var ratingPrefix = ConfigurationManager.Configuration.MetadataCountryCode.Equals("us", StringComparison.OrdinalIgnoreCase) ? "" : ConfigurationManager.Configuration.MetadataCountryCode + "-";
                     movie.OfficialRating = !string.IsNullOrEmpty(ourRelease.certification)
                                                ? ratingPrefix + ourRelease.certification
                                                : !string.IsNullOrEmpty(usRelease.certification)
@@ -725,7 +707,7 @@ namespace MediaBrowser.Providers.Movies
                             movie.ProductionYear = ourRelease.release_date.Year;
                         }
                     }
-                    else if(usRelease.release_date != default (DateTime))
+                    else if (usRelease.release_date != default(DateTime))
                     {
                         if (usRelease.release_date.Year != 1)
                         {
@@ -733,7 +715,7 @@ namespace MediaBrowser.Providers.Movies
                             movie.ProductionYear = usRelease.release_date.Year;
                         }
                     }
-                    else if (minimunRelease.release_date != default (DateTime))
+                    else if (minimunRelease.release_date != default(DateTime))
                     {
                         if (minimunRelease.release_date.Year != 1)
                         {
@@ -1099,7 +1081,7 @@ namespace MediaBrowser.Providers.Movies
             /// <value>The total_results.</value>
             public int total_results { get; set; }
         }
-        
+
         protected class BelongsToCollection
         {
             public int id { get; set; }

+ 7 - 16
MediaBrowser.Providers/Movies/TmdbPersonProvider.cs

@@ -279,13 +279,8 @@ namespace MediaBrowser.Providers.Movies
                 {
                     var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
 
-                    var img = await DownloadAndSaveImage(person, tmdbSettings.images.base_url + ConfigurationManager.Configuration.TmdbFetchedProfileSize + profile.file_path,
-                                             "folder" + Path.GetExtension(profile.file_path), cancellationToken).ConfigureAwait(false);
-
-                    if (!string.IsNullOrEmpty(img))
-                    {
-                        person.PrimaryImagePath = img;
-                    }
+                    await DownloadAndSaveImage(person, tmdbSettings.images.base_url + ConfigurationManager.Configuration.TmdbFetchedProfileSize + profile.file_path,
+                                             MimeTypes.GetMimeType(profile.file_path), cancellationToken).ConfigureAwait(false);
                 }
             }
         }
@@ -300,15 +295,12 @@ namespace MediaBrowser.Providers.Movies
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="source">The source.</param>
-        /// <param name="targetName">Name of the target.</param>
+        /// <param name="mimeType">Type of the MIME.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{System.String}.</returns>
-        private async Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, CancellationToken cancellationToken)
+        private async Task DownloadAndSaveImage(BaseItem item, string source, string mimeType, CancellationToken cancellationToken)
         {
-            if (source == null) return null;
-
-            //download and save locally (if not already there)
-            var localPath = Path.Combine(item.MetaLocation, targetName);
+            if (source == null) return;
 
             using (var sourceStream = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
             {
@@ -317,12 +309,11 @@ namespace MediaBrowser.Providers.Movies
 
             }).ConfigureAwait(false))
             {
-                await ProviderManager.SaveToLibraryFilesystem(item, localPath, sourceStream, cancellationToken).ConfigureAwait(false);
+                await ProviderManager.SaveImage(item, sourceStream, mimeType, ImageType.Primary, null, cancellationToken)
+                                   .ConfigureAwait(false);
 
                 Logger.Debug("TmdbPersonProvider downloaded and saved image for {0}", item.Name);
             }
-
-            return localPath;
         }
 
         #region Result Objects

+ 4 - 2
MediaBrowser.Providers/Music/FanArtAlbumProvider.cs

@@ -199,7 +199,8 @@ namespace MediaBrowser.Providers.Music
 
                         if (!string.IsNullOrEmpty(path))
                         {
-                            item.SetImage(ImageType.Disc, await _providerManager.DownloadAndSaveImage(item, path, DiscFile, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                            await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Disc, null, cancellationToken)
+                                .ConfigureAwait(false);
                         }
                     }
 
@@ -217,7 +218,8 @@ namespace MediaBrowser.Providers.Music
 
                         if (!string.IsNullOrEmpty(path))
                         {
-                            item.SetImage(ImageType.Primary, await _providerManager.DownloadAndSaveImage(item, path, PrimaryFile, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                            await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Primary, null, cancellationToken)
+                                .ConfigureAwait(false);
                         }
                     }
                 }

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

@@ -306,7 +306,8 @@ namespace MediaBrowser.Providers.Music
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(item, path, LogoFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Logo, null, cancellationToken)
+                        .ConfigureAwait(false);
                 }
             }
             cancellationToken.ThrowIfCancellationRequested();
@@ -323,7 +324,8 @@ namespace MediaBrowser.Providers.Music
                         path = node.Value;
                         if (!string.IsNullOrEmpty(path))
                         {
-                            item.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(item, path, ("Backdrop" + (numBackdrops > 0 ? numBackdrops.ToString(UsCulture) : "") + ".jpg"), SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                            await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
+                                .ConfigureAwait(false);
                             numBackdrops++;
                             if (numBackdrops >= ConfigurationManager.Configuration.MaxBackdrops) break;
                         }
@@ -343,7 +345,8 @@ namespace MediaBrowser.Providers.Music
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(item, path, ArtFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Art, null, cancellationToken)
+                        .ConfigureAwait(false);
                 }
             }
             cancellationToken.ThrowIfCancellationRequested();
@@ -355,7 +358,8 @@ namespace MediaBrowser.Providers.Music
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(item, path, BannerFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Banner, null, cancellationToken)
+                        .ConfigureAwait(false);
                 }
             }
 
@@ -368,7 +372,8 @@ namespace MediaBrowser.Providers.Music
                 path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Primary, await _providerManager.DownloadAndSaveImage(item, path, PrimaryFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Primary, null, cancellationToken)
+                        .ConfigureAwait(false);
                 }
             }
         }

+ 9 - 6
MediaBrowser.Providers/Savers/SeriesXmlSaver.cs

@@ -82,13 +82,16 @@ namespace MediaBrowser.Providers.Savers
                 builder.Append("<Airs_Time>" + SecurityElement.Escape(series.AirTime) + "</Airs_Time>");
             }
 
-            if (series.AirDays.Count == 7)
+            if (series.AirDays != null)
             {
-                builder.Append("<Airs_DayOfWeek>" + SecurityElement.Escape("Daily") + "</Airs_DayOfWeek>");
-            }
-            else if (series.AirDays.Count > 0)
-            {
-                builder.Append("<Airs_DayOfWeek>" + SecurityElement.Escape(series.AirDays[0].ToString()) + "</Airs_DayOfWeek>");
+                if (series.AirDays.Count == 7)
+                {
+                    builder.Append("<Airs_DayOfWeek>" + SecurityElement.Escape("Daily") + "</Airs_DayOfWeek>");
+                }
+                else if (series.AirDays.Count > 0)
+                {
+                    builder.Append("<Airs_DayOfWeek>" + SecurityElement.Escape(series.AirDays[0].ToString()) + "</Airs_DayOfWeek>");
+                }
             }
 
             XmlSaverHelpers.AddCommonNodes(item, builder);

+ 2 - 1
MediaBrowser.Providers/TV/FanArtSeasonProvider.cs

@@ -149,7 +149,8 @@ namespace MediaBrowser.Providers.TV
                 
                 if (!string.IsNullOrEmpty(path))
                 {
-                    season.SetImage(ImageType.Thumb, await _providerManager.DownloadAndSaveImage(season, path, ThumbFile, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(season, path, FanArtResourcePool, ImageType.Thumb, null, cancellationToken)
+                                        .ConfigureAwait(false);
                 }
             }
         }

+ 10 - 5
MediaBrowser.Providers/TV/FanArtTVProvider.cs

@@ -234,7 +234,8 @@ namespace MediaBrowser.Providers.TV
                 var path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(item, path, LogoFile, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Logo, null, cancellationToken)
+                          .ConfigureAwait(false);
                 }
             }
 
@@ -250,7 +251,8 @@ namespace MediaBrowser.Providers.TV
                 var path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(item, path, ArtFile, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Art, null, cancellationToken)
+                          .ConfigureAwait(false);
                 }
             }
 
@@ -263,7 +265,8 @@ namespace MediaBrowser.Providers.TV
                 var path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Thumb, await _providerManager.DownloadAndSaveImage(item, path, ThumbFile, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Thumb, null, cancellationToken)
+                          .ConfigureAwait(false);
                 }
             }
 
@@ -274,7 +277,8 @@ namespace MediaBrowser.Providers.TV
                 var path = node != null ? node.Value : null;
                 if (!string.IsNullOrEmpty(path))
                 {
-                    item.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(item, path, BannerFile, ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                    await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Banner, null, cancellationToken)
+                          .ConfigureAwait(false);
                 }
             }
 
@@ -292,7 +296,8 @@ namespace MediaBrowser.Providers.TV
 
                         if (!string.IsNullOrEmpty(path))
                         {
-                            item.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(item, path, ("backdrop" + (numBackdrops > 0 ? numBackdrops.ToString(UsCulture) : "") + ".jpg"), ConfigurationManager.Configuration.SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                            await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
+                                  .ConfigureAwait(false);
 
                             numBackdrops++;
 

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

@@ -276,7 +276,10 @@ namespace MediaBrowser.Providers.TV
 
                     try
                     {
-                        episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken);
+                        var url = TVUtils.BannerUrl + p;
+
+                        await _providerManager.SaveImage(episode, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken)
+                          .ConfigureAwait(false);
                     }
                     catch (HttpException)
                     {

+ 15 - 11
MediaBrowser.Providers/TV/RemoteSeasonProvider.cs

@@ -188,7 +188,12 @@ namespace MediaBrowser.Providers.TV
                     n = n.SelectSingleNode("./BannerPath");
 
                     if (n != null)
-                        season.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false);
+                    {
+                        var url = TVUtils.BannerUrl + n.InnerText;
+
+                        await _providerManager.SaveImage(season, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken)
+                          .ConfigureAwait(false);
+                    }
                 }
             }
 
@@ -203,15 +208,10 @@ namespace MediaBrowser.Providers.TV
                     {
                         try
                         {
-                            var bannerImagePath =
-                                await _providerManager.DownloadAndSaveImage(season,
-                                                                                 TVUtils.BannerUrl + n.InnerText,
-                                                                                 "banner" +
-                                                                                 Path.GetExtension(n.InnerText),
-                                                                                 ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).
-                                                   ConfigureAwait(false);
-
-                            season.SetImage(ImageType.Banner, bannerImagePath);
+                            var url = TVUtils.BannerUrl + n.InnerText;
+
+                            await _providerManager.SaveImage(season, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken)
+                              .ConfigureAwait(false);
                         }
                         catch (HttpException ex)
                         {
@@ -235,7 +235,11 @@ namespace MediaBrowser.Providers.TV
                     n = n.SelectSingleNode("./BannerPath");
                     if (n != null)
                     {
-                        season.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "backdrop" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false));
+                        var url = TVUtils.BannerUrl + n.InnerText;
+
+                        await _providerManager.SaveImage(season, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, 0, cancellationToken)
+                          .ConfigureAwait(false);
+
                     }
                 }
             }

+ 0 - 7
MediaBrowser.Providers/TV/RemoteSeriesProvider.cs

@@ -215,13 +215,6 @@ namespace MediaBrowser.Providers.TV
                 await FetchSeriesData(series, seriesId, seriesDataPath, force, 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;
         }

+ 11 - 6
MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs

@@ -197,9 +197,10 @@ namespace MediaBrowser.Providers.TV
                     n = n.SelectSingleNode("./BannerPath");
                     if (n != null)
                     {
-                        var path = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false);
+                        var url = TVUtils.BannerUrl + n.InnerText;
 
-                        series.SetImage(ImageType.Primary, path);
+                        await _providerManager.SaveImage(series, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken)
+                          .ConfigureAwait(false);
                     }
                 }
             }
@@ -212,9 +213,10 @@ namespace MediaBrowser.Providers.TV
                     n = n.SelectSingleNode("./BannerPath");
                     if (n != null)
                     {
-                        var bannerImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken);
+                        var url = TVUtils.BannerUrl + n.InnerText;
 
-                        series.SetImage(ImageType.Banner, bannerImagePath);
+                        await _providerManager.SaveImage(series, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken)
+                          .ConfigureAwait(false);
                     }
                 }
             }
@@ -232,8 +234,11 @@ namespace MediaBrowser.Providers.TV
 
                         if (p != null)
                         {
-                            var bdName = "backdrop" + (bdNo > 0 ? bdNo.ToString(UsCulture) : "");
-                            series.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + p.InnerText, bdName + Path.GetExtension(p.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false));
+                            var url = TVUtils.BannerUrl + p.InnerText;
+
+                            await _providerManager.SaveImage(series, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken)
+                              .ConfigureAwait(false);
+                            
                             bdNo++;
                         }
 

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

@@ -148,6 +148,7 @@
     <Compile Include="Persistence\SqliteExtensions.cs" />
     <Compile Include="Persistence\TypeMapper.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Providers\ImageSaver.cs" />
     <Compile Include="Providers\ProviderManager.cs" />
     <Compile Include="ScheduledTasks\ArtistValidationTask.cs" />
     <Compile Include="ScheduledTasks\PeopleValidationTask.cs" />

+ 255 - 0
MediaBrowser.Server.Implementations/Providers/ImageSaver.cs

@@ -0,0 +1,255 @@
+using System.Globalization;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Model.Entities;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Providers
+{
+    /// <summary>
+    /// Class ImageSaver
+    /// </summary>
+    public class ImageSaver
+    {
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+        /// <summary>
+        /// The _config
+        /// </summary>
+        private readonly IServerConfigurationManager _config;
+
+        /// <summary>
+        /// The remote image cache
+        /// </summary>
+        private readonly FileSystemRepository _remoteImageCache;
+        /// <summary>
+        /// The _directory watchers
+        /// </summary>
+        private readonly IDirectoryWatchers _directoryWatchers;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImageSaver"/> class.
+        /// </summary>
+        /// <param name="config">The config.</param>
+        /// <param name="directoryWatchers">The directory watchers.</param>
+        public ImageSaver(IServerConfigurationManager config, IDirectoryWatchers directoryWatchers)
+        {
+            _config = config;
+            _directoryWatchers = directoryWatchers;
+            _remoteImageCache = new FileSystemRepository(config.ApplicationPaths.DownloadedImagesDataPath);
+        }
+
+        /// <summary>
+        /// Saves the image.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="source">The source.</param>
+        /// <param name="mimeType">Type of the MIME.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
+        {
+            if (string.IsNullOrEmpty(mimeType))
+            {
+                throw new ArgumentNullException("mimeType");
+            }
+
+            var saveLocally = _config.Configuration.SaveLocalMeta;
+
+            if (item is IItemByName)
+            {
+                saveLocally = true;
+            }
+            else if (item is User)
+            {
+                saveLocally = true;
+            }
+            else if (item is Audio || item.Parent == null || string.IsNullOrEmpty(item.MetaLocation))
+            {
+                saveLocally = false;
+            }
+
+            if (type != ImageType.Primary)
+            {
+                if (item is Episode)
+                {
+                    saveLocally = false;
+                }
+            }
+
+            if (item.LocationType != LocationType.FileSystem)
+            {
+                saveLocally = false;
+            }
+
+            var path = GetSavePath(item, type, imageIndex, mimeType, saveLocally);
+
+            var currentPath = GetCurrentImagePath(item, type, imageIndex);
+
+            try
+            {
+                _directoryWatchers.TemporarilyIgnore(path);
+
+                using (source)
+                {
+                    using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+                    {
+                        await source.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+                    }
+                }
+
+                SetImagePath(item, type, imageIndex, path);
+
+                if (!string.IsNullOrEmpty(currentPath) && !string.Equals(path, currentPath, StringComparison.OrdinalIgnoreCase))
+                {
+                    File.Delete(currentPath);
+                }
+            }
+            finally
+            {
+                _directoryWatchers.RemoveTempIgnore(path);
+            }
+
+        }
+
+        private string GetCurrentImagePath(BaseItem item, ImageType type, int? imageIndex)
+        {
+            switch (type)
+            {
+                case ImageType.Screenshot:
+
+                    if (!imageIndex.HasValue)
+                    {
+                        throw new ArgumentNullException("imageIndex");
+                    }
+                    return item.ScreenshotImagePaths.Count > imageIndex.Value ? item.ScreenshotImagePaths[imageIndex.Value] : null;
+                case ImageType.Backdrop:
+                    if (!imageIndex.HasValue)
+                    {
+                        throw new ArgumentNullException("imageIndex");
+                    }
+                    return item.BackdropImagePaths.Count > imageIndex.Value ? item.BackdropImagePaths[imageIndex.Value] : null;
+                default:
+                    return item.GetImage(type);
+            }
+        }
+
+        private void SetImagePath(BaseItem item, ImageType type, int? imageIndex, string path)
+        {
+            switch (type)
+            {
+                case ImageType.Screenshot:
+
+                    if (!imageIndex.HasValue)
+                    {
+                        throw new ArgumentNullException("imageIndex");
+                    }
+
+                    if (item.ScreenshotImagePaths.Count > imageIndex.Value)
+                    {
+                        item.ScreenshotImagePaths[imageIndex.Value] = path;
+                    }
+                    else
+                    {
+                        item.ScreenshotImagePaths.Add(path);
+                    }
+                    break;
+                case ImageType.Backdrop:
+                    if (!imageIndex.HasValue)
+                    {
+                        throw new ArgumentNullException("imageIndex");
+                    }
+                    if (item.BackdropImagePaths.Count > imageIndex.Value)
+                    {
+                        item.BackdropImagePaths[imageIndex.Value] = path;
+                    }
+                    else
+                    {
+                        item.BackdropImagePaths.Add(path);
+                    }
+                    break;
+                default:
+                    item.SetImage(type, path);
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Gets the save path.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <param name="mimeType">Type of the MIME.</param>
+        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
+        /// <returns>System.String.</returns>
+        /// <exception cref="System.ArgumentNullException">
+        /// imageIndex
+        /// or
+        /// imageIndex
+        /// </exception>
+        private string GetSavePath(BaseItem item, ImageType type, int? imageIndex, string mimeType, bool saveLocally)
+        {
+            string filename;
+
+            switch (type)
+            {
+                case ImageType.Art:
+                    filename = "clearart";
+                    break;
+                case ImageType.Primary:
+                    filename = item is Episode ? Path.GetFileNameWithoutExtension(item.Path) : "folder";
+                    break;
+                case ImageType.Backdrop:
+                    if (!imageIndex.HasValue)
+                    {
+                        throw new ArgumentNullException("imageIndex");
+                    }
+                    filename = imageIndex.Value == 0 ? "backdrop" : "backdrop" + imageIndex.Value.ToString(UsCulture);
+                    break;
+                case ImageType.Screenshot:
+                    if (!imageIndex.HasValue)
+                    {
+                        throw new ArgumentNullException("imageIndex");
+                    }
+                    filename = imageIndex.Value == 0 ? "screenshot" : "screenshot" + imageIndex.Value.ToString(UsCulture);
+                    break;
+                default:
+                    filename = type.ToString().ToLower();
+                    break;
+            }
+
+            var extension = mimeType.Split('/').Last();
+
+            if (string.Equals(extension, "jpeg", StringComparison.OrdinalIgnoreCase))
+            {
+                extension = "jpg";
+            }
+
+            filename += "." + extension.ToLower();
+
+            var path = (saveLocally && !string.IsNullOrEmpty(item.MetaLocation)) ?
+                Path.Combine(item.MetaLocation, filename) :
+                _remoteImageCache.GetResourcePath(item.GetType().FullName + item.Id, filename);
+
+            var parentPath = Path.GetDirectoryName(path);
+
+            if (!Directory.Exists(parentPath))
+            {
+                Directory.CreateDirectory(parentPath);
+            }
+
+            return path;
+        }
+    }
+}

+ 46 - 90
MediaBrowser.Server.Implementations/Providers/ProviderManager.cs

@@ -2,11 +2,14 @@
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -22,11 +25,6 @@ namespace MediaBrowser.Server.Implementations.Providers
     /// </summary>
     public class ProviderManager : IProviderManager
     {
-        /// <summary>
-        /// The remote image cache
-        /// </summary>
-        private readonly FileSystemRepository _remoteImageCache;
-
         /// <summary>
         /// The currently running metadata providers
         /// </summary>
@@ -74,7 +72,6 @@ namespace MediaBrowser.Server.Implementations.Providers
             _httpClient = httpClient;
             ConfigurationManager = configurationManager;
             _directoryWatchers = directoryWatchers;
-            _remoteImageCache = new FileSystemRepository(configurationManager.ApplicationPaths.DownloadedImagesDataPath);
 
             configurationManager.ConfigurationUpdated += configurationManager_ConfigurationUpdated;
         }
@@ -206,7 +203,7 @@ namespace MediaBrowser.Server.Implementations.Providers
             try
             {
                 var changed = await provider.FetchAsync(item, force, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token).ConfigureAwait(false);
-            
+
                 if (changed)
                 {
                     return provider.ItemUpdateType;
@@ -315,90 +312,9 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{System.String}.</returns>
         /// <exception cref="System.ArgumentNullException">item</exception>
-        public async Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, bool saveLocally, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+        public Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, bool saveLocally, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
         {
-            if (item == null)
-            {
-                throw new ArgumentNullException("item");
-            }
-            if (string.IsNullOrEmpty(source))
-            {
-                throw new ArgumentNullException("source");
-            }
-            if (string.IsNullOrEmpty(targetName))
-            {
-                throw new ArgumentNullException("targetName");
-            }
-            if (resourcePool == null)
-            {
-                throw new ArgumentNullException("resourcePool");
-            }
-
-            var img = await _httpClient.Get(source, resourcePool, cancellationToken).ConfigureAwait(false);
-
-            //download and save locally
-            return await SaveImage(item, img, targetName, saveLocally, cancellationToken).ConfigureAwait(false);
-        }
-
-        public async Task<string> SaveImage(BaseItem item, Stream source, string targetName, bool saveLocally, CancellationToken cancellationToken)
-        {
-            //download and save locally
-            var localPath = GetSavePath(item, targetName, saveLocally);
-
-            if (saveLocally) // queue to media directories
-            {
-                await SaveToLibraryFilesystem(item, localPath, source, cancellationToken).ConfigureAwait(false);
-            }
-            else
-            {
-                // we can write directly here because it won't affect the watchers
-
-                try
-                {
-                    using (var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
-                    {
-                        await source.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
-                    }
-                }
-                catch (OperationCanceledException)
-                {
-                    throw;
-                }
-                catch (Exception e)
-                {
-                    _logger.ErrorException("Error downloading and saving image " + localPath, e);
-                    throw;
-                }
-                finally
-                {
-                    source.Dispose();
-                }
-
-            }
-            return localPath;
-        }
-
-        /// <summary>
-        /// Gets the save path.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="targetFileName">Name of the target file.</param>
-        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
-        /// <returns>System.String.</returns>
-        public string GetSavePath(BaseItem item, string targetFileName, bool saveLocally)
-        {
-            var path = (saveLocally && item.MetaLocation != null) ?
-                Path.Combine(item.MetaLocation, targetFileName) :
-                _remoteImageCache.GetResourcePath(item.GetType().FullName + item.Id.ToString(), targetFileName);
-
-            var parentPath = Path.GetDirectoryName(path);
-
-            if (!Directory.Exists(parentPath))
-            {
-                Directory.CreateDirectory(parentPath);
-            }
-
-            return path;
+            throw new HttpException(string.Empty) { IsTimedOut = true };
         }
 
         /// <summary>
@@ -462,5 +378,45 @@ namespace MediaBrowser.Server.Implementations.Providers
                 _directoryWatchers.RemoveTempIgnore(path);
             }
         }
+
+
+        /// <summary>
+        /// Saves the image.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="url">The URL.</param>
+        /// <param name="resourcePool">The resource pool.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task SaveImage(BaseItem item, string url, SemaphoreSlim resourcePool, ImageType type, int? imageIndex, CancellationToken cancellationToken)
+        {
+            var response = await _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                ResourcePool = resourcePool,
+                Url = url
+
+            }).ConfigureAwait(false);
+
+            await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken)
+                    .ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Saves the image.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="source">The source.</param>
+        /// <param name="mimeType">Type of the MIME.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="imageIndex">Index of the image.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
+        {
+            return new ImageSaver(ConfigurationManager, _directoryWatchers).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken);
+        }
     }
 }