Ver código fonte

support backdrops from multiple sources

Luke Pulverenti 11 anos atrás
pai
commit
32cb872b06

+ 22 - 1
MediaBrowser.Api/Images/ImageService.cs

@@ -699,11 +699,32 @@ namespace MediaBrowser.Api.Images
             var temp1 = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
             var temp2 = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
 
+            // Copying over will fail against hidden files
+            RemoveHiddenAttribute(file1);
+            RemoveHiddenAttribute(file2);
+
             File.Copy(file1, temp1);
             File.Copy(file2, temp2);
 
             File.Copy(temp1, file2, true);
             File.Copy(temp2, file1, true);
+
+            File.Delete(temp1);
+            File.Delete(temp2);
+        }
+
+        private void RemoveHiddenAttribute(string path)
+        {
+            var currentFile = new FileInfo(path);
+
+            // This will fail if the file is hidden
+            if (currentFile.Exists)
+            {
+                if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+                {
+                    currentFile.Attributes &= ~FileAttributes.Hidden;
+                }
+            }
         }
 
         /// <summary>
@@ -854,7 +875,7 @@ namespace MediaBrowser.Api.Images
                 // Handle image/png; charset=utf-8
                 mimeType = mimeType.Split(';').FirstOrDefault();
 
-                await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, imageIndex, CancellationToken.None).ConfigureAwait(false);
+                await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, imageIndex, null, CancellationToken.None).ConfigureAwait(false);
 
                 await entity.RefreshMetadata(CancellationToken.None, forceRefresh: true, forceSave: true, allowSlowProviders: false).ConfigureAwait(false);
             }

+ 108 - 20
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -47,6 +47,7 @@ namespace MediaBrowser.Controller.Entities
             LockedFields = new List<MetadataFields>();
             Taglines = new List<string>();
             RemoteTrailers = new List<MediaUrl>();
+            ImageSources = new List<ImageSourceInfo>();
         }
 
         /// <summary>
@@ -233,23 +234,6 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The locked fields.</value>
         public List<MetadataFields> LockedFields { get; set; }
 
-        /// <summary>
-        /// Determines whether the item has a saved local image of the specified name (jpg or png).
-        /// </summary>
-        /// <param name="name">The name.</param>
-        /// <returns><c>true</c> if [has local image] [the specified item]; otherwise, <c>false</c>.</returns>
-        /// <exception cref="System.ArgumentNullException">name</exception>
-        public bool HasLocalImage(string name)
-        {
-            if (string.IsNullOrEmpty(name))
-            {
-                throw new ArgumentNullException("name");
-            }
-
-            return ResolveArgs.ContainsMetaFileByName(name + ".jpg") ||
-                ResolveArgs.ContainsMetaFileByName(name + ".png");
-        }
-
         /// <summary>
         /// Should be overridden to return the proper folder where metadata lives
         /// </summary>
@@ -536,6 +520,12 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The backdrop image paths.</value>
         public List<string> BackdropImagePaths { get; set; }
 
+        /// <summary>
+        /// Gets or sets the backdrop image sources.
+        /// </summary>
+        /// <value>The backdrop image sources.</value>
+        public List<ImageSourceInfo> ImageSources { get; set; }
+
         /// <summary>
         /// Gets or sets the screenshot image paths.
         /// </summary>
@@ -1508,8 +1498,10 @@ namespace MediaBrowser.Controller.Entities
 
                 BackdropImagePaths.Remove(file);
 
+                RemoveImageSourceForPath(file);
+                
                 // Delete the source file
-                File.Delete(file);
+                DeleteImagePath(file);
             }
             else if (type == ImageType.Screenshot)
             {
@@ -1523,12 +1515,12 @@ namespace MediaBrowser.Controller.Entities
                 ScreenshotImagePaths.Remove(file);
 
                 // Delete the source file
-                File.Delete(file);
+                DeleteImagePath(file);
             }
             else
             {
                 // Delete the source file
-                File.Delete(GetImage(type));
+                DeleteImagePath(GetImage(type));
 
                 // Remove it from the item
                 SetImage(type, null);
@@ -1538,6 +1530,26 @@ namespace MediaBrowser.Controller.Entities
             return RefreshMetadata(CancellationToken.None, forceSave: true);
         }
 
+        /// <summary>
+        /// Deletes the image path.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        private void DeleteImagePath(string path)
+        {
+            var currentFile = new FileInfo(path);
+
+            // This will fail if the file is hidden
+            if (currentFile.Exists)
+            {
+                if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+                {
+                    currentFile.Attributes &= ~FileAttributes.Hidden;
+                }
+
+                currentFile.Delete();
+            }
+        }
+
         /// <summary>
         /// Validates that images within the item are still on the file system
         /// </summary>
@@ -1570,7 +1582,83 @@ namespace MediaBrowser.Controller.Entities
             foreach (var path in deletedImages)
             {
                 BackdropImagePaths.Remove(path);
+
+                RemoveImageSourceForPath(path);
+            }
+        }
+
+        /// <summary>
+        /// Adds the image source.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <param name="url">The URL.</param>
+        public void AddImageSource(string path, string url)
+        {
+            RemoveImageSourceForPath(path);
+
+            var pathMd5 = path.ToLower().GetMD5();
+
+            ImageSources.Add(new ImageSourceInfo
+            {
+                ImagePathMD5 = pathMd5,
+                ImageUrlMD5 = url.ToLower().GetMD5()
+            });
+        }
+
+        /// <summary>
+        /// Gets the image source info.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>ImageSourceInfo.</returns>
+        public ImageSourceInfo GetImageSourceInfo(string path)
+        {
+            if (ImageSources.Count == 0)
+            {
+                return null;
+            }
+
+            var pathMd5 = path.ToLower().GetMD5();
+
+            return ImageSources.FirstOrDefault(i => i.ImagePathMD5 == pathMd5);
+        }
+
+        /// <summary>
+        /// Removes the image source for path.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        public void RemoveImageSourceForPath(string path)
+        {
+            if (ImageSources.Count == 0)
+            {
+                return;
             }
+
+            var pathMd5 = path.ToLower().GetMD5();
+
+            // Remove existing
+            foreach (var entry in ImageSources
+                .Where(i => i.ImagePathMD5 == pathMd5)
+                .ToList())
+            {
+                ImageSources.Remove(entry);
+            }
+        }
+
+        /// <summary>
+        /// Determines whether [contains image with source URL] [the specified URL].
+        /// </summary>
+        /// <param name="url">The URL.</param>
+        /// <returns><c>true</c> if [contains image with source URL] [the specified URL]; otherwise, <c>false</c>.</returns>
+        public bool ContainsImageWithSourceUrl(string url)
+        {
+            if (ImageSources.Count == 0)
+            {
+                return false;
+            }
+
+            var md5 = url.ToLower().GetMD5();
+
+            return ImageSources.Any(i => i.ImageUrlMD5 == md5);
         }
 
         /// <summary>

+ 10 - 0
MediaBrowser.Controller/Entities/ImageSourceInfo.cs

@@ -0,0 +1,10 @@
+using System;
+
+namespace MediaBrowser.Controller.Entities
+{
+    public class ImageSourceInfo
+    {
+        public Guid ImagePathMD5 { get; set; }
+        public Guid ImageUrlMD5 { get; set; }
+    }
+}

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

@@ -91,6 +91,7 @@
     <Compile Include="Entities\IByReferenceItem.cs" />
     <Compile Include="Entities\IItemByName.cs" />
     <Compile Include="Entities\ILibraryItem.cs" />
+    <Compile Include="Entities\ImageSourceInfo.cs" />
     <Compile Include="Entities\LinkedChild.cs" />
     <Compile Include="Entities\MusicVideo.cs" />
     <Compile Include="Library\ILibraryPostScanTask.cs" />

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

@@ -43,9 +43,10 @@ namespace MediaBrowser.Controller.Providers
         /// <param name="mimeType">Type of the MIME.</param>
         /// <param name="type">The type.</param>
         /// <param name="imageIndex">Index of the image.</param>
+        /// <param name="sourceUrl">The source URL.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken);
+        Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, string sourceUrl, CancellationToken cancellationToken);
 
         /// <summary>
         /// Adds the metadata providers.

+ 7 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -233,6 +233,12 @@ namespace MediaBrowser.Model.Configuration
         /// <value>The width of the min movie backdrop.</value>
         public int MinMovieBackdropWidth { get; set; }
 
+        /// <summary>
+        /// Gets or sets the width of the min series backdrop.
+        /// </summary>
+        /// <value>The width of the min series backdrop.</value>
+        public int MinSeriesBackdropWidth { get; set; }
+        
         /// <summary>
         /// Gets or sets the width of the min movie poster.
         /// </summary>
@@ -286,6 +292,7 @@ namespace MediaBrowser.Model.Configuration
             SeasonZeroDisplayName = "Specials";
 
             MinMovieBackdropWidth = 1920;
+            MinSeriesBackdropWidth = 1920;
             MinMoviePosterWidth = 1000;
         }
     }

+ 5 - 4
MediaBrowser.Providers/Movies/FanArtMovieProvider.cs

@@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Movies
                 item.HasImage(ImageType.Disc) &&
                 item.HasImage(ImageType.Banner) &&
                 item.HasImage(ImageType.Thumb) &&
-                item.BackdropImagePaths.Count > 0)
+                item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops)
             {
                 return false;
             }
@@ -389,7 +389,8 @@ namespace MediaBrowser.Providers.Movies
                 }
             }
 
-            if (ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count == 0)
+            var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
+            if (ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit)
             {
                 var nodes = doc.SelectNodes("//fanart/movie/moviebackgrounds//@url");
 
@@ -401,14 +402,14 @@ namespace MediaBrowser.Providers.Movies
                     {
                         path = node.Value;
 
-                        if (!string.IsNullOrEmpty(path))
+                        if (!string.IsNullOrEmpty(path) && !item.ContainsImageWithSourceUrl(path))
                         {
                             await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
                                                 .ConfigureAwait(false);
 
                             numBackdrops++;
 
-                            if (item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops) break;
+                            if (item.BackdropImagePaths.Count >= backdropLimit) break;
                         }
                     }
 

+ 13 - 12
MediaBrowser.Providers/Movies/MovieDbImagesProvider.cs

@@ -9,7 +9,6 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
 using System;
 using System.Collections.Generic;
-using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -135,7 +134,7 @@ namespace MediaBrowser.Providers.Movies
             }
 
             // Don't refresh if we already have both poster and backdrop and we're not refreshing images
-            if (item.HasImage(ImageType.Primary) && item.BackdropImagePaths.Count > 0)
+            if (item.HasImage(ImageType.Primary) && item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops)
             {
                 return false;
             }
@@ -239,14 +238,16 @@ namespace MediaBrowser.Providers.Movies
 
                 if (poster != null)
                 {
+                    var url = tmdbImageUrl + poster.file_path;
+
                     var img = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
                     {
-                        Url = tmdbImageUrl + poster.file_path,
+                        Url = url,
                         CancellationToken = cancellationToken
 
                     }).ConfigureAwait(false);
 
-                    await _providerManager.SaveImage(item, img, MimeTypes.GetMimeType(poster.file_path), ImageType.Primary, null, cancellationToken)
+                    await _providerManager.SaveImage(item, img, MimeTypes.GetMimeType(poster.file_path), ImageType.Primary, null, url, cancellationToken)
                                         .ConfigureAwait(false);
 
                 }
@@ -258,8 +259,10 @@ namespace MediaBrowser.Providers.Movies
                 images.backdrops.Where(i => i.width >= ConfigurationManager.Configuration.MinMovieBackdropWidth)
                 .ToList();
 
+            var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
+
             // backdrops - only download if earlier providers didn't find any (fanart)
-            if (eligibleBackdrops.Count > 0 && ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count == 0)
+            if (eligibleBackdrops.Count > 0 && ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit)
             {
                 var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
 
@@ -267,24 +270,22 @@ namespace MediaBrowser.Providers.Movies
 
                 for (var i = 0; i < eligibleBackdrops.Count; i++)
                 {
-                    var bdName = "backdrop" + (i == 0 ? "" : i.ToString(CultureInfo.InvariantCulture));
-
-                    var hasLocalBackdrop = item.LocationType == LocationType.FileSystem && ConfigurationManager.Configuration.SaveLocalMeta ? item.HasLocalImage(bdName) : item.BackdropImagePaths.Count > i;
+                    var url = tmdbImageUrl + eligibleBackdrops[i].file_path;
 
-                    if (!hasLocalBackdrop)
+                    if (!item.ContainsImageWithSourceUrl(url))
                     {
                         var img = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions
                         {
-                            Url = tmdbImageUrl + eligibleBackdrops[i].file_path,
+                            Url = url,
                             CancellationToken = cancellationToken
 
                         }).ConfigureAwait(false);
 
-                        await _providerManager.SaveImage(item, img, MimeTypes.GetMimeType(eligibleBackdrops[i].file_path), ImageType.Backdrop, item.BackdropImagePaths.Count, cancellationToken)
+                        await _providerManager.SaveImage(item, img, MimeTypes.GetMimeType(eligibleBackdrops[i].file_path), ImageType.Backdrop, item.BackdropImagePaths.Count, url, cancellationToken)
                           .ConfigureAwait(false);
                     }
 
-                    if (item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops)
+                    if (item.BackdropImagePaths.Count >= backdropLimit)
                     {
                         break;
                     }

+ 8 - 7
MediaBrowser.Providers/Movies/MovieDbProvider.cs

@@ -513,7 +513,7 @@ namespace MediaBrowser.Providers.Movies
             {
                 var isBoxSet = item is BoxSet;
 
-                var mainResult = await FetchMainResult(id, isBoxSet, cancellationToken).ConfigureAwait(false);
+                var mainResult = await FetchMainResult(id, isBoxSet, language, cancellationToken).ConfigureAwait(false);
 
                 if (mainResult == null) return;
 
@@ -550,7 +550,7 @@ namespace MediaBrowser.Providers.Movies
         {
             var language = ConfigurationManager.Configuration.PreferredMetadataLanguage;
 
-            var mainResult = await FetchMainResult(id, isBoxSet, cancellationToken).ConfigureAwait(false);
+            var mainResult = await FetchMainResult(id, isBoxSet, language, cancellationToken).ConfigureAwait(false);
 
             if (mainResult == null) return;
 
@@ -588,13 +588,14 @@ namespace MediaBrowser.Providers.Movies
         /// </summary>
         /// <param name="id">The id.</param>
         /// <param name="isBoxSet">if set to <c>true</c> [is box set].</param>
+        /// <param name="language">The language.</param>
         /// <param name="cancellationToken">The cancellation token</param>
         /// <returns>Task{CompleteMovieData}.</returns>
-        protected async Task<CompleteMovieData> FetchMainResult(string id, bool isBoxSet, CancellationToken cancellationToken)
+        protected async Task<CompleteMovieData> FetchMainResult(string id, bool isBoxSet, string language, CancellationToken cancellationToken)
         {
             var baseUrl = isBoxSet ? GetBoxSetInfo3 : GetMovieInfo3;
 
-            string url = string.Format(baseUrl, id, ApiKey, ConfigurationManager.Configuration.PreferredMetadataLanguage);
+            string url = string.Format(baseUrl, id, ApiKey, language);
             CompleteMovieData mainResult;
 
             cancellationToken.ThrowIfCancellationRequested();
@@ -614,13 +615,13 @@ namespace MediaBrowser.Providers.Movies
 
             if (mainResult != null && string.IsNullOrEmpty(mainResult.overview))
             {
-                if (ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() != "en")
+                if (language.ToLower() != "en")
                 {
-                    Logger.Info("MovieDbProvider couldn't find meta for language " + ConfigurationManager.Configuration.PreferredMetadataLanguage + ". Trying English...");
+                    Logger.Info("MovieDbProvider couldn't find meta for language " + language + ". Trying English...");
 
                     url = string.Format(baseUrl, id, ApiKey, "en");
 
-                    using (Stream json = await GetMovieDbResponse(new HttpRequestOptions
+                    using (var json = await GetMovieDbResponse(new HttpRequestOptions
                     {
                         Url = url,
                         CancellationToken = cancellationToken,

+ 1 - 1
MediaBrowser.Providers/Movies/TmdbPersonProvider.cs

@@ -390,7 +390,7 @@ namespace MediaBrowser.Providers.Movies
 
             }).ConfigureAwait(false))
             {
-                await ProviderManager.SaveImage(item, sourceStream, mimeType, ImageType.Primary, null, cancellationToken)
+                await ProviderManager.SaveImage(item, sourceStream, mimeType, ImageType.Primary, null, source, cancellationToken)
                                    .ConfigureAwait(false);
 
                 Logger.Debug("TmdbPersonProvider downloaded and saved image for {0}", item.Name);

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

@@ -1,5 +1,4 @@
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;

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

@@ -332,6 +332,8 @@ namespace MediaBrowser.Providers.Music
             }
             cancellationToken.ThrowIfCancellationRequested();
 
+            var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
+
             if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops && item.BackdropImagePaths.Count == 0)
             {
                 var nodes = doc.SelectNodes("//fanart/music/artistbackgrounds//@url");
@@ -342,14 +344,14 @@ namespace MediaBrowser.Providers.Music
                     foreach (XmlNode node in nodes)
                     {
                         path = node.Value;
-                        if (!string.IsNullOrEmpty(path))
+                        if (!string.IsNullOrEmpty(path) && !item.ContainsImageWithSourceUrl(path))
                         {
                             try
                             {
                                 await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
                                     .ConfigureAwait(false);
                                 numBackdrops++;
-                                if (numBackdrops >= ConfigurationManager.Configuration.MaxBackdrops) break;
+                                if (numBackdrops >= backdropLimit) break;
                             }
                             catch (HttpException ex)
                             {

+ 6 - 4
MediaBrowser.Providers/TV/FanArtTVProvider.cs

@@ -94,7 +94,7 @@ namespace MediaBrowser.Providers.TV
                 item.HasImage(ImageType.Logo) &&
                 item.HasImage(ImageType.Banner) &&
                 item.HasImage(ImageType.Thumb) &&
-                item.BackdropImagePaths.Count > 0)
+                item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops)
             {
                 return false;
             }
@@ -298,7 +298,9 @@ namespace MediaBrowser.Providers.TV
                 }
             }
 
-            if (ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count == 0)
+            var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
+
+            if (ConfigurationManager.Configuration.DownloadMovieImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit)
             {
                 var nodes = doc.SelectNodes("//fanart/series/showbackgrounds//@url");
 
@@ -310,14 +312,14 @@ namespace MediaBrowser.Providers.TV
                     {
                         var path = node.Value;
 
-                        if (!string.IsNullOrEmpty(path))
+                        if (!string.IsNullOrEmpty(path) && !item.ContainsImageWithSourceUrl(path))
                         {
                             await _providerManager.SaveImage(item, path, FanArtResourcePool, ImageType.Backdrop, numBackdrops, cancellationToken)
                                   .ConfigureAwait(false);
 
                             numBackdrops++;
 
-                            if (item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops) break;
+                            if (item.BackdropImagePaths.Count >= backdropLimit) break;
                         }
                     }
 

+ 23 - 4
MediaBrowser.Providers/TV/RemoteSeasonProvider.cs

@@ -121,6 +121,15 @@ namespace MediaBrowser.Providers.TV
             return false;
         }
 
+        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+        {
+            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Banner) && item.BackdropImagePaths.Count > 0)
+            {
+                return false;
+            }
+            return base.NeedsRefreshInternal(item, providerInfo);
+        }
+
         /// <summary>
         /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
         /// </summary>
@@ -146,7 +155,7 @@ namespace MediaBrowser.Providers.TV
                 try
                 {
                     var fanartData = FetchFanartXmlData(imagesXmlPath, seasonNumber.Value, cancellationToken);
-                    await DownloadImages(item, fanartData, ConfigurationManager.Configuration.MaxBackdrops, cancellationToken).ConfigureAwait(false);
+                    await DownloadImages(item, fanartData, 1, cancellationToken).ConfigureAwait(false);
                 }
                 catch (FileNotFoundException)
                 {
@@ -186,13 +195,18 @@ namespace MediaBrowser.Providers.TV
                 }
             }
 
-            if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && item.BackdropImagePaths.Count == 0)
+            if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit)
             {
                 var bdNo = item.BackdropImagePaths.Count;
 
                 foreach (var backdrop in data.Backdrops)
                 {
-                    var url = TVUtils.BannerUrl + backdrop;
+                    var url = TVUtils.BannerUrl + backdrop.Url;
+
+                    if (item.ContainsImageWithSourceUrl(url))
+                    {
+                        continue;
+                    }
 
                     await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken)
                       .ConfigureAwait(false);
@@ -260,6 +274,7 @@ namespace MediaBrowser.Providers.TV
             string bannerType2 = null;
             string url = null;
             int? bannerSeason = null;
+            string resolution = null;
 
             while (reader.Read())
             {
@@ -319,7 +334,11 @@ namespace MediaBrowser.Providers.TV
                 }
                 else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase))
                 {
-                    data.Backdrops.Add(url);
+                    data.Backdrops.Add(new ImageInfo
+                    {
+                        Url = url,
+                        Resolution = resolution
+                    });
                 }
             }
         }

+ 62 - 23
MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using System.Linq;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -133,6 +134,15 @@ namespace MediaBrowser.Providers.TV
             return base.CompareDate(item);
         }
 
+        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+        {
+            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Banner) && item.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops)
+            {
+                return false;
+            }
+            return base.NeedsRefreshInternal(item, providerInfo);
+        }
+        
         /// <summary>
         /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
         /// </summary>
@@ -154,10 +164,10 @@ namespace MediaBrowser.Providers.TV
 
                 var imagesXmlPath = Path.Combine(seriesDataPath, "banners.xml");
 
-                if (!series.HasImage(ImageType.Primary) || !series.HasImage(ImageType.Banner) || series.BackdropImagePaths.Count == 0)
-                {
-                    var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
+                var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops;
 
+                if (!series.HasImage(ImageType.Primary) || !series.HasImage(ImageType.Banner) || series.BackdropImagePaths.Count < backdropLimit)
+                {
                     Directory.CreateDirectory(seriesDataPath);
                     
                     try
@@ -171,13 +181,6 @@ namespace MediaBrowser.Providers.TV
                     }
                 }
 
-                BaseProviderInfo data;
-                if (!item.ProviderData.TryGetValue(Id, out data))
-                {
-                    data = new BaseProviderInfo();
-                    item.ProviderData[Id] = data;
-                }
-
                 SetLastRefreshed(item, DateTime.UtcNow);
                 return true;
             }
@@ -213,13 +216,39 @@ namespace MediaBrowser.Providers.TV
                 }
             }
 
-            if (ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops && item.BackdropImagePaths.Count == 0)
+            if (ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit)
             {
                 var bdNo = item.BackdropImagePaths.Count;
 
-                foreach (var backdrop in data.Backdrops)
+                var eligibleBackdrops = data.Backdrops
+                    .Where(i =>
+                    {
+                        if (string.IsNullOrEmpty(i.Resolution))
+                        {
+                            return true;
+                        }
+
+                        var parts = i.Resolution.Split('x');
+
+                        int width;
+
+                        if (int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width))
+                        {
+                            return width >= ConfigurationManager.Configuration.MinSeriesBackdropWidth;
+                        }
+
+                        return true;
+                    })
+                    .ToList();
+
+                foreach (var backdrop in eligibleBackdrops)
                 {
-                    var url = TVUtils.BannerUrl + backdrop;
+                    var url = TVUtils.BannerUrl + backdrop.Url;
+
+                    if (item.ContainsImageWithSourceUrl(url))
+                    {
+                        continue;
+                    }
 
                     await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken)
                       .ConfigureAwait(false);
@@ -285,6 +314,7 @@ namespace MediaBrowser.Providers.TV
 
             string type = null;
             string url = null;
+            string resolution = null;
 
             while (reader.Read())
             {
@@ -312,13 +342,6 @@ namespace MediaBrowser.Providers.TV
                                         return;
                                     }
                                 }
-                                else if (string.Equals(type, "fanart", StringComparison.OrdinalIgnoreCase))
-                                {
-                                    if (data.Backdrops.Count >= backdropLimit)
-                                    {
-                                        return;
-                                    }
-                                }
                                 else
                                 {
                                     return;
@@ -333,6 +356,12 @@ namespace MediaBrowser.Providers.TV
                                 break;
                             }
 
+                        case "BannerType2":
+                            {
+                                resolution = reader.ReadElementContentAsString() ?? string.Empty;
+                                break;
+                            }
+
                         default:
                             reader.Skip();
                             break;
@@ -352,7 +381,11 @@ namespace MediaBrowser.Providers.TV
                 }
                 else if (string.Equals(type, "fanart", StringComparison.OrdinalIgnoreCase))
                 {
-                    data.Backdrops.Add(url);
+                    data.Backdrops.Add(new ImageInfo
+                    {
+                        Url = url,
+                        Resolution = resolution
+                    });
                 }
             }
         }
@@ -364,6 +397,12 @@ namespace MediaBrowser.Providers.TV
         public string LanguageBanner { get; set; }
         public string Poster { get; set; }
         public string Banner { get; set; }
-        public List<string> Backdrops = new List<string>();
+        public List<ImageInfo> Backdrops = new List<ImageInfo>();
+    }
+
+    internal class ImageInfo
+    {
+        public string Url { get; set; }
+        public string Resolution { get; set; }
     }
 }

+ 15 - 3
MediaBrowser.Server.Implementations/Library/Validators/ArtistsValidator.cs

@@ -90,10 +90,22 @@ namespace MediaBrowser.Server.Implementations.Library.Validators
                     MergeImages(musicArtist.Images, artist.Images);
 
                     // Merge backdrops
-                    var backdrops = musicArtist.BackdropImagePaths.ToList();
-                    backdrops.InsertRange(0, artist.BackdropImagePaths);
-                    artist.BackdropImagePaths = backdrops.Distinct(StringComparer.OrdinalIgnoreCase)
+                    var additionalBackdrops = musicArtist
+                        .BackdropImagePaths
+                        .Except(artist.BackdropImagePaths)
                         .ToList();
+
+                    var sources = additionalBackdrops
+                        .Select(musicArtist.GetImageSourceInfo)
+                        .ToList();
+
+                    foreach (var path in additionalBackdrops)
+                    {
+                        artist.RemoveImageSourceForPath(path);
+                    }
+
+                    artist.BackdropImagePaths.AddRange(additionalBackdrops);
+                    artist.ImageSources.AddRange(sources);
                 }
 
                 if (!artist.LockedFields.Contains(MetadataFields.Genres))

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

@@ -56,9 +56,11 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="mimeType">Type of the MIME.</param>
         /// <param name="type">The type.</param>
         /// <param name="imageIndex">Index of the image.</param>
+        /// <param name="sourceUrl">The source URL.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        public async Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
+        /// <exception cref="System.ArgumentNullException">mimeType</exception>
+        public async Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, string sourceUrl, CancellationToken cancellationToken)
         {
             if (string.IsNullOrEmpty(mimeType))
             {
@@ -128,7 +130,7 @@ namespace MediaBrowser.Server.Implementations.Providers
             }
 
             // Set the path into the BaseItem
-            SetImagePath(item, type, imageIndex, paths[0]);
+            SetImagePath(item, type, imageIndex, paths[0], sourceUrl);
 
             // Delete the current path
             if (!string.IsNullOrEmpty(currentPath) && !paths.Contains(currentPath, StringComparer.OrdinalIgnoreCase))
@@ -137,7 +139,18 @@ namespace MediaBrowser.Server.Implementations.Providers
 
                 try
                 {
-                    File.Delete(currentPath);
+                    var currentFile = new FileInfo(currentPath);
+
+                    // This will fail if the file is hidden
+                    if (currentFile.Exists)
+                    {
+                        if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+                        {
+                            currentFile.Attributes &= ~FileAttributes.Hidden;
+                        }
+
+                        currentFile.Delete();
+                    }
                 }
                 finally
                 {
@@ -244,12 +257,11 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="type">The type.</param>
         /// <param name="imageIndex">Index of the image.</param>
         /// <param name="path">The path.</param>
-        /// <exception cref="System.ArgumentNullException">
-        /// imageIndex
+        /// <param name="sourceUrl">The source URL.</param>
+        /// <exception cref="System.ArgumentNullException">imageIndex
         /// or
-        /// imageIndex
-        /// </exception>
-        private void SetImagePath(BaseItem item, ImageType type, int? imageIndex, string path)
+        /// imageIndex</exception>
+        private void SetImagePath(BaseItem item, ImageType type, int? imageIndex, string path, string sourceUrl)
         {
             switch (type)
             {
@@ -282,6 +294,15 @@ namespace MediaBrowser.Server.Implementations.Providers
                     {
                         item.BackdropImagePaths.Add(path);
                     }
+
+                    if (string.IsNullOrEmpty(sourceUrl))
+                    {
+                        item.RemoveImageSourceForPath(path);
+                    }
+                    else
+                    {
+                        item.AddImageSource(path, sourceUrl);
+                    }
                     break;
                 default:
                     item.SetImage(type, path);

+ 4 - 3
MediaBrowser.Server.Implementations/Providers/ProviderManager.cs

@@ -325,7 +325,7 @@ namespace MediaBrowser.Server.Implementations.Providers
 
             }).ConfigureAwait(false);
 
-            await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken)
+            await SaveImage(item, response.Content, response.ContentType, type, imageIndex, url, cancellationToken)
                     .ConfigureAwait(false);
         }
 
@@ -337,11 +337,12 @@ namespace MediaBrowser.Server.Implementations.Providers
         /// <param name="mimeType">Type of the MIME.</param>
         /// <param name="type">The type.</param>
         /// <param name="imageIndex">Index of the image.</param>
+        /// <param name="sourceUrl">The source URL.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken)
+        public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, string sourceUrl, CancellationToken cancellationToken)
         {
-            return new ImageSaver(ConfigurationManager, _directoryWatchers).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken);
+            return new ImageSaver(ConfigurationManager, _directoryWatchers).SaveImage(item, source, mimeType, type, imageIndex, sourceUrl, cancellationToken);
         }
     }
 }

+ 15 - 0
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -174,6 +174,11 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <param name="item">The item.</param>
         private void RemoveNowPlayingItem(SessionInfo session, BaseItem item)
         {
+            if (item == null)
+            {
+                throw new ArgumentNullException("item");
+            }
+
             if (session.NowPlayingItem != null && session.NowPlayingItem.Id == item.Id)
             {
                 session.NowPlayingItem = null;
@@ -319,6 +324,16 @@ namespace MediaBrowser.Server.Implementations.Session
                 throw new ArgumentNullException("info");
             }
 
+            if (info.Item == null)
+            {
+                throw new ArgumentException("PlaybackStopInfo.Item cannot be null");
+            }
+
+            if (info.SessionId == Guid.Empty)
+            {
+                throw new ArgumentException("PlaybackStopInfo.SessionId cannot be Guid.Empty");
+            }
+            
             if (info.PositionTicks.HasValue && info.PositionTicks.Value < 0)
             {
                 throw new ArgumentOutOfRangeException("positionTicks");