Переглянути джерело

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

Eric Reed 12 роки тому
батько
коміт
873be2f284

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

@@ -74,6 +74,7 @@
     <Compile Include="Entities\Audio\MusicAlbumDisc.cs" />
     <Compile Include="Library\ILibraryPrescanTask.cs" />
     <Compile Include="Providers\Movies\MovieDbImagesProvider.cs" />
+    <Compile Include="Providers\Music\FanArtUpdatesPrescanTask.cs" />
     <Compile Include="Providers\TV\FanArtSeasonProvider.cs" />
     <Compile Include="Providers\TV\TvdbPrescanTask.cs" />
     <Compile Include="Providers\TV\TvdbSeriesImageProvider.cs" />

+ 4 - 3
MediaBrowser.Controller/Providers/FanartBaseProvider.cs

@@ -10,7 +10,7 @@ namespace MediaBrowser.Controller.Providers
     /// </summary>
     public abstract class FanartBaseProvider : BaseMetadataProvider
     {
-        protected static readonly SemaphoreSlim FanArtResourcePool = new SemaphoreSlim(3,3);
+        internal static readonly SemaphoreSlim FanArtResourcePool = new SemaphoreSlim(3, 3);
 
         /// <summary>
         /// The LOG o_ FILE
@@ -50,9 +50,10 @@ namespace MediaBrowser.Controller.Providers
         /// <summary>
         /// The API key
         /// </summary>
-        protected const string ApiKey = "5c6b04c68e904cfed1e6cbc9a9e683d4";
+        internal const string ApiKey = "5c6b04c68e904cfed1e6cbc9a9e683d4";
 
-        protected FanartBaseProvider(ILogManager logManager, IServerConfigurationManager configurationManager) : base(logManager, configurationManager)
+        protected FanartBaseProvider(ILogManager logManager, IServerConfigurationManager configurationManager)
+            : base(logManager, configurationManager)
         {
         }
 

+ 11 - 5
MediaBrowser.Controller/Providers/ImagesByNameProvider.cs

@@ -153,14 +153,20 @@ namespace MediaBrowser.Controller.Providers
         {
             var location = GetLocation(item);
 
-            var result = new FileInfo(Path.Combine(location, filenameWithoutExtension + ".png"));
+            var files = new DirectoryInfo(location).EnumerateFiles("*", SearchOption.TopDirectoryOnly).ToList();
 
-            if (!result.Exists)
-                result = new FileInfo(Path.Combine(location, filenameWithoutExtension + ".jpg"));
+            var file = files.FirstOrDefault(i => string.Equals(i.Name, filenameWithoutExtension + ".png", StringComparison.OrdinalIgnoreCase));
 
-            if (result.Exists)
+            if (file != null)
             {
-                return result;
+                return file;
+            }
+
+            file = files.FirstOrDefault(i => string.Equals(i.Name, filenameWithoutExtension + ".jpg", StringComparison.OrdinalIgnoreCase));
+
+            if (file != null)
+            {
+                return file;
             }
 
             return null;

+ 0 - 2
MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs

@@ -5,9 +5,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Net;
 using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;

+ 187 - 78
MediaBrowser.Controller/Providers/Music/FanArtArtistProvider.cs

@@ -1,5 +1,4 @@
-using System.IO;
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
@@ -11,6 +10,8 @@ using MediaBrowser.Model.Logging;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
+using System.IO;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
@@ -28,8 +29,21 @@ namespace MediaBrowser.Controller.Providers.Music
         /// <value>The HTTP client.</value>
         protected IHttpClient HttpClient { get; private set; }
 
+        /// <summary>
+        /// The _provider manager
+        /// </summary>
         private readonly IProviderManager _providerManager;
 
+        internal static FanArtArtistProvider Current;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FanArtArtistProvider"/> class.
+        /// </summary>
+        /// <param name="httpClient">The HTTP client.</param>
+        /// <param name="logManager">The log manager.</param>
+        /// <param name="configurationManager">The configuration manager.</param>
+        /// <param name="providerManager">The provider manager.</param>
+        /// <exception cref="System.ArgumentNullException">httpClient</exception>
         public FanArtArtistProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
             : base(logManager, configurationManager)
         {
@@ -39,6 +53,8 @@ namespace MediaBrowser.Controller.Providers.Music
             }
             HttpClient = httpClient;
             _providerManager = providerManager;
+
+            Current = this;
         }
 
         /// <summary>
@@ -56,11 +72,19 @@ namespace MediaBrowser.Controller.Providers.Music
             return item is MusicArtist;
         }
 
+        /// <summary>
+        /// Gets a value indicating whether [save local meta].
+        /// </summary>
+        /// <value><c>true</c> if [save local meta]; otherwise, <c>false</c>.</value>
         protected virtual bool SaveLocalMeta
         {
             get { return ConfigurationManager.Configuration.SaveLocalMeta; }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether [refresh on version change].
+        /// </summary>
+        /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
         protected override bool RefreshOnVersionChange
         {
             get
@@ -69,11 +93,15 @@ namespace MediaBrowser.Controller.Providers.Music
             }
         }
 
+        /// <summary>
+        /// Gets the provider version.
+        /// </summary>
+        /// <value>The provider version.</value>
         protected override string ProviderVersion
         {
             get
             {
-                return "5";
+                return "7";
             }
         }
 
@@ -91,17 +119,57 @@ namespace MediaBrowser.Controller.Providers.Music
             }
 
             if (!ConfigurationManager.Configuration.DownloadMusicArtistImages.Art &&
-                !ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops &&
-                !ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner &&
-                !ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo &&
-                !ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary)
+              !ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops &&
+              !ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner &&
+              !ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo &&
+              !ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary &&
+
+              // The fanart album provider depends on xml downloaded here, so honor it's settings too
+                !ConfigurationManager.Configuration.DownloadMusicAlbumImages.Disc &&
+                !ConfigurationManager.Configuration.DownloadMusicAlbumImages.Primary)
             {
                 return false;
             }
 
+            if (GetComparisonData(item) != providerInfo.Data)
+            {
+                return true;
+            }
+
             return base.NeedsRefreshInternal(item, providerInfo);
         }
 
+        /// <summary>
+        /// Gets the comparison data.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>Guid.</returns>
+        private Guid GetComparisonData(BaseItem item)
+        {
+            var musicBrainzId = item.GetProviderId(MetadataProviders.Musicbrainz);
+
+            if (!string.IsNullOrEmpty(musicBrainzId))
+            {
+                // Process images
+                var path = GetArtistDataPath(ConfigurationManager.ApplicationPaths, musicBrainzId);
+
+                var files = new DirectoryInfo(path)
+                    .EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)
+                    .Select(i => i.FullName + i.LastWriteTimeUtc.Ticks)
+                    .ToArray();
+
+                if (files.Length > 0)
+                {
+                    return string.Join(string.Empty, files).GetMD5();
+                }
+            }
+
+            return Guid.Empty;
+        }
+
+        /// <summary>
+        /// The us culture
+        /// </summary>
         protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
         /// <summary>
@@ -138,7 +206,7 @@ namespace MediaBrowser.Controller.Providers.Music
 
             return dataPath;
         }
-        
+
         /// <summary>
         /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
         /// </summary>
@@ -150,15 +218,57 @@ namespace MediaBrowser.Controller.Providers.Music
         {
             cancellationToken.ThrowIfCancellationRequested();
 
-            //var artist = item;
-
             var musicBrainzId = item.GetProviderId(MetadataProviders.Musicbrainz);
-            var url = string.Format(FanArtBaseUrl, ApiKey, musicBrainzId);
 
-            var status = ProviderRefreshStatus.Success;
+            var artistDataPath = GetArtistDataPath(ConfigurationManager.ApplicationPaths, musicBrainzId);
+            var xmlPath = Path.Combine(artistDataPath, "fanart.xml");
+
+            // Only download the xml if it doesn't already exist. The prescan task will take care of getting updates
+            if (!File.Exists(xmlPath))
+            {
+                await DownloadArtistXml(artistDataPath, musicBrainzId, cancellationToken).ConfigureAwait(false);
+            }
 
-            var xmlPath = Path.Combine(GetArtistDataPath(ConfigurationManager.ApplicationPaths, musicBrainzId), "fanart.xml");
+            if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Art ||
+              ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops ||
+              ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner ||
+              ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo ||
+              ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary)
+            {
+                if (File.Exists(xmlPath))
+                {
+                    await FetchFromXml(item, xmlPath, cancellationToken).ConfigureAwait(false);
+                }
+            }
+
+            BaseProviderInfo data;
+            if (!item.ProviderData.TryGetValue(Id, out data))
+            {
+                data = new BaseProviderInfo();
+                item.ProviderData[Id] = data;
+            }
+
+            data.Data = GetComparisonData(item);
             
+            SetLastRefreshed(item, DateTime.UtcNow);
+            return true;
+        }
+
+        /// <summary>
+        /// Downloads the artist XML.
+        /// </summary>
+        /// <param name="artistPath">The artist path.</param>
+        /// <param name="musicBrainzId">The music brainz id.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{System.Boolean}.</returns>
+        internal async Task DownloadArtistXml(string artistPath, string musicBrainzId, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var url = string.Format(FanArtBaseUrl, ApiKey, musicBrainzId);
+
+            var xmlPath = Path.Combine(artistPath, "fanart.xml");
+
             using (var response = await HttpClient.Get(new HttpRequestOptions
             {
                 Url = url,
@@ -172,98 +282,97 @@ namespace MediaBrowser.Controller.Providers.Music
                     await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
                 }
             }
-
+        }
+        
+        /// <summary>
+        /// Fetches from XML.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="xmlFilePath">The XML file path.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task FetchFromXml(BaseItem item, string xmlFilePath, CancellationToken cancellationToken)
+        {
             var doc = new XmlDocument();
-            doc.Load(xmlPath);
+            doc.Load(xmlFilePath);
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            if (doc.HasChildNodes)
+            string path;
+            var hd = ConfigurationManager.Configuration.DownloadHDFanArt ? "hd" : "";
+            if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo && !item.HasImage(ImageType.Logo))
             {
-                string path;
-                var hd = ConfigurationManager.Configuration.DownloadHDFanArt ? "hd" : "";
-                if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Logo && !item.HasImage(ImageType.Logo))
+                var node =
+                    doc.SelectSingleNode("//fanart/music/musiclogos/" + hd + "musiclogo/@url") ??
+                    doc.SelectSingleNode("//fanart/music/musiclogos/musiclogo/@url");
+                path = node != null ? node.Value : null;
+                if (!string.IsNullOrEmpty(path))
                 {
-                    var node =
-                        doc.SelectSingleNode("//fanart/music/musiclogos/" + hd + "musiclogo/@url") ??
-                        doc.SelectSingleNode("//fanart/music/musiclogos/musiclogo/@url");
-                    path = node != null ? node.Value : null;
-                    if (!string.IsNullOrEmpty(path))
-                    {
-                        Logger.Debug("FanArtProvider getting ClearLogo for " + item.Name);
-                        item.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(item, path, LogoFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
-                    }
+                    item.SetImage(ImageType.Logo, await _providerManager.DownloadAndSaveImage(item, path, LogoFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                 }
-                cancellationToken.ThrowIfCancellationRequested();
+            }
+            cancellationToken.ThrowIfCancellationRequested();
 
-                if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops && item.BackdropImagePaths.Count == 0)
+            if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Backdrops && item.BackdropImagePaths.Count == 0)
+            {
+                var nodes = doc.SelectNodes("//fanart/music/artistbackgrounds//@url");
+                if (nodes != null)
                 {
-                    var nodes = doc.SelectNodes("//fanart/music/artistbackgrounds//@url");
-                    if (nodes != null)
+                    var numBackdrops = 0;
+                    item.BackdropImagePaths = new List<string>();
+                    foreach (XmlNode node in nodes)
                     {
-                        var numBackdrops = 0;
-                        item.BackdropImagePaths = new List<string>();
-                        foreach (XmlNode node in nodes)
+                        path = node.Value;
+                        if (!string.IsNullOrEmpty(path))
                         {
-                            path = node.Value;
-                            if (!string.IsNullOrEmpty(path))
-                            {
-                                Logger.Debug("FanArtProvider getting Backdrop for " + item.Name);
-                                item.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(item, path, ("Backdrop" + (numBackdrops > 0 ? numBackdrops.ToString(UsCulture) : "") + ".jpg"), SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
-                                numBackdrops++;
-                                if (numBackdrops >= ConfigurationManager.Configuration.MaxBackdrops) break;
-                            }
+                            item.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(item, path, ("Backdrop" + (numBackdrops > 0 ? numBackdrops.ToString(UsCulture) : "") + ".jpg"), SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
+                            numBackdrops++;
+                            if (numBackdrops >= ConfigurationManager.Configuration.MaxBackdrops) break;
                         }
-
                     }
 
                 }
 
-                cancellationToken.ThrowIfCancellationRequested();
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
 
-                if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Art && !item.HasImage(ImageType.Art))
+            if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Art && !item.HasImage(ImageType.Art))
+            {
+                var node =
+                    doc.SelectSingleNode("//fanart/music/musicarts/" + hd + "musicart/@url") ??
+                    doc.SelectSingleNode("//fanart/music/musicarts/musicart/@url");
+                path = node != null ? node.Value : null;
+                if (!string.IsNullOrEmpty(path))
                 {
-                    var node =
-                        doc.SelectSingleNode("//fanart/music/musicarts/" + hd + "musicart/@url") ??
-                        doc.SelectSingleNode("//fanart/music/musicarts/musicart/@url");
-                    path = node != null ? node.Value : null;
-                    if (!string.IsNullOrEmpty(path))
-                    {
-                        Logger.Debug("FanArtProvider getting ClearArt for " + item.Name);
-                        item.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(item, path, ArtFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
-                    }
+                    item.SetImage(ImageType.Art, await _providerManager.DownloadAndSaveImage(item, path, ArtFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                 }
-                cancellationToken.ThrowIfCancellationRequested();
+            }
+            cancellationToken.ThrowIfCancellationRequested();
 
-                if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner && !item.HasImage(ImageType.Banner))
+            if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Banner && !item.HasImage(ImageType.Banner))
+            {
+                var node = doc.SelectSingleNode("//fanart/music/musicbanners/" + hd + "musicbanner/@url") ??
+                           doc.SelectSingleNode("//fanart/music/musicbanners/musicbanner/@url");
+                path = node != null ? node.Value : null;
+                if (!string.IsNullOrEmpty(path))
                 {
-                    var node = doc.SelectSingleNode("//fanart/music/musicbanners/" + hd + "musicbanner/@url") ??
-                               doc.SelectSingleNode("//fanart/music/musicbanners/musicbanner/@url");
-                    path = node != null ? node.Value : null;
-                    if (!string.IsNullOrEmpty(path))
-                    {
-                        Logger.Debug("FanArtProvider getting Banner for " + item.Name);
-                        item.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(item, path, BannerFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
-                    }
+                    item.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(item, path, BannerFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                 }
+            }
 
-                cancellationToken.ThrowIfCancellationRequested();
+            cancellationToken.ThrowIfCancellationRequested();
 
-                // Artist thumbs are actually primary images (they are square/portrait)
-                if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary && !item.HasImage(ImageType.Primary))
+            // Artist thumbs are actually primary images (they are square/portrait)
+            if (ConfigurationManager.Configuration.DownloadMusicArtistImages.Primary && !item.HasImage(ImageType.Primary))
+            {
+                var node = doc.SelectSingleNode("//fanart/music/artistthumbs/artistthumb/@url");
+                path = node != null ? node.Value : null;
+                if (!string.IsNullOrEmpty(path))
                 {
-                    var node = doc.SelectSingleNode("//fanart/music/artistthumbs/artistthumb/@url");
-                    path = node != null ? node.Value : null;
-                    if (!string.IsNullOrEmpty(path))
-                    {
-                        Logger.Debug("FanArtProvider getting Primary image for " + item.Name);
-                        item.SetImage(ImageType.Primary, await _providerManager.DownloadAndSaveImage(item, path, PrimaryFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
-                    }
+                    item.SetImage(ImageType.Primary, await _providerManager.DownloadAndSaveImage(item, path, PrimaryFile, SaveLocalMeta, FanArtResourcePool, cancellationToken).ConfigureAwait(false));
                 }
             }
-
-            SetLastRefreshed(item, DateTime.UtcNow, status);
-            return true;
         }
     }
 }

+ 199 - 0
MediaBrowser.Controller/Providers/Music/FanArtUpdatesPrescanTask.cs

@@ -0,0 +1,199 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Music
+{
+    class FanArtUpdatesPrescanTask : ILibraryPrescanTask
+    {
+        private const string UpdatesUrl = "http://api.fanart.tv/webservice/newmusic/{0}/{1}/";
+
+        /// <summary>
+        /// The _HTTP client
+        /// </summary>
+        private readonly IHttpClient _httpClient;
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+        /// <summary>
+        /// The _config
+        /// </summary>
+        private readonly IServerConfigurationManager _config;
+        private readonly IJsonSerializer _jsonSerializer;
+
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+        public FanArtUpdatesPrescanTask(IJsonSerializer jsonSerializer, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient)
+        {
+            _jsonSerializer = jsonSerializer;
+            _config = config;
+            _logger = logger;
+            _httpClient = httpClient;
+        }
+
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var path = FanArtArtistProvider.GetArtistDataPath(_config.CommonApplicationPaths);
+
+            var timestampFile = Path.Combine(path, "time.txt");
+
+            var timestampFileInfo = new FileInfo(timestampFile);
+
+            if (_config.Configuration.MetadataRefreshDays > 0 && timestampFileInfo.Exists && (DateTime.UtcNow - timestampFileInfo.LastWriteTimeUtc).TotalDays < _config.Configuration.MetadataRefreshDays)
+            {
+                return;
+            }
+
+            // Find out the last time we queried tvdb for updates
+            var lastUpdateTime = timestampFileInfo.Exists ? File.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty;
+
+            var existingDirectories = Directory.EnumerateDirectories(path).Select(Path.GetFileName).ToList();
+
+            // If this is our first time, don't do any updates and just record the timestamp
+            if (!string.IsNullOrEmpty(lastUpdateTime))
+            {
+                var artistsToUpdate = await GetArtistIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false);
+
+                progress.Report(5);
+
+                await UpdateArtists(artistsToUpdate, path, progress, cancellationToken).ConfigureAwait(false);
+            }
+
+            var newUpdateTime = Convert.ToInt64(DateTimeToUnixTimestamp(DateTime.UtcNow)).ToString(UsCulture);
+            
+            File.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8);
+
+            progress.Report(100);
+        }
+
+        /// <summary>
+        /// Gets the artist ids to update.
+        /// </summary>
+        /// <param name="existingSeriesIds">The existing series ids.</param>
+        /// <param name="lastUpdateTime">The last update time.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{System.String}}.</returns>
+        private async Task<IEnumerable<string>> GetArtistIdsToUpdate(IEnumerable<string> existingSeriesIds, string lastUpdateTime, CancellationToken cancellationToken)
+        {
+            // First get last time
+            using (var stream = await _httpClient.Get(new HttpRequestOptions
+            {
+                Url = string.Format(UpdatesUrl, FanartBaseProvider.ApiKey, lastUpdateTime),
+                CancellationToken = cancellationToken,
+                EnableHttpCompression = true,
+                ResourcePool = FanartBaseProvider.FanArtResourcePool
+
+            }).ConfigureAwait(false))
+            {
+                // If empty fanart will return a string of "null", rather than an empty list
+                using (var reader = new StreamReader(stream))
+                {
+                    var json = await reader.ReadToEndAsync().ConfigureAwait(false);
+
+                    if (string.Equals(json, "null", StringComparison.OrdinalIgnoreCase))
+                    {
+                        return new List<string>();
+                    }
+
+                    var updates = _jsonSerializer.DeserializeFromString<List<FanArtUpdate>>(json);
+
+                    return updates.Select(i => i.id).Where(i => existingSeriesIds.Contains(i, StringComparer.OrdinalIgnoreCase));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Updates the artists.
+        /// </summary>
+        /// <param name="idList">The id list.</param>
+        /// <param name="artistsDataPath">The artists data path.</param>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task UpdateArtists(IEnumerable<string> idList, string artistsDataPath, IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var list = idList.ToList();
+            var numComplete = 0;
+
+            foreach (var id in list)
+            {
+                try
+                {
+                    await UpdateArtist(id, artistsDataPath, cancellationToken).ConfigureAwait(false);
+                }
+                catch (HttpException ex)
+                {
+                    // Already logged at lower levels, but don't fail the whole operation, unless something other than a timeout
+                    if (!ex.IsTimedOut)
+                    {
+                        throw;
+                    }
+                }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= list.Count;
+                percent *= 95;
+
+                progress.Report(percent + 5);
+            }
+        }
+
+        /// <summary>
+        /// Updates the artist.
+        /// </summary>
+        /// <param name="musicBrainzId">The musicBrainzId.</param>
+        /// <param name="artistsDataPath">The artists data path.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private Task UpdateArtist(string musicBrainzId, string artistsDataPath, CancellationToken cancellationToken)
+        {
+            _logger.Info("Updating artist " + musicBrainzId);
+
+            artistsDataPath = Path.Combine(artistsDataPath, musicBrainzId);
+
+            if (!Directory.Exists(artistsDataPath))
+            {
+                Directory.CreateDirectory(artistsDataPath);
+            }
+
+            return FanArtArtistProvider.Current.DownloadArtistXml(artistsDataPath, musicBrainzId, cancellationToken);
+        }
+
+        /// <summary>
+        /// Dates the time to unix timestamp.
+        /// </summary>
+        /// <param name="dateTime">The date time.</param>
+        /// <returns>System.Double.</returns>
+        private static double DateTimeToUnixTimestamp(DateTime dateTime)
+        {
+            return (dateTime - new DateTime(1970, 1, 1).ToUniversalTime()).TotalSeconds;
+        }
+
+        public class FanArtUpdate
+        {
+            public string id { get; set; }
+            public string name { get; set; }
+            public string new_images { get; set; }
+            public string total_images { get; set; }
+        }
+    }
+}

+ 2 - 2
MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs

@@ -185,8 +185,7 @@ namespace MediaBrowser.Controller.Providers.TV
         /// <returns>Guid.</returns>
         private Guid GetComparisonData(BaseItem item)
         {
-            var series = (Series)item;
-            var seriesId = series.GetProviderId(MetadataProviders.Tvdb);
+            var seriesId = item.GetProviderId(MetadataProviders.Tvdb);
 
             if (!string.IsNullOrEmpty(seriesId))
             {
@@ -206,6 +205,7 @@ namespace MediaBrowser.Controller.Providers.TV
 
             return Guid.Empty;
         }
+
         /// <summary>
         /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
         /// </summary>

+ 17 - 5
MediaBrowser.Controller/Providers/TV/TvdbPrescanTask.cs

@@ -103,7 +103,7 @@ namespace MediaBrowser.Controller.Providers.TV
                     newUpdateTime = doc.SafeGetString("//Time");
                 }
 
-                await UpdateSeries(existingDirectories, path, cancellationToken).ConfigureAwait(false);
+                await UpdateSeries(existingDirectories, path, progress, cancellationToken).ConfigureAwait(false);
             }
             else
             {
@@ -111,10 +111,11 @@ namespace MediaBrowser.Controller.Providers.TV
 
                 newUpdateTime = seriesToUpdate.Item2;
 
-                await UpdateSeries(seriesToUpdate.Item1, path, cancellationToken).ConfigureAwait(false);
+                await UpdateSeries(seriesToUpdate.Item1, path, progress, cancellationToken).ConfigureAwait(false);
             }
 
             File.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8);
+            progress.Report(100);
         }
 
         /// <summary>
@@ -158,11 +159,15 @@ namespace MediaBrowser.Controller.Providers.TV
         /// </summary>
         /// <param name="seriesIds">The series ids.</param>
         /// <param name="seriesDataPath">The series data path.</param>
+        /// <param name="progress">The progress.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        private async Task UpdateSeries(IEnumerable<string> seriesIds, string seriesDataPath, CancellationToken cancellationToken)
+        private async Task UpdateSeries(IEnumerable<string> seriesIds, string seriesDataPath, IProgress<double> progress, CancellationToken cancellationToken)
         {
-            foreach (var seriesId in seriesIds)
+            var list = seriesIds.ToList();
+            var numComplete = 0;
+
+            foreach (var seriesId in list)
             {
                 try
                 {
@@ -171,12 +176,19 @@ namespace MediaBrowser.Controller.Providers.TV
                 catch (HttpException ex)
                 {
                     // Already logged at lower levels, but don't fail the whole operation, unless timed out
-
+                    // We have to fail this to make it run again otherwise new episode data could potentially be missing
                     if (ex.IsTimedOut)
                     {
                         throw;
                     }
                 }
+
+                numComplete++;
+                double percent = numComplete;
+                percent /= list.Count;
+                percent *= 100;
+
+                progress.Report(percent);
             }
         }
 

+ 53 - 13
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -252,7 +252,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
             var newSeasonZeroName = ConfigurationManager.Configuration.SeasonZeroDisplayName;
             var seasonZeroNameChanged = !string.Equals(_seasonZeroDisplayName, newSeasonZeroName, StringComparison.CurrentCulture);
-            
+
             RecordConfigurationValues(config);
 
             Task.Run(async () =>
@@ -895,30 +895,28 @@ namespace MediaBrowser.Server.Implementations.Library
 
             await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 
+            progress.Report(.5);
+
             // Start by just validating the children of the root, but go no further
             await RootFolder.ValidateChildren(new Progress<double>(), cancellationToken, recursive: false);
 
+            progress.Report(1);
+
             foreach (var folder in _userManager.Users.Select(u => u.RootFolder).Distinct())
             {
                 await ValidateCollectionFolders(folder, cancellationToken).ConfigureAwait(false);
             }
 
+            progress.Report(2);
+
             // Run prescan tasks
-            foreach (var task in PrescanTasks)
-            {
-                try
-                {
-                    await task.Run(new Progress<double>(), cancellationToken);
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error running prescan task", ex);
-                }
-            }
+            await RunPrescanTasks(progress, cancellationToken).ConfigureAwait(false);
 
+            progress.Report(15);
+            
             var innerProgress = new ActionableProgress<double>();
 
-            innerProgress.RegisterAction(pct => progress.Report(pct * .8));
+            innerProgress.RegisterAction(pct => progress.Report(15 + pct * .65));
 
             // Now validate the entire media library
             await RootFolder.ValidateChildren(innerProgress, cancellationToken, recursive: true).ConfigureAwait(false);
@@ -932,6 +930,48 @@ namespace MediaBrowser.Server.Implementations.Library
             progress.Report(100);
         }
 
+        /// <summary>
+        /// Runs the prescan tasks.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task RunPrescanTasks(IProgress<double> progress, CancellationToken cancellationToken)
+        {
+            var prescanTasks = PrescanTasks.ToList();
+            var progressDictionary = new Dictionary<ILibraryPrescanTask, double>();
+
+            var tasks = prescanTasks.Select(i => Task.Run(async () =>
+            {
+                var innerProgress = new ActionableProgress<double>();
+
+                innerProgress.RegisterAction(pct =>
+                {
+                    lock (progressDictionary)
+                    {
+                        progressDictionary[i] = pct;
+
+                        double percent = progressDictionary.Values.Sum();
+                        percent /= prescanTasks.Count;
+
+                        progress.Report(2 + percent * .13);
+                    }
+                });
+                
+                try
+                {
+                    await i.Run(innerProgress, cancellationToken);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error running prescan task", ex);
+                }
+            }));
+
+            // Run prescan tasks
+            await Task.WhenAll(tasks).ConfigureAwait(false);
+        }
+
         /// <summary>
         /// Validates only the collection folders for a User and goes no further
         /// </summary>