瀏覽代碼

reduce requests against tvdb by getting entire series metadata at once

Luke Pulverenti 12 年之前
父節點
當前提交
f3a7307ebb

+ 12 - 6
MediaBrowser.Controller/Extensions/XmlExtensions.cs

@@ -96,11 +96,15 @@ namespace MediaBrowser.Controller.Extensions
         /// <returns>System.String.</returns>
         public static string SafeGetString(this XmlDocument doc, string path, string defaultString)
         {
-            XmlNode rvalNode = doc.SelectSingleNode(path);
-            if (rvalNode != null && rvalNode.InnerText.Trim().Length > 0)
+            var rvalNode = doc.SelectSingleNode(path);
+
+            if (rvalNode != null)
             {
-                return rvalNode.InnerText;
+                var text = rvalNode.InnerText;
+
+                return !string.IsNullOrWhiteSpace(text) ? text : defaultString;
             }
+
             return defaultString;
         }
 
@@ -124,10 +128,12 @@ namespace MediaBrowser.Controller.Extensions
         /// <returns>System.String.</returns>
         public static string SafeGetString(this XmlNode doc, string path, string defaultValue)
         {
-            XmlNode rvalNode = doc.SelectSingleNode(path);
-            if (rvalNode != null && rvalNode.InnerText.Length > 0)
+            var rvalNode = doc.SelectSingleNode(path);
+            if (rvalNode != null)
             {
-                return rvalNode.InnerText;
+                var text = rvalNode.InnerText;
+
+                return !string.IsNullOrWhiteSpace(text) ? text : defaultValue;
             }
             return defaultValue;
         }

+ 8 - 3
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -11,6 +11,9 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Controller.Library
 {
+    /// <summary>
+    /// Interface ILibraryManager
+    /// </summary>
     public interface ILibraryManager
     {
         /// <summary>
@@ -140,11 +143,13 @@ namespace MediaBrowser.Controller.Library
         /// <param name="resolvers">The resolvers.</param>
         /// <param name="introProviders">The intro providers.</param>
         /// <param name="itemComparers">The item comparers.</param>
+        /// <param name="prescanTasks">The prescan tasks.</param>
         void AddParts(IEnumerable<IResolverIgnoreRule> rules, 
             IEnumerable<IVirtualFolderCreator> pluginFolders, 
             IEnumerable<IItemResolver> resolvers, 
             IEnumerable<IIntroProvider> introProviders, 
-            IEnumerable<IBaseItemComparer> itemComparers);
+            IEnumerable<IBaseItemComparer> itemComparers,
+            IEnumerable<ILibraryPrescanTask> prescanTasks);
 
         /// <summary>
         /// Sorts the specified items.
@@ -160,7 +165,7 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// Ensure supplied item has only one instance throughout
         /// </summary>
-        /// <param name="item"></param>
+        /// <param name="item">The item.</param>
         /// <returns>The proper instance to the item</returns>
         BaseItem GetOrAddByReferenceItem(BaseItem item);
 
@@ -186,7 +191,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task UpdateItem(BaseItem item, CancellationToken cancellationToken);
-        
+
         /// <summary>
         /// Retrieves the item.
         /// </summary>

+ 20 - 0
MediaBrowser.Controller/Library/ILibraryPrescanTask.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Library
+{
+    /// <summary>
+    /// An interface for tasks that run prior to the media library scan
+    /// </summary>
+    public interface ILibraryPrescanTask
+    {
+        /// <summary>
+        /// Runs the specified progress.
+        /// </summary>
+        /// <param name="progress">The progress.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task Run(IProgress<double> progress, CancellationToken cancellationToken);
+    }
+}

+ 16 - 4
MediaBrowser.Controller/Library/TVUtils.cs

@@ -1,7 +1,7 @@
-using System.Globalization;
-using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Resolvers;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
@@ -243,7 +243,7 @@ namespace MediaBrowser.Controller.Library
         /// </summary>
         /// <param name="fullPath">The full path.</param>
         /// <returns>System.String.</returns>
-        public static string SeasonNumberFromEpisodeFile(string fullPath)
+        public static int? GetSeasonNumberFromEpisodeFile(string fullPath)
         {
             string fl = fullPath.ToLower();
             foreach (var r in EpisodeExpressions)
@@ -253,7 +253,19 @@ namespace MediaBrowser.Controller.Library
                 {
                     Group g = m.Groups["seasonnumber"];
                     if (g != null)
-                        return g.Value;
+                    {
+                        var val = g.Value;
+
+                        if (!string.IsNullOrWhiteSpace(val))
+                        {
+                            int num;
+
+                            if (int.TryParse(val, NumberStyles.Integer, UsCulture, out num))
+                            {
+                                return num;
+                            }
+                        }
+                    }
                     return null;
                 }
             }

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

@@ -72,7 +72,9 @@
     <Compile Include="Configuration\IServerConfigurationManager.cs" />
     <Compile Include="Dto\SessionInfoDtoBuilder.cs" />
     <Compile Include="Entities\Audio\MusicAlbumDisc.cs" />
+    <Compile Include="Library\ILibraryPrescanTask.cs" />
     <Compile Include="Providers\Movies\MovieDbImagesProvider.cs" />
+    <Compile Include="Providers\TV\TvdbPrescanTask.cs" />
     <Compile Include="Session\ISessionManager.cs" />
     <Compile Include="Drawing\ImageExtensions.cs" />
     <Compile Include="Drawing\ImageHeader.cs" />

+ 2 - 2
MediaBrowser.Controller/Providers/Movies/MovieDbImagesProvider.cs

@@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Providers.Movies
 
             var status = ProviderRefreshStatus.Success;
 
-            var hasLocalPoster = item.LocationType == LocationType.FileSystem ? item.HasLocalImage("folder") : item.HasImage(ImageType.Primary);
+            var hasLocalPoster = item.HasImage(ImageType.Primary);
 
             //        poster
             if (images.posters != null && images.posters.Count > 0 && (ConfigurationManager.Configuration.RefreshItemImages || !hasLocalPoster))
@@ -290,7 +290,7 @@ namespace MediaBrowser.Controller.Providers.Movies
                 {
                     var bdName = "backdrop" + (i == 0 ? "" : i.ToString(CultureInfo.InvariantCulture));
 
-                    var hasLocalBackdrop = item.LocationType == LocationType.FileSystem ? item.HasLocalImage(bdName) : item.BackdropImagePaths.Count > i;
+                    var hasLocalBackdrop = item.BackdropImagePaths.Count > i;
 
                     if (ConfigurationManager.Configuration.RefreshItemImages || !hasLocalBackdrop)
                     {

+ 191 - 131
MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -22,8 +23,11 @@ namespace MediaBrowser.Controller.Providers.TV
     /// </summary>
     class RemoteEpisodeProvider : BaseMetadataProvider
     {
+        /// <summary>
+        /// The _provider manager
+        /// </summary>
         private readonly IProviderManager _providerManager;
-        
+
         /// <summary>
         /// Gets the HTTP client.
         /// </summary>
@@ -36,6 +40,7 @@ namespace MediaBrowser.Controller.Providers.TV
         /// <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>
         public RemoteEpisodeProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
             : base(logManager, configurationManager)
         {
@@ -80,6 +85,10 @@ namespace MediaBrowser.Controller.Providers.TV
             get { return true; }
         }
 
+        /// <summary>
+        /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
+        /// </summary>
+        /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
         protected override bool RefreshOnFileSystemStampChange
         {
             get
@@ -88,6 +97,30 @@ namespace MediaBrowser.Controller.Providers.TV
             }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether [refresh on version change].
+        /// </summary>
+        /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
+        protected override bool RefreshOnVersionChange
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Gets the provider version.
+        /// </summary>
+        /// <value>The provider version.</value>
+        protected override string ProviderVersion
+        {
+            get
+            {
+                return "1";
+            }
+        }
+
         /// <summary>
         /// Needses the refresh internal.
         /// </summary>
@@ -101,34 +134,102 @@ namespace MediaBrowser.Controller.Providers.TV
                 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 episode = (Episode)item;
+
+            var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+            if (!string.IsNullOrEmpty(seriesId))
+            {
+                // Process images
+                var seriesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml");
+
+                var seriesXmlFileInfo = new FileInfo(seriesXmlPath);
+
+                return GetComparisonData(seriesXmlFileInfo);
+            }
+
+            return Guid.Empty;
+        }
+
+        /// <summary>
+        /// Gets the comparison data.
+        /// </summary>
+        /// <param name="seriesXmlFileInfo">The series XML file info.</param>
+        /// <returns>Guid.</returns>
+        private Guid GetComparisonData(FileInfo seriesXmlFileInfo)
+        {
+            var date = seriesXmlFileInfo.Exists ? seriesXmlFileInfo.LastWriteTimeUtc : DateTime.MinValue;
+
+            var key = date.Ticks + seriesXmlFileInfo.FullName;
+
+            return key.GetMD5();
+        }
+
         /// <summary>
         /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="force">if set to <c>true</c> [force].</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{System.Boolean}.</returns>
         public override async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
         {
+            if (HasLocalMeta(item))
+            {
+                return false;
+            }
+
             cancellationToken.ThrowIfCancellationRequested();
-            
+
             var episode = (Episode)item;
-            if (!HasLocalMeta(episode))
+
+            var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+            if (!string.IsNullOrEmpty(seriesId))
             {
-                var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+                var seriesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml");
+
+                var seriesXmlFileInfo = new FileInfo(seriesXmlPath);
 
-                if (seriesId != null)
+                var status = ProviderRefreshStatus.Success;
+
+                if (seriesXmlFileInfo.Exists)
                 {
-                    var status = await FetchEpisodeData(episode, seriesId, cancellationToken).ConfigureAwait(false);
-                    SetLastRefreshed(item, DateTime.UtcNow, status);
-                    return true;
+                    var xmlDoc = new XmlDocument();
+                    xmlDoc.Load(seriesXmlPath);
+
+                    status = await FetchEpisodeData(xmlDoc, episode, seriesId, cancellationToken).ConfigureAwait(false);
                 }
-                Logger.Info("Episode provider not fetching because series does not have a tvdb id: " + item.Path);
-                return false;
+
+                BaseProviderInfo data;
+                if (!item.ProviderData.TryGetValue(Id, out data))
+                {
+                    data = new BaseProviderInfo();
+                    item.ProviderData[Id] = data;
+                }
+
+                data.Data = GetComparisonData(seriesXmlFileInfo);
+
+                SetLastRefreshed(item, DateTime.UtcNow, status);
+                return true;
             }
-            Logger.Info("Episode provider not fetching because local meta exists or requested to ignore: " + item.Name);
+
+            Logger.Info("Episode provider not fetching because series does not have a tvdb id: " + item.Path);
             return false;
         }
 
@@ -136,162 +237,121 @@ namespace MediaBrowser.Controller.Providers.TV
         /// <summary>
         /// Fetches the episode data.
         /// </summary>
+        /// <param name="seriesXml">The series XML.</param>
         /// <param name="episode">The episode.</param>
         /// <param name="seriesId">The series id.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{System.Boolean}.</returns>
-        private async Task<ProviderRefreshStatus> FetchEpisodeData(Episode episode, string seriesId, CancellationToken cancellationToken)
+        private async Task<ProviderRefreshStatus> FetchEpisodeData(XmlDocument seriesXml, Episode episode, string seriesId, CancellationToken cancellationToken)
         {
-            string location = episode.Path;
-
-            var episodeNumber = episode.IndexNumber ?? TVUtils.GetEpisodeNumberFromFile(location, episode.Season != null);
-
             var status = ProviderRefreshStatus.Success;
 
-            if (episodeNumber == null)
+            if (episode.IndexNumber == null)
             {
-                Logger.Warn("TvDbProvider: Could not determine episode number for: " + episode.Path);
                 return status;
             }
 
-            episode.IndexNumber = episodeNumber;
-            var usingAbsoluteData = false;
-
-            if (string.IsNullOrEmpty(seriesId)) return status;
+            var seasonNumber = episode.ParentIndexNumber ?? TVUtils.GetSeasonNumberFromEpisodeFile(episode.Path);
 
-            var seasonNumber = "";
-            if (episode.Parent is Season)
+            if (seasonNumber == null)
             {
-                seasonNumber = episode.Parent.IndexNumber.ToString();
+                return status;
             }
 
-            if (string.IsNullOrEmpty(seasonNumber))
-                seasonNumber = TVUtils.SeasonNumberFromEpisodeFile(location); // try and extract the season number from the file name for S1E1, 1x04 etc.
-
-            if (!string.IsNullOrEmpty(seasonNumber))
-            {
-                seasonNumber = seasonNumber.TrimStart('0');
-
-                if (string.IsNullOrEmpty(seasonNumber))
-                {
-                    seasonNumber = "0"; // Specials
-                }
-
-                var url = string.Format(EpisodeQuery, TVUtils.TvdbApiKey, seriesId, seasonNumber, episodeNumber, ConfigurationManager.Configuration.PreferredMetadataLanguage);
-                var doc = new XmlDocument();
+            var usingAbsoluteData = false;
 
-                using (var result = await HttpClient.Get(new HttpRequestOptions
-                {
-                    Url = url,
-                    ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool,
-                    CancellationToken = cancellationToken,
-                    EnableResponseCache = true
+            var episodeNode = seriesXml.SelectSingleNode("//Episode[EpisodeNumber='" + episode.IndexNumber.Value + "'][SeasonNumber='" + seasonNumber.Value + "']");
 
-                }).ConfigureAwait(false))
+            if (episodeNode == null)
+            {
+                if (seasonNumber.Value == 1)
                 {
-                    doc.Load(result);
+                    episodeNode = seriesXml.SelectSingleNode("//Episode[absolute_number='" + episode.IndexNumber.Value + "']");
+                    usingAbsoluteData = true;
                 }
+            }
 
-                //episode does not exist under this season, try absolute numbering.
-                //still assuming it's numbered as 1x01
-                //this is basicly just for anime.
-                if (!doc.HasChildNodes && Int32.Parse(seasonNumber) == 1)
-                {
-                    url = string.Format(AbsEpisodeQuery, TVUtils.TvdbApiKey, seriesId, episodeNumber, ConfigurationManager.Configuration.PreferredMetadataLanguage);
+            // If still null, nothing we can do
+            if (episodeNode == null)
+            {
+                return status;
+            }
 
-                    using (var result = await HttpClient.Get(new HttpRequestOptions
-                    {
-                        Url = url,
-                        ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool,
-                        CancellationToken = cancellationToken,
-                        EnableResponseCache = true
+            var doc = new XmlDocument();
+            doc.LoadXml(episodeNode.OuterXml);
 
-                    }).ConfigureAwait(false))
-                    {
-                        if (result != null) doc.Load(result);
-                        usingAbsoluteData = true;
-                    }
-                }
-
-                if (doc.HasChildNodes)
+            if (!episode.HasImage(ImageType.Primary))
+            {
+                var p = doc.SafeGetString("//filename");
+                if (p != null)
                 {
-                    if (!episode.HasImage(ImageType.Primary))
-                    {
-                        var p = doc.SafeGetString("//filename");
-                        if (p != null)
-                        {
-                            if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation);
-
-                            try
-                            {
-                                episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken);
-                            }
-                            catch (HttpException)
-                            {
-                                status = ProviderRefreshStatus.CompletedWithErrors;
-                            }
-                        }
-                    }
+                    if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation);
 
-                    episode.Overview = doc.SafeGetString("//Overview");
-                    if (usingAbsoluteData)
-                        episode.IndexNumber = doc.SafeGetInt32("//absolute_number", -1);
-                    if (episode.IndexNumber < 0)
-                        episode.IndexNumber = doc.SafeGetInt32("//EpisodeNumber");
-
-                    episode.Name = doc.SafeGetString("//EpisodeName");
-                    episode.CommunityRating = doc.SafeGetSingle("//Rating", -1, 10);
-                    var firstAired = doc.SafeGetString("//FirstAired");
-                    DateTime airDate;
-                    if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
+                    try
                     {
-                        episode.PremiereDate = airDate.ToUniversalTime();
-                        episode.ProductionYear = airDate.Year;
+                        episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken);
                     }
-
-                    episode.People.Clear();
-
-                    var actors = doc.SafeGetString("//GuestStars");
-                    if (actors != null)
+                    catch (HttpException)
                     {
-                        foreach (var person in actors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.GuestStar, Name = str }))
-                        {
-                            episode.AddPerson(person);
-                        }
+                        status = ProviderRefreshStatus.CompletedWithErrors;
                     }
+                }
+            }
 
+            episode.Overview = doc.SafeGetString("//Overview");
+            if (usingAbsoluteData)
+                episode.IndexNumber = doc.SafeGetInt32("//absolute_number", -1);
+            if (episode.IndexNumber < 0)
+                episode.IndexNumber = doc.SafeGetInt32("//EpisodeNumber");
+
+            episode.Name = doc.SafeGetString("//EpisodeName");
+            episode.CommunityRating = doc.SafeGetSingle("//Rating", -1, 10);
+            var firstAired = doc.SafeGetString("//FirstAired");
+            DateTime airDate;
+            if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
+            {
+                episode.PremiereDate = airDate.ToUniversalTime();
+                episode.ProductionYear = airDate.Year;
+            }
 
-                    var directors = doc.SafeGetString("//Director");
-                    if (directors != null)
-                    {
-                        foreach (var person in directors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.Director, Name = str }))
-                        {
-                            episode.AddPerson(person);
-                        }
-                    }
+            episode.People.Clear();
 
+            var actors = doc.SafeGetString("//GuestStars");
+            if (actors != null)
+            {
+                foreach (var person in actors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.GuestStar, Name = str }))
+                {
+                    episode.AddPerson(person);
+                }
+            }
 
-                    var writers = doc.SafeGetString("//Writer");
-                    if (writers != null)
-                    {
-                        foreach (var person in writers.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.Writer, Name = str }))
-                        {
-                            episode.AddPerson(person);
-                        }
-                    }
 
-                    if (ConfigurationManager.Configuration.SaveLocalMeta)
-                    {
-                        if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation);
-                        var ms = new MemoryStream();
-                        doc.Save(ms);
+            var directors = doc.SafeGetString("//Director");
+            if (directors != null)
+            {
+                foreach (var person in directors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.Director, Name = str }))
+                {
+                    episode.AddPerson(person);
+                }
+            }
 
-                        await _providerManager.SaveToLibraryFilesystem(episode, Path.Combine(episode.MetaLocation, Path.GetFileNameWithoutExtension(episode.Path) + ".xml"), ms, cancellationToken).ConfigureAwait(false);
-                    }
 
-                    return status;
+            var writers = doc.SafeGetString("//Writer");
+            if (writers != null)
+            {
+                foreach (var person in writers.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.Writer, Name = str }))
+                {
+                    episode.AddPerson(person);
                 }
+            }
+
+            if (ConfigurationManager.Configuration.SaveLocalMeta)
+            {
+                if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation);
+                var ms = new MemoryStream();
+                doc.Save(ms);
 
+                await _providerManager.SaveToLibraryFilesystem(episode, Path.Combine(episode.MetaLocation, Path.GetFileNameWithoutExtension(episode.Path) + ".xml"), ms, cancellationToken).ConfigureAwait(false);
             }
 
             return status;

+ 148 - 127
MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -25,8 +26,19 @@ namespace MediaBrowser.Controller.Providers.TV
         /// <value>The HTTP client.</value>
         protected IHttpClient HttpClient { get; private set; }
 
+        /// <summary>
+        /// The _provider manager
+        /// </summary>
         private readonly IProviderManager _providerManager;
-        
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RemoteSeasonProvider"/> 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 RemoteSeasonProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
             : base(logManager, configurationManager)
         {
@@ -70,6 +82,10 @@ namespace MediaBrowser.Controller.Providers.TV
             }
         }
 
+        /// <summary>
+        /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
+        /// </summary>
+        /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
         protected override bool RefreshOnFileSystemStampChange
         {
             get
@@ -78,6 +94,30 @@ namespace MediaBrowser.Controller.Providers.TV
             }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether [refresh on version change].
+        /// </summary>
+        /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
+        protected override bool RefreshOnVersionChange
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Gets the provider version.
+        /// </summary>
+        /// <value>The provider version.</value>
+        protected override string ProviderVersion
+        {
+            get
+            {
+                return "1";
+            }
+        }
+
         /// <summary>
         /// Needses the refresh internal.
         /// </summary>
@@ -86,14 +126,51 @@ namespace MediaBrowser.Controller.Providers.TV
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
         {
-            if (HasLocalMeta(item))
+            if (GetComparisonData(item) != providerInfo.Data)
             {
-                return false;
+                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 season = (Season)item;
+            var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+            if (!string.IsNullOrEmpty(seriesId))
+            {
+                // Process images
+                var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml");
+
+                var imagesFileInfo = new FileInfo(imagesXmlPath);
+
+                return GetComparisonData(imagesFileInfo);
+            }
+
+            return Guid.Empty;
+        }
+
+        /// <summary>
+        /// Gets the comparison data.
+        /// </summary>
+        /// <param name="imagesFileInfo">The images file info.</param>
+        /// <returns>Guid.</returns>
+        private Guid GetComparisonData(FileInfo imagesFileInfo)
+        {
+            var date = imagesFileInfo.Exists ? imagesFileInfo.LastWriteTimeUtc : DateTime.MinValue;
+
+            var key = date.Ticks + imagesFileInfo.FullName;
+
+            return key.GetMD5();
+        }
+
         /// <summary>
         /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
         /// </summary>
@@ -107,162 +184,106 @@ namespace MediaBrowser.Controller.Providers.TV
 
             var season = (Season)item;
 
-            if (!HasLocalMeta(item))
+            var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+            if (!string.IsNullOrEmpty(seriesId))
             {
-                var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+                // Process images
+                var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml");
+
+                var imagesFileInfo = new FileInfo(imagesXmlPath);
 
-                if (seriesId != null)
+                if (imagesFileInfo.Exists)
                 {
-                    var status = await FetchSeasonData(season, seriesId, cancellationToken).ConfigureAwait(false);
+                    if (!season.HasImage(ImageType.Primary) || !season.HasImage(ImageType.Banner) || season.BackdropImagePaths.Count == 0)
+                    {
+                        var xmlDoc = new XmlDocument();
+                        xmlDoc.Load(imagesXmlPath);
 
-                    SetLastRefreshed(item, DateTime.UtcNow, status);
+                        await FetchImages(season, xmlDoc, cancellationToken).ConfigureAwait(false);
+                    }
+                }
 
-                    return true;
+                BaseProviderInfo data;
+                if (!item.ProviderData.TryGetValue(Id, out data))
+                {
+                    data = new BaseProviderInfo();
+                    item.ProviderData[Id] = data;
                 }
-                Logger.Info("Season provider not fetching because series does not have a tvdb id: " + season.Path);
-            }
-            else
-            {
-                Logger.Info("Season provider not fetching because local meta exists: " + season.Name);
+
+                data.Data = GetComparisonData(imagesFileInfo);
+                
+                SetLastRefreshed(item, DateTime.UtcNow);
+                return true;
             }
+
             return false;
         }
 
-
         /// <summary>
-        /// Fetches the season data.
+        /// Fetches the images.
         /// </summary>
         /// <param name="season">The season.</param>
-        /// <param name="seriesId">The series id.</param>
+        /// <param name="images">The images.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{System.Boolean}.</returns>
-        private async Task<ProviderRefreshStatus> FetchSeasonData(Season season, string seriesId, CancellationToken cancellationToken)
+        /// <returns>Task.</returns>
+        private async Task FetchImages(Season season, XmlDocument images, CancellationToken cancellationToken)
         {
-            var seasonNumber = TVUtils.GetSeasonNumberFromPath(season.Path) ?? -1;
-
-            season.IndexNumber = seasonNumber;
-
-            if (seasonNumber == 0)
-            {
-                season.Name = "Specials";
-            }
-
-            var status = ProviderRefreshStatus.Success;
+            var seasonNumber = season.IndexNumber ?? -1;
 
-            if (string.IsNullOrEmpty(seriesId))
+            if (seasonNumber == -1)
             {
-                return status;
+                return;
             }
 
-            if ((season.PrimaryImagePath == null) || (!season.HasImage(ImageType.Banner)) || (season.BackdropImagePaths == null))
+            if (ConfigurationManager.Configuration.RefreshItemImages || !season.HasImage(ImageType.Primary))
             {
-                var images = new XmlDocument();
-                var url = string.Format("http://www.thetvdb.com/api/" + TVUtils.TvdbApiKey + "/series/{0}/banners.xml", seriesId);
-
-                using (var imgs = await HttpClient.Get(new HttpRequestOptions
+                var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='season'][Season='" + seasonNumber + "'][Language='" + ConfigurationManager.Configuration.PreferredMetadataLanguage + "']") ??
+                        images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='season'][Season='" + seasonNumber + "'][Language='en']");
+                if (n != null)
                 {
-                    Url = url,
-                    ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool,
-                    CancellationToken = cancellationToken,
-                    EnableResponseCache = true
+                    n = n.SelectSingleNode("./BannerPath");
 
-                }).ConfigureAwait(false))
-                {
-                    images.Load(imgs);
+                    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);
                 }
+            }
 
-                if (images.HasChildNodes)
+            if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !season.HasImage(ImageType.Banner)))
+            {
+                var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='seasonwide'][Season='" + seasonNumber + "'][Language='" + ConfigurationManager.Configuration.PreferredMetadataLanguage + "']") ??
+                        images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='seasonwide'][Season='" + seasonNumber + "'][Language='en']");
+                if (n != null)
                 {
-                    if (ConfigurationManager.Configuration.RefreshItemImages || !season.HasLocalImage("folder"))
+                    n = n.SelectSingleNode("./BannerPath");
+                    if (n != null)
                     {
-                        var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='season'][Season='" + seasonNumber + "'][Language='" + ConfigurationManager.Configuration.PreferredMetadataLanguage + "']") ??
-                                images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='season'][Season='" + seasonNumber + "'][Language='en']");
-                        if (n != null)
-                        {
-                            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 bannerImagePath =
+                            await _providerManager.DownloadAndSaveImage(season,
+                                                                             TVUtils.BannerUrl + n.InnerText,
+                                                                             "banner" +
+                                                                             Path.GetExtension(n.InnerText),
+                                                                             ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).
+                                               ConfigureAwait(false);
 
-                    if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !season.HasLocalImage("banner")))
-                    {
-                        var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='seasonwide'][Season='" + seasonNumber + "'][Language='" + ConfigurationManager.Configuration.PreferredMetadataLanguage + "']") ?? 
-                                images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='seasonwide'][Season='" + seasonNumber + "'][Language='en']");
-                        if (n != null)
-                        {
-                            n = n.SelectSingleNode("./BannerPath");
-                            if (n != null)
-                            {
-                                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);
-                            }
-                        }
+                        season.SetImage(ImageType.Banner, bannerImagePath);
                     }
+                }
+            }
 
-                    if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && (ConfigurationManager.Configuration.RefreshItemImages || !season.HasLocalImage("backdrop")))
+            if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && (ConfigurationManager.Configuration.RefreshItemImages || season.BackdropImagePaths.Count == 0))
+            {
+                var n = images.SelectSingleNode("//Banner[BannerType='fanart'][Season='" + seasonNumber + "']");
+                if (n != null)
+                {
+                    n = n.SelectSingleNode("./BannerPath");
+                    if (n != null)
                     {
-                        var n = images.SelectSingleNode("//Banner[BannerType='fanart'][Season='" + seasonNumber + "']");
-                        if (n != null)
-                        {
-                            n = n.SelectSingleNode("./BannerPath");
-                            if (n != null)
-                            {
-                                if (season.BackdropImagePaths == null) season.BackdropImagePaths = new List<string>();
-                                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));
-                            }
-                        }
-                        else if (!ConfigurationManager.Configuration.SaveLocalMeta) //if saving local - season will inherit from series
-                        {
-                            // not necessarily accurate but will give a different bit of art to each season
-                            var lst = images.SelectNodes("//Banner[BannerType='fanart']");
-                            if (lst != null && lst.Count > 0)
-                            {
-                                var num = seasonNumber % lst.Count;
-                                n = lst[num];
-                                n = n.SelectSingleNode("./BannerPath");
-                                if (n != null)
-                                {
-                                    if (season.BackdropImagePaths == null)
-                                        season.BackdropImagePaths = new List<string>();
-
-                                    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));
-                                }
-                            }
-                        }
+                        if (season.BackdropImagePaths == null) season.BackdropImagePaths = new List<string>();
+                        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));
                     }
                 }
             }
-            return status;
-        }
-
-        /// <summary>
-        /// Determines whether [has local meta] [the specified item].
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns><c>true</c> if [has local meta] [the specified item]; otherwise, <c>false</c>.</returns>
-        private bool HasLocalMeta(BaseItem item)
-        {
-            //just folder.jpg/png
-            return (item.ResolveArgs.ContainsMetaFileByName("folder.jpg") ||
-                    item.ResolveArgs.ContainsMetaFileByName("folder.png"));
         }
-
     }
 }

+ 319 - 208
MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -6,12 +7,13 @@ using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Net;
 using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
+using System.Linq;
 using System.Net;
 using System.Text;
 using System.Threading;
@@ -25,15 +27,27 @@ namespace MediaBrowser.Controller.Providers.TV
     /// </summary>
     class RemoteSeriesProvider : BaseMetadataProvider, IDisposable
     {
+        /// <summary>
+        /// The _provider manager
+        /// </summary>
         private readonly IProviderManager _providerManager;
-        
+
         /// <summary>
         /// The tv db
         /// </summary>
-        internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(3, 3);
+        internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(1, 1);
 
+        /// <summary>
+        /// Gets the current.
+        /// </summary>
+        /// <value>The current.</value>
         internal static RemoteSeriesProvider Current { get; private set; }
 
+        /// <summary>
+        /// The _zip client
+        /// </summary>
+        private readonly IZipClient _zipClient;
+
         /// <summary>
         /// Gets the HTTP client.
         /// </summary>
@@ -47,8 +61,9 @@ namespace MediaBrowser.Controller.Providers.TV
         /// <param name="logManager">The log manager.</param>
         /// <param name="configurationManager">The configuration manager.</param>
         /// <param name="providerManager">The provider manager.</param>
+        /// <param name="zipClient">The zip client.</param>
         /// <exception cref="System.ArgumentNullException">httpClient</exception>
-        public RemoteSeriesProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager)
+        public RemoteSeriesProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IZipClient zipClient)
             : base(logManager, configurationManager)
         {
             if (httpClient == null)
@@ -57,6 +72,7 @@ namespace MediaBrowser.Controller.Providers.TV
             }
             HttpClient = httpClient;
             _providerManager = providerManager;
+            _zipClient = zipClient;
             Current = this;
         }
 
@@ -81,13 +97,9 @@ namespace MediaBrowser.Controller.Providers.TV
         /// </summary>
         private const string SeriesQuery = "GetSeries.php?seriesname={0}";
         /// <summary>
-        /// The series get
-        /// </summary>
-        private const string SeriesGet = "http://www.thetvdb.com/api/{0}/series/{1}/{2}.xml";
-        /// <summary>
-        /// The get actors
+        /// The series get zip
         /// </summary>
-        private const string GetActors = "http://www.thetvdb.com/api/{0}/series/{1}/actors.xml";
+        private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip";
 
         /// <summary>
         /// The LOCA l_ MET a_ FIL e_ NAME
@@ -125,6 +137,30 @@ namespace MediaBrowser.Controller.Providers.TV
             }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether [refresh on version change].
+        /// </summary>
+        /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
+        protected override bool RefreshOnVersionChange
+        {
+            get
+            {
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// Gets the provider version.
+        /// </summary>
+        /// <value>The provider version.</value>
+        protected override string ProviderVersion
+        {
+            get
+            {
+                return "1";
+            }
+        }
+
         /// <summary>
         /// Needses the refresh internal.
         /// </summary>
@@ -133,9 +169,43 @@ namespace MediaBrowser.Controller.Providers.TV
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
         protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
         {
-            return !HasLocalMeta(item) && base.NeedsRefreshInternal(item, providerInfo);
+            // Refresh even if local metadata exists because we need episode infos
+            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 series = (Series)item;
+            var seriesId = series.GetProviderId(MetadataProviders.Tvdb);
+
+            if (!string.IsNullOrEmpty(seriesId))
+            {
+                // Process images
+                var path = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId);
+
+                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>
         /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
         /// </summary>
@@ -146,30 +216,40 @@ namespace MediaBrowser.Controller.Providers.TV
         public override async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
         {
             cancellationToken.ThrowIfCancellationRequested();
-            
+
             var series = (Series)item;
-            if (!HasLocalMeta(series))
+
+            var seriesId = series.GetProviderId(MetadataProviders.Tvdb);
+
+            if (string.IsNullOrEmpty(seriesId))
             {
-                var path = item.Path ?? "";
-                var seriesId = Path.GetFileName(path).GetAttributeValue("tvdbid") ?? await GetSeriesId(series, cancellationToken);
+                seriesId = await GetSeriesId(series, cancellationToken);
+            }
 
-                cancellationToken.ThrowIfCancellationRequested();
+            cancellationToken.ThrowIfCancellationRequested();
 
-                var status = ProviderRefreshStatus.Success;
+            var status = ProviderRefreshStatus.Success;
 
-                if (!string.IsNullOrEmpty(seriesId))
-                {
-                    series.SetProviderId(MetadataProviders.Tvdb, seriesId);
+            if (!string.IsNullOrEmpty(seriesId))
+            {
+                series.SetProviderId(MetadataProviders.Tvdb, seriesId);
 
-                    status = await FetchSeriesData(series, seriesId, cancellationToken).ConfigureAwait(false);
-                }
+                var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId);
 
-                SetLastRefreshed(item, DateTime.UtcNow, status);
-                return true;
+                status = await FetchSeriesData(series, seriesId, seriesDataPath, cancellationToken).ConfigureAwait(false);
             }
-            Logger.Info("Series provider not fetching because local meta exists or requested to ignore: " + item.Name);
-            return 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, status);
+            return true;
         }
 
         /// <summary>
@@ -177,263 +257,291 @@ namespace MediaBrowser.Controller.Providers.TV
         /// </summary>
         /// <param name="series">The series.</param>
         /// <param name="seriesId">The series id.</param>
+        /// <param name="seriesDataPath">The series data path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{System.Boolean}.</returns>
-        private async Task<ProviderRefreshStatus> FetchSeriesData(Series series, string seriesId, CancellationToken cancellationToken)
+        private async Task<ProviderRefreshStatus> FetchSeriesData(Series series, string seriesId, string seriesDataPath, CancellationToken cancellationToken)
         {
             var status = ProviderRefreshStatus.Success;
 
-            if (!string.IsNullOrEmpty(seriesId))
+            var files = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).ToArray();
+
+            var seriesXmlFilename = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml";
+
+            // Only download if not already there
+            // The prescan task will take care of updates so we don't need to re-download here
+            if (!files.Contains("banners.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains("actors.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains(seriesXmlFilename, StringComparer.OrdinalIgnoreCase))
+            {
+                await DownloadSeriesZip(seriesId, seriesDataPath, cancellationToken).ConfigureAwait(false);
+            }
+
+            // Only examine the main info if there's no local metadata
+            if (!HasLocalMeta(series))
             {
+                var seriesXmlPath = Path.Combine(seriesDataPath, seriesXmlFilename);
+                var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml");
 
-                string url = string.Format(SeriesGet, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage);
-                var doc = new XmlDocument();
+                var seriesDoc = new XmlDocument();
+                seriesDoc.Load(seriesXmlPath);
 
-                using (var xml = await HttpClient.Get(new HttpRequestOptions
-                {
-                    Url = url,
-                    ResourcePool = TvDbResourcePool,
-                    CancellationToken = cancellationToken,
-                    EnableResponseCache = true
+                FetchMainInfo(series, seriesDoc);
+
+                var actorsDoc = new XmlDocument();
+                actorsDoc.Load(actorsXmlPath);
+
+                FetchActors(series, actorsDoc, seriesDoc);
 
-                }).ConfigureAwait(false))
+                if (ConfigurationManager.Configuration.SaveLocalMeta)
                 {
-                    doc.Load(xml);
+                    var ms = new MemoryStream();
+                    seriesDoc.Save(ms);
+
+                    await _providerManager.SaveToLibraryFilesystem(series, Path.Combine(series.MetaLocation, LocalMetaFileName), ms, cancellationToken).ConfigureAwait(false);
                 }
+            }
 
-                if (doc.HasChildNodes)
-                {
-                    //kick off the actor and image fetch simultaneously
-                    var actorTask = FetchActors(series, seriesId, doc, cancellationToken);
-                    var imageTask = FetchImages(series, seriesId, cancellationToken);
-
-                    series.Name = doc.SafeGetString("//SeriesName");
-                    series.Overview = doc.SafeGetString("//Overview");
-                    series.CommunityRating = doc.SafeGetSingle("//Rating", 0, 10);
-                    series.AirDays = TVUtils.GetAirDays(doc.SafeGetString("//Airs_DayOfWeek"));
-                    series.AirTime = doc.SafeGetString("//Airs_Time");
-
-                    string n = doc.SafeGetString("//banner");
-                    if (!string.IsNullOrWhiteSpace(n) && !series.HasImage(ImageType.Banner))
-                    {
-                        series.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n, "banner" + Path.GetExtension(n), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false));
-                    }
+            // Process images
+            var imagesXmlPath = Path.Combine(seriesDataPath, "banners.xml");
 
-                    string s = doc.SafeGetString("//Network");
+            try
+            {
+                var xmlDoc = new XmlDocument();
+                xmlDoc.Load(imagesXmlPath);
 
-                    if (!string.IsNullOrWhiteSpace(s))
-                    {
-                        series.Studios.Clear();
+                await FetchImages(series, xmlDoc, cancellationToken).ConfigureAwait(false);
+            }
+            catch (HttpException)
+            {
+                // Have the provider try again next time, but don't let it fail here
+                status = ProviderRefreshStatus.CompletedWithErrors;
+            }
 
-                        foreach (var studio in s.Trim().Split('|'))
-                        {
-                            series.AddStudio(studio);
-                        }
-                    }
+            return status;
+        }
+
+        /// <summary>
+        /// Downloads the series zip.
+        /// </summary>
+        /// <param name="seriesId">The series id.</param>
+        /// <param name="seriesDataPath">The series data path.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, CancellationToken cancellationToken)
+        {
+            var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage);
+            
+            using (var zipStream = await HttpClient.Get(new HttpRequestOptions
+            {
+                Url = url,
+                ResourcePool = TvDbResourcePool,
+                CancellationToken = cancellationToken
 
-                    series.OfficialRating = doc.SafeGetString("//ContentRating");
+            }).ConfigureAwait(false))
+            {
+                // Copy to memory stream because we need a seekable stream
+                using (var ms = new MemoryStream())
+                {
+                    await zipStream.CopyToAsync(ms).ConfigureAwait(false);
 
-                    string g = doc.SafeGetString("//Genre");
+                    ms.Position = 0;
+                    _zipClient.ExtractAll(ms, seriesDataPath, true);
+                }
+            }
+        }
 
-                    if (g != null)
-                    {
-                        string[] genres = g.Trim('|').Split('|');
-                        if (g.Length > 0)
-                        {
-                            series.Genres.Clear();
+        /// <summary>
+        /// Gets the series data path.
+        /// </summary>
+        /// <param name="appPaths">The app paths.</param>
+        /// <param name="seriesId">The series id.</param>
+        /// <returns>System.String.</returns>
+        internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId)
+        {
+            var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
 
-                            foreach (var genre in genres)
-                            {
-                                series.AddGenre(genre);
-                            }
-                        }
-                    }
+            if (!Directory.Exists(seriesDataPath))
+            {
+                Directory.CreateDirectory(seriesDataPath);
+            }
 
-                    try
-                    {
-                        //wait for other tasks
-                        await Task.WhenAll(actorTask, imageTask).ConfigureAwait(false);
-                    }
-                    catch (HttpException)
-                    {
-                        status = ProviderRefreshStatus.CompletedWithErrors;
-                    }
+            return seriesDataPath;
+        }
 
-                    if (ConfigurationManager.Configuration.SaveLocalMeta)
-                    {
-                        var ms = new MemoryStream();
-                        doc.Save(ms);
+        /// <summary>
+        /// Gets the series data path.
+        /// </summary>
+        /// <param name="appPaths">The app paths.</param>
+        /// <returns>System.String.</returns>
+        internal static string GetSeriesDataPath(IApplicationPaths appPaths)
+        {
+            var dataPath = Path.Combine(appPaths.DataPath, "tvdb");
 
-                        await _providerManager.SaveToLibraryFilesystem(series, Path.Combine(series.MetaLocation, LocalMetaFileName), ms, cancellationToken).ConfigureAwait(false);
-                    }
-                }
+            if (!Directory.Exists(dataPath))
+            {
+                Directory.CreateDirectory(dataPath);
             }
 
-            return status;
+            return dataPath;
         }
 
         /// <summary>
-        /// Fetches the actors.
+        /// Fetches the main info.
         /// </summary>
         /// <param name="series">The series.</param>
-        /// <param name="seriesId">The series id.</param>
         /// <param name="doc">The doc.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        private async Task FetchActors(Series series, string seriesId, XmlDocument doc, CancellationToken cancellationToken)
+        private void FetchMainInfo(Series series, XmlDocument doc)
         {
-            string urlActors = string.Format(GetActors, TVUtils.TvdbApiKey, seriesId);
-            var docActors = new XmlDocument();
+            series.Name = doc.SafeGetString("//SeriesName");
+            series.Overview = doc.SafeGetString("//Overview");
+            series.CommunityRating = doc.SafeGetSingle("//Rating", 0, 10);
+            series.AirDays = TVUtils.GetAirDays(doc.SafeGetString("//Airs_DayOfWeek"));
+            series.AirTime = doc.SafeGetString("//Airs_Time");
 
-            using (var actors = await HttpClient.Get(new HttpRequestOptions
-            {
-                Url = urlActors,
-                ResourcePool = TvDbResourcePool,
-                CancellationToken = cancellationToken,
-                EnableResponseCache = true
+            string s = doc.SafeGetString("//Network");
 
-            }).ConfigureAwait(false))
+            if (!string.IsNullOrWhiteSpace(s))
             {
-                docActors.Load(actors);
+                series.Studios.Clear();
+
+                foreach (var studio in s.Trim().Split('|'))
+                {
+                    series.AddStudio(studio);
+                }
             }
 
-            if (docActors.HasChildNodes)
+            series.OfficialRating = doc.SafeGetString("//ContentRating");
+
+            string g = doc.SafeGetString("//Genre");
+
+            if (g != null)
             {
-                XmlNode actorsNode = null;
-                if (ConfigurationManager.Configuration.SaveLocalMeta)
+                string[] genres = g.Trim('|').Split('|');
+                if (g.Length > 0)
                 {
-                    //add to the main doc for saving
-                    var seriesNode = doc.SelectSingleNode("//Series");
-                    if (seriesNode != null)
+                    series.Genres.Clear();
+
+                    foreach (var genre in genres)
                     {
-                        actorsNode = doc.CreateNode(XmlNodeType.Element, "Persons", null);
-                        seriesNode.AppendChild(actorsNode);
+                        series.AddGenre(genre);
                     }
                 }
+            }
+        }
 
-                var xmlNodeList = docActors.SelectNodes("Actors/Actor");
-
-                if (xmlNodeList != null)
+        /// <summary>
+        /// Fetches the actors.
+        /// </summary>
+        /// <param name="series">The series.</param>
+        /// <param name="actorsDoc">The actors doc.</param>
+        /// <param name="seriesDoc">The seriesDoc.</param>
+        /// <returns>Task.</returns>
+        private void FetchActors(Series series, XmlDocument actorsDoc, XmlDocument seriesDoc)
+        {
+            XmlNode actorsNode = null;
+            if (ConfigurationManager.Configuration.SaveLocalMeta)
+            {
+                //add to the main seriesDoc for saving
+                var seriesNode = seriesDoc.SelectSingleNode("//Series");
+                if (seriesNode != null)
                 {
-                    series.People.Clear();
+                    actorsNode = seriesDoc.CreateNode(XmlNodeType.Element, "Persons", null);
+                    seriesNode.AppendChild(actorsNode);
+                }
+            }
+
+            var xmlNodeList = actorsDoc.SelectNodes("Actors/Actor");
+
+            if (xmlNodeList != null)
+            {
+                series.People.Clear();
 
-                    foreach (XmlNode p in xmlNodeList)
+                foreach (XmlNode p in xmlNodeList)
+                {
+                    string actorName = p.SafeGetString("Name");
+                    string actorRole = p.SafeGetString("Role");
+                    if (!string.IsNullOrWhiteSpace(actorName))
                     {
-                        string actorName = p.SafeGetString("Name");
-                        string actorRole = p.SafeGetString("Role");
-                        if (!string.IsNullOrWhiteSpace(actorName))
-                        {
-                            series.AddPerson(new PersonInfo { Type = PersonType.Actor, Name = actorName, Role = actorRole });
-
-                            if (ConfigurationManager.Configuration.SaveLocalMeta && actorsNode != null)
-                            {
-                                //create in main doc
-                                var personNode = doc.CreateNode(XmlNodeType.Element, "Person", null);
-                                foreach (XmlNode subNode in p.ChildNodes)
-                                    personNode.AppendChild(doc.ImportNode(subNode, true));
-                                //need to add the type
-                                var typeNode = doc.CreateNode(XmlNodeType.Element, "Type", null);
-                                typeNode.InnerText = PersonType.Actor;
-                                personNode.AppendChild(typeNode);
-                                actorsNode.AppendChild(personNode);
-                            }
+                        series.AddPerson(new PersonInfo { Type = PersonType.Actor, Name = actorName, Role = actorRole });
 
+                        if (ConfigurationManager.Configuration.SaveLocalMeta && actorsNode != null)
+                        {
+                            //create in main seriesDoc
+                            var personNode = seriesDoc.CreateNode(XmlNodeType.Element, "Person", null);
+                            foreach (XmlNode subNode in p.ChildNodes)
+                                personNode.AppendChild(seriesDoc.ImportNode(subNode, true));
+                            //need to add the type
+                            var typeNode = seriesDoc.CreateNode(XmlNodeType.Element, "Type", null);
+                            typeNode.InnerText = PersonType.Actor;
+                            personNode.AppendChild(typeNode);
+                            actorsNode.AppendChild(personNode);
                         }
+
                     }
                 }
             }
         }
 
+        /// <summary>
+        /// The us culture
+        /// </summary>
         protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-        
+
         /// <summary>
         /// Fetches the images.
         /// </summary>
         /// <param name="series">The series.</param>
-        /// <param name="seriesId">The series id.</param>
+        /// <param name="images">The images.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        private async Task FetchImages(Series series, string seriesId, CancellationToken cancellationToken)
+        private async Task FetchImages(Series series, XmlDocument images, CancellationToken cancellationToken)
         {
-            if ((!string.IsNullOrEmpty(seriesId)) && ((series.PrimaryImagePath == null) || (series.BackdropImagePaths == null)))
+            if (ConfigurationManager.Configuration.RefreshItemImages || !series.HasImage(ImageType.Primary))
             {
-                string url = string.Format("http://www.thetvdb.com/api/" + TVUtils.TvdbApiKey + "/series/{0}/banners.xml", seriesId);
-                var images = new XmlDocument();
-
-                try
+                var n = images.SelectSingleNode("//Banner[BannerType='poster']");
+                if (n != null)
                 {
-                    using (var imgs = await HttpClient.Get(new HttpRequestOptions
-                    {
-                        Url = url,
-                        ResourcePool = TvDbResourcePool,
-                        CancellationToken = cancellationToken,
-                        EnableResponseCache = true
-
-                    }).ConfigureAwait(false))
+                    n = n.SelectSingleNode("./BannerPath");
+                    if (n != null)
                     {
-                        images.Load(imgs);
+                        series.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false);
                     }
                 }
-                catch (HttpException ex)
+            }
+
+            if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !series.HasImage(ImageType.Banner)))
+            {
+                var n = images.SelectSingleNode("//Banner[BannerType='series']");
+                if (n != null)
                 {
-                    if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+                    n = n.SelectSingleNode("./BannerPath");
+                    if (n != null)
                     {
-                        // If a series has no images this will produce a 404.
-                        // Return gracefully so we don't keep retrying on subsequent scans
-                        return;
-                    }
+                        var bannerImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken);
 
-                    throw;
+                        series.SetImage(ImageType.Banner, bannerImagePath);
+                    }
                 }
+            }
 
-                if (images.HasChildNodes)
+            if (series.BackdropImagePaths.Count < ConfigurationManager.Configuration.MaxBackdrops)
+            {
+                var bdNo = 0;
+                var xmlNodeList = images.SelectNodes("//Banner[BannerType='fanart']");
+                if (xmlNodeList != null)
                 {
-                    if (ConfigurationManager.Configuration.RefreshItemImages || !series.HasLocalImage("folder"))
+                    foreach (XmlNode b in xmlNodeList)
                     {
-                        var n = images.SelectSingleNode("//Banner[BannerType='poster']");
-                        if (n != null)
-                        {
-                            n = n.SelectSingleNode("./BannerPath");
-                            if (n != null)
-                            {
-                                series.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false);
-                            }
-                        }
-                    }
+                        var p = b.SelectSingleNode("./BannerPath");
 
-                    if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !series.HasLocalImage("banner")))
-                    {
-                        var n = images.SelectSingleNode("//Banner[BannerType='series']");
-                        if (n != null)
+                        if (p != null)
                         {
-                            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, TvDbResourcePool, cancellationToken);
-
-                                series.SetImage(ImageType.Banner, bannerImagePath);
-                            }
+                            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, TvDbResourcePool, cancellationToken).ConfigureAwait(false));
+                            bdNo++;
                         }
-                    }
 
-                    var bdNo = 0;
-                    var xmlNodeList = images.SelectNodes("//Banner[BannerType='fanart']");
-                    if (xmlNodeList != null)
-                        foreach (XmlNode b in xmlNodeList)
-                        {
-                            series.BackdropImagePaths = new List<string>();
-                            var p = b.SelectSingleNode("./BannerPath");
-                            if (p != null)
-                            {
-                                var bdName = "backdrop" + (bdNo > 0 ? bdNo.ToString(UsCulture) : "");
-                                if (ConfigurationManager.Configuration.RefreshItemImages || !series.HasLocalImage(bdName))
-                                {
-                                    series.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + p.InnerText, bdName + Path.GetExtension(p.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false));
-                                }
-                                bdNo++;
-                                if (bdNo >= ConfigurationManager.Configuration.MaxBackdrops) break;
-                            }
-                        }
+                        if (series.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops) break;
+                    }
                 }
             }
         }
@@ -573,6 +681,9 @@ namespace MediaBrowser.Controller.Providers.TV
             return name.Trim();
         }
 
+        /// <summary>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
         public void Dispose()
         {
             Dispose(true);

+ 204 - 0
MediaBrowser.Controller/Providers/TV/TvdbPrescanTask.cs

@@ -0,0 +1,204 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+    /// <summary>
+    /// Class TvdbPrescanTask
+    /// </summary>
+    public class TvdbPrescanTask : ILibraryPrescanTask
+    {
+        /// <summary>
+        /// The server time URL
+        /// </summary>
+        private const string ServerTimeUrl = "http://thetvdb.com/api/Updates.php?type=none";
+
+        /// <summary>
+        /// The updates URL
+        /// </summary>
+        private const string UpdatesUrl = "http://thetvdb.com/api/Updates.php?type=all&time={0}";
+
+        /// <summary>
+        /// The _HTTP client
+        /// </summary>
+        private readonly IHttpClient _httpClient;
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+        /// <summary>
+        /// The _config
+        /// </summary>
+        private readonly IConfigurationManager _config;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TvdbPrescanTask"/> class.
+        /// </summary>
+        /// <param name="logger">The logger.</param>
+        /// <param name="httpClient">The HTTP client.</param>
+        /// <param name="config">The config.</param>
+        public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IConfigurationManager config)
+        {
+            _logger = logger;
+            _httpClient = httpClient;
+            _config = config;
+        }
+
+        /// <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 = RemoteSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths);
+
+            var timestampFile = Path.Combine(path, "time.txt");
+
+            var timestampFileInfo = new FileInfo(timestampFile);
+
+            // Don't check for tvdb updates anymore frequently than 24 hours
+            if (timestampFileInfo.Exists && (DateTime.UtcNow - timestampFileInfo.LastWriteTimeUtc).TotalDays < 1)
+            {
+                return;
+            }
+
+            // Find out the last time we queried tvdb for updates
+            var lastUpdateTime = timestampFileInfo.Exists ? File.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty;
+
+            string newUpdateTime;
+
+            var existingDirectories = Directory.EnumerateDirectories(path).Select(Path.GetFileName).ToList();
+
+            // If this is our first time, update all series
+            if (string.IsNullOrEmpty(lastUpdateTime))
+            {
+                // First get tvdb server time
+                using (var stream = await _httpClient.Get(new HttpRequestOptions
+                {
+                    Url = ServerTimeUrl,
+                    CancellationToken = cancellationToken,
+                    EnableHttpCompression = true,
+                    ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool
+
+                }).ConfigureAwait(false))
+                {
+                    var doc = new XmlDocument();
+
+                    doc.Load(stream);
+
+                    newUpdateTime = doc.SafeGetString("//Time");
+                }
+
+                await UpdateSeries(existingDirectories, path, cancellationToken).ConfigureAwait(false);
+            }
+            else
+            {
+                var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false);
+
+                newUpdateTime = seriesToUpdate.Item2;
+
+                await UpdateSeries(seriesToUpdate.Item1, path, cancellationToken).ConfigureAwait(false);
+            }
+
+            File.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8);
+        }
+
+        /// <summary>
+        /// Gets the series 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<Tuple<IEnumerable<string>, string>> GetSeriesIdsToUpdate(IEnumerable<string> existingSeriesIds, string lastUpdateTime, CancellationToken cancellationToken)
+        {
+            // First get last time
+            using (var stream = await _httpClient.Get(new HttpRequestOptions
+            {
+                Url = string.Format(UpdatesUrl, lastUpdateTime),
+                CancellationToken = cancellationToken,
+                EnableHttpCompression = true,
+                ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool
+
+            }).ConfigureAwait(false))
+            {
+                var doc = new XmlDocument();
+
+                doc.Load(stream);
+
+                var newUpdateTime = doc.SafeGetString("//Time");
+
+                var seriesNodes = doc.SelectNodes("//Series");
+
+                var seriesList = seriesNodes == null ? new string[] { } :
+                    seriesNodes.Cast<XmlNode>()
+                    .Select(i => i.InnerText)
+                    .Where(i => !string.IsNullOrWhiteSpace(i) && existingSeriesIds.Contains(i, StringComparer.OrdinalIgnoreCase));
+
+                return new Tuple<IEnumerable<string>, string>(seriesList, newUpdateTime);
+            }
+        }
+
+        /// <summary>
+        /// Updates the series.
+        /// </summary>
+        /// <param name="seriesIds">The series ids.</param>
+        /// <param name="seriesDataPath">The series data path.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task UpdateSeries(IEnumerable<string> seriesIds, string seriesDataPath, CancellationToken cancellationToken)
+        {
+            foreach (var seriesId in seriesIds)
+            {
+                try
+                {
+                    await UpdateSeries(seriesId, seriesDataPath, cancellationToken).ConfigureAwait(false);
+                }
+                catch (HttpException ex)
+                {
+                    // Already logged at lower levels, but don't fail the whole operation, unless timed out
+
+                    if (ex.IsTimedOut)
+                    {
+                        throw;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Updates the series.
+        /// </summary>
+        /// <param name="id">The id.</param>
+        /// <param name="seriesDataPath">The series data path.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private Task UpdateSeries(string id, string seriesDataPath, CancellationToken cancellationToken)
+        {
+            _logger.Info("Updating series " + id);
+
+            seriesDataPath = Path.Combine(seriesDataPath, id);
+
+            if (!Directory.Exists(seriesDataPath))
+            {
+                Directory.CreateDirectory(seriesDataPath);
+            }
+            
+            return RemoteSeriesProvider.Current.DownloadSeriesZip(id, seriesDataPath, cancellationToken);
+        }
+    }
+}

+ 18 - 1
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -31,6 +31,8 @@ namespace MediaBrowser.Server.Implementations.Library
     /// </summary>
     public class LibraryManager : ILibraryManager
     {
+        private IEnumerable<ILibraryPrescanTask> PrescanTasks { get; set; }
+        
         /// <summary>
         /// Gets the intro providers.
         /// </summary>
@@ -161,13 +163,15 @@ namespace MediaBrowser.Server.Implementations.Library
             IEnumerable<IVirtualFolderCreator> pluginFolders,
             IEnumerable<IItemResolver> resolvers,
             IEnumerable<IIntroProvider> introProviders,
-            IEnumerable<IBaseItemComparer> itemComparers)
+            IEnumerable<IBaseItemComparer> itemComparers,
+            IEnumerable<ILibraryPrescanTask> prescanTasks)
         {
             EntityResolutionIgnoreRules = rules;
             PluginFolderCreators = pluginFolders;
             EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
             IntroProviders = introProviders;
             Comparers = itemComparers;
+            PrescanTasks = prescanTasks;
         }
 
         /// <summary>
@@ -841,6 +845,19 @@ namespace MediaBrowser.Server.Implementations.Library
                 await ValidateCollectionFolders(folder, cancellationToken).ConfigureAwait(false);
             }
 
+            // 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);
+                }
+            }
+
             var innerProgress = new ActionableProgress<double>();
 
             innerProgress.RegisterAction(pct => progress.Report(pct * .8));

+ 23 - 10
MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs

@@ -18,41 +18,54 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV
         /// <returns>Episode.</returns>
         protected override Episode Resolve(ItemResolveArgs args)
         {
-            var isInSeason = args.Parent is Season;
+            var season = args.Parent as Season;
 
             // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
-            if (isInSeason || args.Parent is Series)
+            if (season != null || args.Parent is Series)
             {
+                Episode episode = null;
+
                 if (args.IsDirectory)
                 {
                     if (args.ContainsFileSystemEntryByName("video_ts"))
                     {
-                        return new Episode
+                        episode = new Episode
                         {
-                            IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, isInSeason),
                             Path = args.Path,
                             VideoType = VideoType.Dvd
                         };
                     }
                     if (args.ContainsFileSystemEntryByName("bdmv"))
                     {
-                        return new Episode
+                        episode = new Episode
                         {
-                            IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, isInSeason),
                             Path = args.Path,
                             VideoType = VideoType.BluRay
                         };
                     }
                 }
 
-                var episide = base.Resolve(args);
+                if (episode == null)
+                {
+                    episode = base.Resolve(args);
+                }
 
-                if (episide != null)
+                if (episode != null)
                 {
-                    episide.IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, isInSeason);
+                    episode.IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, season != null);
+
+                    if (season != null)
+                    {
+                        episode.ParentIndexNumber = season.IndexNumber;
+                    }
+                    
+                    if (episode.ParentIndexNumber == null)
+                    {
+                        episode.ParentIndexNumber = TVUtils.GetSeasonNumberFromEpisodeFile(args.Path);
+                    }
                 }
 
-                return episide;
+                return episode;
             }
 
             return null;

+ 6 - 1
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -367,7 +367,12 @@ namespace MediaBrowser.ServerApplication
             
             Parallel.Invoke(
 
-                () => LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(), GetExports<IVirtualFolderCreator>(), GetExports<IItemResolver>(), GetExports<IIntroProvider>(), GetExports<IBaseItemComparer>()),
+                () => LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(), 
+                    GetExports<IVirtualFolderCreator>(), 
+                    GetExports<IItemResolver>(), 
+                    GetExports<IIntroProvider>(),
+                    GetExports<IBaseItemComparer>(),
+                    GetExports<ILibraryPrescanTask>()),
 
                 () => ProviderManager.AddMetadataProviders(GetExports<BaseMetadataProvider>().ToArray())