Bladeren bron

convert series providers to new system

Luke Pulverenti 11 jaren geleden
bovenliggende
commit
48b9f657a4

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

@@ -784,7 +784,8 @@ namespace MediaBrowser.Api.Images
                 // Validate first
                 using (var validationStream = new MemoryStream(bytes))
                 {
-                    using (var image = Image.FromStream(validationStream))
+                    // This will throw an exception if it's not a valid image
+                    using (Image.FromStream(validationStream))
                     {
                     }
                 }

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

@@ -224,6 +224,7 @@ namespace MediaBrowser.Model.Configuration
             EnableEpisodeChapterImageExtraction = false;
             EnableOtherVideoChapterImageExtraction = false;
             EnableAutomaticRestart = true;
+            EnablePeoplePrefixSubFolders = true;
 
             MinResumePct = 5;
             MaxResumePct = 90;

+ 1 - 1
MediaBrowser.Providers/BoxSets/BoxSetXmlProvider.cs

@@ -9,7 +9,7 @@ using System.Threading.Tasks;
 namespace MediaBrowser.Providers.BoxSets
 {
     /// <summary>
-    /// Class SeriesProviderFromXml
+    /// Class BoxSetXmlProvider.
     /// </summary>
     public class BoxSetXmlProvider : BaseXmlProvider, ILocalMetadataProvider<BoxSet>
     {

+ 7 - 7
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -107,6 +107,8 @@
     <Compile Include="Music\ArtistMetadataService.cs" />
     <Compile Include="Music\LastfmArtistProvider.cs" />
     <Compile Include="Music\MusicBrainzArtistProvider.cs" />
+    <Compile Include="Omdb\OmdbProvider.cs" />
+    <Compile Include="Omdb\OmdbSeriesProvider.cs" />
     <Compile Include="People\MovieDbPersonImageProvider.cs" />
     <Compile Include="Movies\MovieUpdatesPrescanTask.cs" />
     <Compile Include="Movies\MovieXmlParser.cs" />
@@ -152,22 +154,20 @@
     <Compile Include="TV\EpisodeIndexNumberProvider.cs" />
     <Compile Include="TV\EpisodeProviderFromXml.cs" />
     <Compile Include="TV\EpisodeXmlParser.cs" />
-    <Compile Include="TV\FanArtTVProvider.cs" />
     <Compile Include="TV\FanArtTvUpdatesPrescanTask.cs" />
     <Compile Include="TV\FanartSeasonProvider.cs" />
-    <Compile Include="TV\ManualFanartSeriesProvider.cs" />
-    <Compile Include="TV\ManualTvdbEpisodeImageProvider.cs" />
+    <Compile Include="TV\FanartSeriesProvider.cs" />
+    <Compile Include="TV\SeriesMetadataService.cs" />
+    <Compile Include="TV\TvdbEpisodeImageProvider.cs" />
     <Compile Include="People\TvdbPersonImageProvider.cs" />
     <Compile Include="TV\TvdbSeasonImageProvider.cs" />
-    <Compile Include="TV\ManualTvdbSeriesImageProvider.cs" />
+    <Compile Include="TV\TvdbSeriesImageProvider.cs" />
     <Compile Include="TV\SeasonMetadataService.cs" />
     <Compile Include="TV\TvdbEpisodeProvider.cs" />
-    <Compile Include="TV\TvdbSeriesImageProvider.cs" />
     <Compile Include="TV\TvdbSeriesProvider.cs" />
     <Compile Include="TV\SeasonXmlProvider.cs" />
-    <Compile Include="TV\SeriesDynamicInfoProvider.cs" />
     <Compile Include="TV\SeriesPostScanTask.cs" />
-    <Compile Include="TV\SeriesProviderFromXml.cs" />
+    <Compile Include="TV\SeriesXmlProvider.cs" />
     <Compile Include="TV\SeriesXmlParser.cs" />
     <Compile Include="TV\TvdbPrescanTask.cs" />
     <Compile Include="UserRootFolderNameProvider.cs" />

+ 200 - 0
MediaBrowser.Providers/Omdb/OmdbProvider.cs

@@ -0,0 +1,200 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Omdb
+{
+    public class OmdbProvider
+    {
+        private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
+        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IHttpClient _httpClient;
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+        public OmdbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient)
+        {
+            _jsonSerializer = jsonSerializer;
+            _httpClient = httpClient;
+        }
+
+        public async Task Fetch(BaseItem item, CancellationToken cancellationToken)
+        {
+            var imdbId = item.GetProviderId(MetadataProviders.Imdb);
+
+            if (string.IsNullOrEmpty(imdbId))
+            {
+                return;
+            }
+
+            var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
+
+            var url = string.Format("http://www.omdbapi.com/?i={0}&tomatoes=true", imdbParam);
+
+            using (var stream = await _httpClient.Get(new HttpRequestOptions
+            {
+                Url = url,
+                ResourcePool = _resourcePool,
+                CancellationToken = cancellationToken
+
+            }).ConfigureAwait(false))
+            {
+                var result = _jsonSerializer.DeserializeFromStream<RootObject>(stream);
+
+                var hasCriticRating = item as IHasCriticRating;
+                if (hasCriticRating != null)
+                {
+                    // Seeing some bogus RT data on omdb for series, so filter it out here
+                    // RT doesn't even have tv series
+                    int tomatoMeter;
+
+                    if (!string.IsNullOrEmpty(result.tomatoMeter)
+                        && int.TryParse(result.tomatoMeter, NumberStyles.Integer, _usCulture, out tomatoMeter)
+                        && tomatoMeter >= 0)
+                    {
+                        hasCriticRating.CriticRating = tomatoMeter;
+                    }
+
+                    if (!string.IsNullOrEmpty(result.tomatoConsensus)
+                        && !string.Equals(result.tomatoConsensus, "n/a", StringComparison.OrdinalIgnoreCase)
+                        && !string.Equals(result.tomatoConsensus, "No consensus yet.", StringComparison.OrdinalIgnoreCase))
+                    {
+                        hasCriticRating.CriticRatingSummary = WebUtility.HtmlDecode(result.tomatoConsensus);
+                    }
+                }
+
+                int voteCount;
+
+                if (!string.IsNullOrEmpty(result.imdbVotes)
+                    && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out voteCount)
+                    && voteCount >= 0)
+                {
+                    item.VoteCount = voteCount;
+                }
+
+                float imdbRating;
+
+                if (!string.IsNullOrEmpty(result.imdbRating)
+                    && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out imdbRating)
+                    && imdbRating >= 0)
+                {
+                    item.CommunityRating = imdbRating;
+                }
+
+                if (!string.IsNullOrEmpty(result.Website)
+                        && !string.Equals(result.Website, "n/a", StringComparison.OrdinalIgnoreCase))
+                {
+                    item.HomePageUrl = result.Website;
+                }
+
+                ParseAdditionalMetadata(item, result);
+            }
+        }
+
+        private void ParseAdditionalMetadata(BaseItem item, RootObject result)
+        {
+            // Grab series genres because imdb data is better than tvdb. Leave movies alone
+            // But only do it if english is the preferred language because this data will not be localized
+            if (!item.LockedFields.Contains(MetadataFields.Genres) &&
+                ShouldFetchGenres(item) &&
+                !string.IsNullOrWhiteSpace(result.Genre) &&
+                !string.Equals(result.Genre, "n/a", StringComparison.OrdinalIgnoreCase))
+            {
+                item.Genres.Clear();
+
+                foreach (var genre in result.Genre
+                    .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                    .Select(i => i.Trim())
+                    .Where(i => !string.IsNullOrWhiteSpace(i)))
+                {
+                    item.AddGenre(genre);
+                }
+            }
+
+            var hasMetascore = item as IHasMetascore;
+            if (hasMetascore != null)
+            {
+                float metascore;
+
+                if (!string.IsNullOrEmpty(result.Metascore) && float.TryParse(result.Metascore, NumberStyles.Any, _usCulture, out metascore) && metascore >= 0)
+                {
+                    hasMetascore.Metascore = metascore;
+                }
+            }
+
+            var hasAwards = item as IHasAwards;
+            if (hasAwards != null && !string.IsNullOrEmpty(result.Awards) &&
+                !string.Equals(result.Awards, "n/a", StringComparison.OrdinalIgnoreCase))
+            {
+                hasAwards.AwardSummary = WebUtility.HtmlDecode(result.Awards);
+            }
+        }
+
+        private bool ShouldFetchGenres(BaseItem item)
+        {
+            var lang = item.GetPreferredMetadataLanguage();
+
+            // The data isn't localized and so can only be used for english users
+            if (!string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase))
+            {
+                return false;
+            }
+
+            // Only fetch if other providers didn't get anything
+            if (item is Trailer)
+            {
+                return item.Genres.Count == 0;
+            }
+
+            return item is Series || item is Movie;
+        }
+
+        protected class RootObject
+        {
+            public string Title { get; set; }
+            public string Year { get; set; }
+            public string Rated { get; set; }
+            public string Released { get; set; }
+            public string Runtime { get; set; }
+            public string Genre { get; set; }
+            public string Director { get; set; }
+            public string Writer { get; set; }
+            public string Actors { get; set; }
+            public string Plot { get; set; }
+            public string Poster { get; set; }
+            public string imdbRating { get; set; }
+            public string imdbVotes { get; set; }
+            public string imdbID { get; set; }
+            public string Type { get; set; }
+            public string tomatoMeter { get; set; }
+            public string tomatoImage { get; set; }
+            public string tomatoRating { get; set; }
+            public string tomatoReviews { get; set; }
+            public string tomatoFresh { get; set; }
+            public string tomatoRotten { get; set; }
+            public string tomatoConsensus { get; set; }
+            public string tomatoUserMeter { get; set; }
+            public string tomatoUserRating { get; set; }
+            public string tomatoUserReviews { get; set; }
+            public string DVD { get; set; }
+            public string BoxOffice { get; set; }
+            public string Production { get; set; }
+            public string Website { get; set; }
+            public string Response { get; set; }
+
+            public string Language { get; set; }
+            public string Country { get; set; }
+            public string Awards { get; set; }
+            public string Metascore { get; set; }
+        }
+
+    }
+}

+ 31 - 0
MediaBrowser.Providers/Omdb/OmdbSeriesProvider.cs

@@ -0,0 +1,31 @@
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Omdb
+{
+    public class OmdbSeriesProvider : ICustomMetadataProvider<Series>
+    {
+        private readonly IJsonSerializer _jsonSerializer;
+        private readonly IHttpClient _httpClient;
+
+        public OmdbSeriesProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient)
+        {
+            _jsonSerializer = jsonSerializer;
+            _httpClient = httpClient;
+        }
+
+        public Task FetchAsync(Series item, CancellationToken cancellationToken)
+        {
+            return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken);
+        }
+
+        public string Name
+        {
+            get { return "OMDb"; }
+        }
+    }
+}

+ 5 - 5
MediaBrowser.Providers/TV/FanArtSeasonProvider.cs

@@ -20,14 +20,14 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class FanartSeasonImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor
+    public class FanartSeasonProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
         private readonly IFileSystem _fileSystem;
 
-        public FanartSeasonImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
+        public FanartSeasonProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
         {
             _config = config;
             _httpClient = httpClient;
@@ -78,9 +78,9 @@ namespace MediaBrowser.Providers.TV
 
                 if (!string.IsNullOrEmpty(id) && season.IndexNumber.HasValue)
                 {
-                    await FanArtTvProvider.Current.EnsureSeriesXml(id, cancellationToken).ConfigureAwait(false);
+                    await FanartSeriesProvider.Current.EnsureSeriesXml(id, cancellationToken).ConfigureAwait(false);
 
-                    var xmlPath = FanArtTvProvider.Current.GetFanartXmlPath(id);
+                    var xmlPath = FanartSeriesProvider.Current.GetFanartXmlPath(id);
 
                     try
                     {
@@ -290,7 +290,7 @@ namespace MediaBrowser.Providers.TV
             if (!String.IsNullOrEmpty(tvdbId))
             {
                 // Process images
-                var imagesXmlPath = FanArtTvProvider.Current.GetFanartXmlPath(tvdbId);
+                var imagesXmlPath = FanartSeriesProvider.Current.GetFanartXmlPath(tvdbId);
 
                 var fileInfo = new FileInfo(imagesXmlPath);
 

+ 0 - 331
MediaBrowser.Providers/TV/FanArtTVProvider.cs

@@ -1,331 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Providers;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-using System.Net;
-using MediaBrowser.Providers.Music;
-
-namespace MediaBrowser.Providers.TV
-{
-    class FanArtTvProvider : BaseMetadataProvider
-    {
-        protected string FanArtBaseUrl = "http://api.fanart.tv/webservice/series/{0}/{1}/xml/all/1/1";
-
-        internal static FanArtTvProvider Current { get; private set; }
-
-        /// <summary>
-        /// Gets the HTTP client.
-        /// </summary>
-        /// <value>The HTTP client.</value>
-        protected IHttpClient HttpClient { get; private set; }
-
-        private readonly IProviderManager _providerManager;
-        private readonly IFileSystem _fileSystem;
-
-        public FanArtTvProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem)
-            : base(logManager, configurationManager)
-        {
-            if (httpClient == null)
-            {
-                throw new ArgumentNullException("httpClient");
-            }
-            HttpClient = httpClient;
-            _providerManager = providerManager;
-            _fileSystem = fileSystem;
-            Current = this;
-        }
-
-        public override bool Supports(BaseItem item)
-        {
-            return item is Series;
-        }
-
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Third; }
-        }
-
-        public override ItemUpdateType ItemUpdateType
-        {
-            get
-            {
-                return ItemUpdateType.ImageUpdate;
-            }
-        }
-
-        /// <summary>
-        /// Needses the refresh internal.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="providerInfo">The provider info.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            if (string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb)))
-            {
-                return false;
-            }
-
-            return base.NeedsRefreshInternal(item, providerInfo);
-        }
-
-        protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            var id = item.GetProviderId(MetadataProviders.Tvdb);
-
-            if (!string.IsNullOrEmpty(id))
-            {
-                // Process images
-                var xmlPath = GetFanartXmlPath(id);
-
-                var fileInfo = new FileInfo(xmlPath);
-
-                return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed;
-            }
-
-            return base.NeedsRefreshBasedOnCompareDate(item, providerInfo);
-        }
-
-        /// <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>
-        /// 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);
-
-            return seriesDataPath;
-        }
-
-        /// <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, "fanart-tv");
-
-            return dataPath;
-        }
-
-        public string GetFanartXmlPath(string tvdbId)
-        {
-            var dataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, tvdbId);
-            return Path.Combine(dataPath, "fanart.xml");
-        }
-
-        protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
-        public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            var seriesId = item.GetProviderId(MetadataProviders.Tvdb);
-
-            if (!string.IsNullOrEmpty(seriesId))
-            {
-                var xmlPath = GetFanartXmlPath(seriesId);
-
-                // Only download the xml if it doesn't already exist. The prescan task will take care of getting updates
-                if (!File.Exists(xmlPath))
-                {
-                    await DownloadSeriesXml(seriesId, cancellationToken).ConfigureAwait(false);
-                }
-
-                var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartSeriesImageProvider.ProviderName).ConfigureAwait(false);
-
-                await FetchFromXml(item, images.ToList(), cancellationToken).ConfigureAwait(false);
-            }
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-
-            return true;
-        }
-
-        /// <summary>
-        /// Fetches from XML.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="images">The images.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        private async Task FetchFromXml(BaseItem item, List<RemoteImageInfo> images, CancellationToken cancellationToken)
-        {
-            var options = ConfigurationManager.Configuration.GetMetadataOptions("Series") ?? new MetadataOptions();
-
-            if (!item.LockedFields.Contains(MetadataFields.Images))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (options.IsEnabled(ImageType.Primary) && !item.HasImage(ImageType.Primary))
-                {
-                    await SaveImage(item, images, ImageType.Primary, cancellationToken).ConfigureAwait(false);
-                }
-
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (options.IsEnabled(ImageType.Logo) && !item.HasImage(ImageType.Logo))
-                {
-                    await SaveImage(item, images, ImageType.Logo, cancellationToken).ConfigureAwait(false);
-                }
-
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (options.IsEnabled(ImageType.Art) && !item.HasImage(ImageType.Art))
-                {
-                    await SaveImage(item, images, ImageType.Art, cancellationToken).ConfigureAwait(false);
-                }
-
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (options.IsEnabled(ImageType.Thumb) && !item.HasImage(ImageType.Thumb))
-                {
-                    await SaveImage(item, images, ImageType.Thumb, cancellationToken).ConfigureAwait(false);
-                }
-
-                cancellationToken.ThrowIfCancellationRequested();
-
-                if (options.IsEnabled(ImageType.Banner) && !item.HasImage(ImageType.Banner))
-                {
-                    await SaveImage(item, images, ImageType.Banner, cancellationToken).ConfigureAwait(false);
-                }
-            }
-
-            if (!item.LockedFields.Contains(MetadataFields.Backdrops))
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-
-                var backdropLimit = options.GetLimit(ImageType.Backdrop);
-                if (options.IsEnabled(ImageType.Backdrop) &&
-                    item.BackdropImagePaths.Count < backdropLimit)
-                {
-                    foreach (var image in images.Where(i => i.Type == ImageType.Backdrop))
-                    {
-                        await _providerManager.SaveImage(item, image.Url, FanartArtistProvider.FanArtResourcePool, ImageType.Backdrop, null, cancellationToken)
-                            .ConfigureAwait(false);
-
-                        if (item.BackdropImagePaths.Count >= backdropLimit) break;
-                    }
-                }
-            }
-        }
-
-        private async Task SaveImage(BaseItem item, List<RemoteImageInfo> images, ImageType type, CancellationToken cancellationToken)
-        {
-            foreach (var image in images.Where(i => i.Type == type))
-            {
-                try
-                {
-                    await _providerManager.SaveImage(item, image.Url, FanartArtistProvider.FanArtResourcePool, type, null, cancellationToken).ConfigureAwait(false);
-                    break;
-                }
-                catch (HttpException ex)
-                {
-                    // Sometimes fanart has bad url's in their xml
-                    if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
-                    {
-                        continue;
-                    }
-                    break;
-                }
-            }
-        }
-
-        private readonly Task _cachedTask = Task.FromResult(true);
-        internal Task EnsureSeriesXml(string tvdbId, CancellationToken cancellationToken)
-        {
-            var xmlPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, tvdbId);
-
-            var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath);
-
-            if (fileInfo.Exists)
-            {
-                if (ConfigurationManager.Configuration.EnableFanArtUpdates || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7)
-                {
-                    return _cachedTask;
-                }
-            }
-
-            return DownloadSeriesXml(tvdbId, cancellationToken);
-        }
-
-        /// <summary>
-        /// Downloads the series XML.
-        /// </summary>
-        /// <param name="tvdbId">The TVDB id.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        internal async Task DownloadSeriesXml(string tvdbId, CancellationToken cancellationToken)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            var url = string.Format(FanArtBaseUrl, FanartArtistProvider.ApiKey, tvdbId);
-
-            var xmlPath = GetFanartXmlPath(tvdbId);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(xmlPath));
-
-            using (var response = await HttpClient.Get(new HttpRequestOptions
-            {
-                Url = url,
-                ResourcePool = FanartArtistProvider.FanArtResourcePool,
-                CancellationToken = cancellationToken
-
-            }).ConfigureAwait(false))
-            {
-                using (var xmlFileStream = _fileSystem.GetFileStream(xmlPath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
-                {
-                    await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
-                }
-            }
-        }
-
-    }
-}

+ 3 - 3
MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs

@@ -60,7 +60,7 @@ namespace MediaBrowser.Providers.TV
                 return;
             }
 
-            var path = FanArtTvProvider.GetSeriesDataPath(_config.CommonApplicationPaths);
+            var path = FanartSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths);
 
             Directory.CreateDirectory(path);
             
@@ -149,8 +149,8 @@ namespace MediaBrowser.Providers.TV
             foreach (var id in list)
             {
                 _logger.Info("Updating series " + id);
-                
-                await FanArtTvProvider.Current.DownloadSeriesXml(id, cancellationToken).ConfigureAwait(false);
+
+                await FanartSeriesProvider.Current.DownloadSeriesXml(id, cancellationToken).ConfigureAwait(false);
 
                 numComplete++;
                 double percent = numComplete;

+ 117 - 11
MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs → MediaBrowser.Providers/TV/FanartSeriesProvider.cs

@@ -1,4 +1,6 @@
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
@@ -6,6 +8,7 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Music;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -15,20 +18,27 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
-using MediaBrowser.Providers.Music;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class ManualFanartSeriesImageProvider : IRemoteImageProvider, IHasOrder
+    public class FanartSeriesProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor
     {
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IServerConfigurationManager _config;
         private readonly IHttpClient _httpClient;
+        private readonly IFileSystem _fileSystem;
+
+        protected string FanArtBaseUrl = "http://api.fanart.tv/webservice/series/{0}/{1}/xml/all/1/1";
+
+        internal static FanartSeriesProvider Current { get; private set; }
 
-        public ManualFanartSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
+        public FanartSeriesProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
         {
             _config = config;
             _httpClient = httpClient;
+            _fileSystem = fileSystem;
+
+            Current = this;
         }
 
         public string Name
@@ -66,7 +76,7 @@ namespace MediaBrowser.Providers.TV
             return images.Where(i => i.Type == imageType);
         }
 
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
         {
             var list = new List<RemoteImageInfo>();
 
@@ -76,7 +86,9 @@ namespace MediaBrowser.Providers.TV
 
             if (!string.IsNullOrEmpty(id))
             {
-                var xmlPath = FanArtTvProvider.Current.GetFanartXmlPath(id);
+                await EnsureSeriesXml(id, cancellationToken).ConfigureAwait(false);
+
+                var xmlPath = GetFanartXmlPath(id);
 
                 try
                 {
@@ -93,7 +105,7 @@ namespace MediaBrowser.Providers.TV
             var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
 
             // Sort first by width to prioritize HD versions
-            list = list.OrderByDescending(i => i.Width ?? 0)
+            return list.OrderByDescending(i => i.Width ?? 0)
                 .ThenByDescending(i =>
                 {
                     if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
@@ -114,10 +126,7 @@ namespace MediaBrowser.Providers.TV
                     return 0;
                 })
                 .ThenByDescending(i => i.CommunityRating ?? 0)
-                .ThenByDescending(i => i.VoteCount ?? 0)
-                .ToList();
-
-            return Task.FromResult<IEnumerable<RemoteImageInfo>>(list);
+                .ThenByDescending(i => i.VoteCount ?? 0);
         }
 
         private void AddImages(List<RemoteImageInfo> list, string xmlPath, CancellationToken cancellationToken)
@@ -333,5 +342,102 @@ namespace MediaBrowser.Providers.TV
                 ResourcePool = FanartArtistProvider.FanArtResourcePool
             });
         }
+
+        /// <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);
+
+            return seriesDataPath;
+        }
+
+        /// <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, "fanart-tv");
+
+            return dataPath;
+        }
+
+        public string GetFanartXmlPath(string tvdbId)
+        {
+            var dataPath = GetSeriesDataPath(_config.ApplicationPaths, tvdbId);
+            return Path.Combine(dataPath, "fanart.xml");
+        }
+
+        private readonly Task _cachedTask = Task.FromResult(true);
+        internal Task EnsureSeriesXml(string tvdbId, CancellationToken cancellationToken)
+        {
+            var xmlPath = GetSeriesDataPath(_config.ApplicationPaths, tvdbId);
+
+            var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath);
+
+            if (fileInfo.Exists)
+            {
+                if (_config.Configuration.EnableFanArtUpdates || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7)
+                {
+                    return _cachedTask;
+                }
+            }
+
+            return DownloadSeriesXml(tvdbId, cancellationToken);
+        }
+
+        /// <summary>
+        /// Downloads the series XML.
+        /// </summary>
+        /// <param name="tvdbId">The TVDB id.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        internal async Task DownloadSeriesXml(string tvdbId, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var url = string.Format(FanArtBaseUrl, FanartArtistProvider.ApiKey, tvdbId);
+
+            var xmlPath = GetFanartXmlPath(tvdbId);
+
+            Directory.CreateDirectory(Path.GetDirectoryName(xmlPath));
+
+            using (var response = await _httpClient.Get(new HttpRequestOptions
+            {
+                Url = url,
+                ResourcePool = FanartArtistProvider.FanArtResourcePool,
+                CancellationToken = cancellationToken
+
+            }).ConfigureAwait(false))
+            {
+                using (var xmlFileStream = _fileSystem.GetFileStream(xmlPath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
+                {
+                    await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
+                }
+            }
+        }
+
+        public bool HasChanged(IHasMetadata item, DateTime date)
+        {
+            var tvdbId = item.GetProviderId(MetadataProviders.Tvdb);
+
+            if (!String.IsNullOrEmpty(tvdbId))
+            {
+                // Process images
+                var imagesXmlPath = GetFanartXmlPath(tvdbId);
+
+                var fileInfo = new FileInfo(imagesXmlPath);
+
+                return fileInfo.Exists && _fileSystem.GetLastWriteTimeUtc(fileInfo) > date;
+            }
+
+            return false;
+        }
     }
 }

+ 0 - 335
MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs

@@ -1,335 +0,0 @@
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Xml;
-
-namespace MediaBrowser.Providers.TV
-{
-    public class ManualTvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
-    {
-        private readonly IServerConfigurationManager _config;
-        private readonly IHttpClient _httpClient;
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
-        public ManualTvdbSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
-        {
-            _config = config;
-            _httpClient = httpClient;
-        }
-
-        public string Name
-        {
-            get { return ProviderName; }
-        }
-
-        public static string ProviderName
-        {
-            get { return "TheTVDB"; }
-        }
-
-        public bool Supports(IHasImages item)
-        {
-            return item is Series;
-        }
-
-        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
-        {
-            return new List<ImageType>
-            {
-                ImageType.Primary, 
-                ImageType.Banner,
-                ImageType.Backdrop
-            };
-        }
-
-        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
-        {
-            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
-            return images.Where(i => i.Type == imageType);
-        }
-
-        public Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
-        {
-            var series = (Series)item;
-            var seriesId = series.GetProviderId(MetadataProviders.Tvdb);
-
-            if (!string.IsNullOrEmpty(seriesId))
-            {
-                // Process images
-                var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId);
-
-                var path = Path.Combine(seriesDataPath, "banners.xml");
-
-                try
-                {
-                    var result = GetImages(path, item.GetPreferredMetadataLanguage(), cancellationToken);
-
-                    return Task.FromResult(result);
-                }
-                catch (FileNotFoundException)
-                {
-                    // No tvdb data yet. Don't blow up
-                }
-            }
-
-            return Task.FromResult<IEnumerable<RemoteImageInfo>>(new RemoteImageInfo[] { });
-        }
-
-        private IEnumerable<RemoteImageInfo> GetImages(string xmlPath, string preferredLanguage, CancellationToken cancellationToken)
-        {
-            var settings = new XmlReaderSettings
-            {
-                CheckCharacters = false,
-                IgnoreProcessingInstructions = true,
-                IgnoreComments = true,
-                ValidationType = ValidationType.None
-            };
-
-            var list = new List<RemoteImageInfo>();
-
-            using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
-            {
-                // Use XmlReader for best performance
-                using (var reader = XmlReader.Create(streamReader, settings))
-                {
-                    reader.MoveToContent();
-
-                    // Loop through each element
-                    while (reader.Read())
-                    {
-                        cancellationToken.ThrowIfCancellationRequested();
-
-                        if (reader.NodeType == XmlNodeType.Element)
-                        {
-                            switch (reader.Name)
-                            {
-                                case "Banner":
-                                    {
-                                        using (var subtree = reader.ReadSubtree())
-                                        {
-                                            AddImage(subtree, list);
-                                        }
-                                        break;
-                                    }
-                                default:
-                                    reader.Skip();
-                                    break;
-                            }
-                        }
-                    }
-                }
-            }
-
-            var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase);
-
-            return list.OrderByDescending(i =>
-            {
-                if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-                if (!isLanguageEn)
-                {
-                    if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
-                    {
-                        return 2;
-                    }
-                }
-                if (string.IsNullOrEmpty(i.Language))
-                {
-                    return isLanguageEn ? 3 : 2;
-                }
-                return 0;
-            })
-                .ThenByDescending(i => i.CommunityRating ?? 0)
-                .ThenByDescending(i => i.VoteCount ?? 0)
-                .ToList();
-        }
-
-        private void AddImage(XmlReader reader, List<RemoteImageInfo> images)
-        {
-            reader.MoveToContent();
-
-            string bannerType = null;
-            string url = null;
-            int? bannerSeason = null;
-            int? width = null;
-            int? height = null;
-            string language = null;
-            double? rating = null;
-            int? voteCount = null;
-            string thumbnailUrl = null;
-
-            while (reader.Read())
-            {
-                if (reader.NodeType == XmlNodeType.Element)
-                {
-                    switch (reader.Name)
-                    {
-                        case "Rating":
-                            {
-                                var val = reader.ReadElementContentAsString() ?? string.Empty;
-
-                                double rval;
-
-                                if (double.TryParse(val, NumberStyles.Any, _usCulture, out rval))
-                                {
-                                    rating = rval;
-                                }
-
-                                break;
-                            }
-
-                        case "RatingCount":
-                            {
-                                var val = reader.ReadElementContentAsString() ?? string.Empty;
-
-                                int rval;
-
-                                if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
-                                {
-                                    voteCount = rval;
-                                }
-
-                                break;
-                            }
-
-                        case "Language":
-                            {
-                                language = reader.ReadElementContentAsString() ?? string.Empty;
-                                break;
-                            }
-
-                        case "ThumbnailPath":
-                            {
-                                thumbnailUrl = reader.ReadElementContentAsString() ?? string.Empty;
-                                break;
-                            }
-
-                        case "BannerType":
-                            {
-                                bannerType = reader.ReadElementContentAsString() ?? string.Empty;
-
-                                break;
-                            }
-
-                        case "BannerPath":
-                            {
-                                url = reader.ReadElementContentAsString() ?? string.Empty;
-                                break;
-                            }
-
-                        case "BannerType2":
-                            {
-                                var bannerType2 = reader.ReadElementContentAsString() ?? string.Empty;
-
-                                // Sometimes the resolution is stuffed in here
-                                var resolutionParts = bannerType2.Split('x');
-
-                                if (resolutionParts.Length == 2)
-                                {
-                                    int rval;
-
-                                    if (int.TryParse(resolutionParts[0], NumberStyles.Integer, _usCulture, out rval))
-                                    {
-                                        width = rval;
-                                    }
-
-                                    if (int.TryParse(resolutionParts[1], NumberStyles.Integer, _usCulture, out rval))
-                                    {
-                                        height = rval;
-                                    }
-
-                                }
-
-                                break;
-                            }
-
-                        case "Season":
-                            {
-                                var val = reader.ReadElementContentAsString();
-
-                                if (!string.IsNullOrWhiteSpace(val))
-                                {
-                                    bannerSeason = int.Parse(val);
-                                }
-                                break;
-                            }
-
-
-                        default:
-                            reader.Skip();
-                            break;
-                    }
-                }
-            }
-
-            if (!string.IsNullOrEmpty(url) && !bannerSeason.HasValue)
-            {
-                var imageInfo = new RemoteImageInfo
-                {
-                    RatingType = RatingType.Score,
-                    CommunityRating = rating,
-                    VoteCount = voteCount,
-                    Url = TVUtils.BannerUrl + url,
-                    ProviderName = Name,
-                    Language = language,
-                    Width = width,
-                    Height = height
-                };
-
-                if (!string.IsNullOrEmpty(thumbnailUrl))
-                {
-                    imageInfo.ThumbnailUrl = TVUtils.BannerUrl + thumbnailUrl;
-                }
-
-                if (string.Equals(bannerType, "poster", StringComparison.OrdinalIgnoreCase))
-                {
-                    imageInfo.Type = ImageType.Primary;
-                    images.Add(imageInfo);
-                }
-                else if (string.Equals(bannerType, "series", StringComparison.OrdinalIgnoreCase))
-                {
-                    imageInfo.Type = ImageType.Banner;
-                    images.Add(imageInfo);
-                }
-                else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase))
-                {
-                    imageInfo.Type = ImageType.Backdrop;
-                    images.Add(imageInfo);
-                }
-            }
-
-        }
-
-        public int Order
-        {
-            get { return 0; }
-        }
-
-        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
-        {
-            return _httpClient.GetResponse(new HttpRequestOptions
-            {
-                CancellationToken = cancellationToken,
-                Url = url,
-                ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
-            });
-        }
-    }
-}

+ 0 - 45
MediaBrowser.Providers/TV/SeriesDynamicInfoProvider.cs

@@ -1,45 +0,0 @@
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Logging;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Providers.TV
-{
-    public class SeriesDynamicInfoProvider : BaseMetadataProvider, IDynamicInfoProvider
-    {
-        public SeriesDynamicInfoProvider(ILogManager logManager, IServerConfigurationManager configurationManager)
-            : base(logManager, configurationManager)
-        {
-        }
-
-        public override bool Supports(BaseItem item)
-        {
-            return item is Series;
-        }
-
-        public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            var series = (Series)item;
-
-            var episodes = series.RecursiveChildren
-                .OfType<Episode>()
-                .ToList();
-
-            series.DateLastEpisodeAdded = episodes.Select(i => i.DateCreated)
-                .OrderByDescending(i => i)
-                .FirstOrDefault();
-
-            // Don't save to the db
-            return FalseTaskResult;
-        }
-
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.Last; }
-        }
-    }
-}

+ 66 - 0
MediaBrowser.Providers/TV/SeriesMetadataService.cs

@@ -0,0 +1,66 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Providers.Manager;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.TV
+{
+    public class SeriesMetadataService : MetadataService<Series, ItemId>
+    {
+        private readonly ILibraryManager _libraryManager;
+
+        public SeriesMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem)
+        {
+            _libraryManager = libraryManager;
+        }
+
+        /// <summary>
+        /// Merges the specified source.
+        /// </summary>
+        /// <param name="source">The source.</param>
+        /// <param name="target">The target.</param>
+        /// <param name="lockedFields">The locked fields.</param>
+        /// <param name="replaceData">if set to <c>true</c> [replace data].</param>
+        /// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
+        protected override void MergeData(Series source, Series target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+        }
+
+        protected override Task SaveItem(Series item, ItemUpdateType reason, CancellationToken cancellationToken)
+        {
+            return _libraryManager.UpdateItem(item, reason, cancellationToken);
+        }
+
+        protected override ItemUpdateType AfterMetadataRefresh(Series item)
+        {
+            var updateType = base.AfterMetadataRefresh(item);
+
+            var episodes = item.RecursiveChildren
+                .OfType<Episode>()
+                .ToList();
+
+            var dateLastEpisodeAdded = item.DateLastEpisodeAdded;
+
+            item.DateLastEpisodeAdded = episodes.Select(i => i.DateCreated)
+                .OrderByDescending(i => i)
+                .FirstOrDefault();
+
+            if (dateLastEpisodeAdded != item.DateLastEpisodeAdded)
+            {
+                updateType = updateType | ItemUpdateType.MetadataImport;
+            }
+
+            return updateType;
+        }
+    }
+}

+ 0 - 95
MediaBrowser.Providers/TV/SeriesProviderFromXml.cs

@@ -1,95 +0,0 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Providers.TV
-{
-    /// <summary>
-    /// Class SeriesProviderFromXml
-    /// </summary>
-    public class SeriesProviderFromXml : BaseMetadataProvider
-    {
-        private readonly IFileSystem _fileSystem;
-
-        public SeriesProviderFromXml(ILogManager logManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem)
-            : base(logManager, configurationManager)
-        {
-            _fileSystem = fileSystem;
-        }
-
-        /// <summary>
-        /// Supportses the specified item.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        public override bool Supports(BaseItem item)
-        {
-            return item is Series && item.LocationType == LocationType.FileSystem;
-        }
-
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        public override MetadataProviderPriority Priority
-        {
-            get { return MetadataProviderPriority.First; }
-        }
-
-        private const string XmlFileName = "series.xml";
-        protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
-        {
-            var xml = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, XmlFileName));
-
-            if (xml == null)
-            {
-                return false;
-            }
-
-            return _fileSystem.GetLastWriteTimeUtc(xml) > item.DateLastSaved;
-        }
-
-        /// <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, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, XmlFileName));
-
-            if (metadataFile != null)
-            {
-                var path = metadataFile.FullName;
-
-                await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-                try
-                {
-                    new SeriesXmlParser(Logger).Fetch((Series)item, path, cancellationToken);
-                }
-                finally
-                {
-                    XmlParsingResourcePool.Release();
-                }
-
-            }
-
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
-    }
-}

+ 62 - 0
MediaBrowser.Providers/TV/SeriesXmlProvider.cs

@@ -0,0 +1,62 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.TV
+{
+    /// <summary>
+    /// Class SeriesProviderFromXml
+    /// </summary>
+    public class SeriesXmlProvider : BaseXmlProvider, ILocalMetadataProvider<Series>
+    {
+        private readonly ILogger _logger;
+
+        public SeriesXmlProvider(IFileSystem fileSystem, ILogger logger)
+            : base(fileSystem)
+        {
+            _logger = logger;
+        }
+
+        public async Task<MetadataResult<Series>> GetMetadata(string path, CancellationToken cancellationToken)
+        {
+            path = GetXmlFile(path).FullName;
+
+            var result = new MetadataResult<Series>();
+
+            await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                var person = new Series();
+
+                new SeriesXmlParser(_logger).Fetch(person, path, cancellationToken);
+                result.HasMetadata = true;
+                result.Item = person;
+            }
+            catch (FileNotFoundException)
+            {
+                result.HasMetadata = false;
+            }
+            finally
+            {
+                XmlParsingResourcePool.Release();
+            }
+
+            return result;
+        }
+
+        public string Name
+        {
+            get { return "Media Browser Xml"; }
+        }
+
+        protected override FileInfo GetXmlFile(string path)
+        {
+            return new FileInfo(Path.Combine(path, "series.xml"));
+        }
+    }
+}

+ 2 - 2
MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs → MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs

@@ -17,13 +17,13 @@ using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class ManualTvdbEpisodeImageProvider : IRemoteImageProvider
+    public class TvdbEpisodeImageProvider : IRemoteImageProvider
     {
         private readonly IServerConfigurationManager _config;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IHttpClient _httpClient;
 
-        public ManualTvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
+        public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
         {
             _config = config;
             _httpClient = httpClient;

+ 273 - 131
MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs

@@ -5,211 +5,353 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Providers;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using System.Xml;
 
 namespace MediaBrowser.Providers.TV
 {
-    public class TvdbSeriesImageProvider : BaseMetadataProvider
+    public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor
     {
-        /// <summary>
-        /// Gets the HTTP client.
-        /// </summary>
-        /// <value>The HTTP client.</value>
-        protected IHttpClient HttpClient { get; private set; }
-
-        /// <summary>
-        /// The _provider manager
-        /// </summary>
-        private readonly IProviderManager _providerManager;
+        private readonly IServerConfigurationManager _config;
+        private readonly IHttpClient _httpClient;
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly IFileSystem _fileSystem;
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="TvdbSeriesImageProvider"/> 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 TvdbSeriesImageProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem)
-            : base(logManager, configurationManager)
+        public TvdbSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
         {
-            if (httpClient == null)
-            {
-                throw new ArgumentNullException("httpClient");
-            }
-            HttpClient = httpClient;
-            _providerManager = providerManager;
+            _config = config;
+            _httpClient = httpClient;
             _fileSystem = fileSystem;
         }
 
-        /// <summary>
-        /// Supportses the specified item.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
-        public override bool Supports(BaseItem item)
+        public string Name
         {
-            return item is Series;
+            get { return ProviderName; }
         }
 
-        /// <summary>
-        /// Gets the priority.
-        /// </summary>
-        /// <value>The priority.</value>
-        public override MetadataProviderPriority Priority
+        public static string ProviderName
         {
-            // Run after fanart
-            get { return MetadataProviderPriority.Fourth; }
+            get { return "TheTVDB"; }
         }
 
-        /// <summary>
-        /// Gets a value indicating whether [requires internet].
-        /// </summary>
-        /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
-        public override bool RequiresInternet
+        public bool Supports(IHasImages item)
         {
-            get
-            {
-                return true;
-            }
+            return item is Series;
         }
 
-        public override ItemUpdateType ItemUpdateType
+        public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
         {
-            get
+            return new List<ImageType>
             {
-                return ItemUpdateType.ImageUpdate;
-            }
+                ImageType.Primary, 
+                ImageType.Banner,
+                ImageType.Backdrop
+            };
         }
 
-        /// <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
+        public async Task<IEnumerable<RemoteImageInfo>> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
         {
-            get
-            {
-                return true;
-            }
-        }
+            var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
 
-        /// <summary>
-        /// Gets the provider version.
-        /// </summary>
-        /// <value>The provider version.</value>
-        protected override string ProviderVersion
-        {
-            get
-            {
-                return "1";
-            }
+            return images.Where(i => i.Type == imageType);
         }
 
-        protected override DateTime CompareDate(BaseItem item)
+        public async Task<IEnumerable<RemoteImageInfo>> GetAllImages(IHasImages item, CancellationToken cancellationToken)
         {
-            var seriesId = item.GetProviderId(MetadataProviders.Tvdb);
+            var series = (Series)item;
+            var seriesId = series.GetProviderId(MetadataProviders.Tvdb);
 
             if (!string.IsNullOrEmpty(seriesId))
             {
+                var language = item.GetPreferredMetadataLanguage();
+
+                await TvdbSeriesProvider.Current.EnsureSeriesInfo(seriesId, language, cancellationToken).ConfigureAwait(false);
+
                 // Process images
-                var imagesXmlPath = Path.Combine(TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml");
+                var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId);
 
-                var imagesFileInfo = new FileInfo(imagesXmlPath);
+                var path = Path.Combine(seriesDataPath, "banners.xml");
 
-                if (imagesFileInfo.Exists)
+                try
+                {
+                    return GetImages(path, language, cancellationToken);
+                }
+                catch (FileNotFoundException)
                 {
-                    return _fileSystem.GetLastWriteTimeUtc(imagesFileInfo);
+                    // No tvdb data yet. Don't blow up
                 }
             }
 
-            return base.CompareDate(item);
+            return new RemoteImageInfo[] { };
         }
 
-        protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+        private IEnumerable<RemoteImageInfo> GetImages(string xmlPath, string preferredLanguage, CancellationToken cancellationToken)
         {
-            var options = ConfigurationManager.Configuration.GetMetadataOptions("Series") ?? new MetadataOptions();
-            
-            if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Banner) && item.BackdropImagePaths.Count >= options.GetLimit(ImageType.Backdrop))
+            var settings = new XmlReaderSettings
             {
-                return false;
-            }
-            return base.NeedsRefreshInternal(item, providerInfo);
-        }
+                CheckCharacters = false,
+                IgnoreProcessingInstructions = true,
+                IgnoreComments = true,
+                ValidationType = ValidationType.None
+            };
 
-        /// <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, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
+            var list = new List<RemoteImageInfo>();
 
-            var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualTvdbSeriesImageProvider.ProviderName).ConfigureAwait(false);
+            using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
+            {
+                // Use XmlReader for best performance
+                using (var reader = XmlReader.Create(streamReader, settings))
+                {
+                    reader.MoveToContent();
 
-            const int backdropLimit = 1;
+                    // Loop through each element
+                    while (reader.Read())
+                    {
+                        cancellationToken.ThrowIfCancellationRequested();
 
-            await DownloadImages(item, images.ToList(), backdropLimit, cancellationToken).ConfigureAwait(false);
+                        if (reader.NodeType == XmlNodeType.Element)
+                        {
+                            switch (reader.Name)
+                            {
+                                case "Banner":
+                                    {
+                                        using (var subtree = reader.ReadSubtree())
+                                        {
+                                            AddImage(subtree, list);
+                                        }
+                                        break;
+                                    }
+                                default:
+                                    reader.Skip();
+                                    break;
+                            }
+                        }
+                    }
+                }
+            }
 
-            SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
-            return true;
-        }
+            var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase);
 
-        private async Task DownloadImages(BaseItem item, List<RemoteImageInfo> images, int backdropLimit, CancellationToken cancellationToken)
-        {
-            var options = ConfigurationManager.Configuration.GetMetadataOptions("Series") ?? new MetadataOptions();
-            
-            if (!item.LockedFields.Contains(MetadataFields.Images))
+            return list.OrderByDescending(i =>
             {
-                if (!item.HasImage(ImageType.Primary))
+                if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
                 {
-                    var image = images.FirstOrDefault(i => i.Type == ImageType.Primary);
-
-                    if (image != null)
+                    return 3;
+                }
+                if (!isLanguageEn)
+                {
+                    if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
                     {
-                        await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken)
-                            .ConfigureAwait(false);
+                        return 2;
                     }
                 }
-
-                if (options.IsEnabled(ImageType.Banner) && !item.HasImage(ImageType.Banner))
+                if (string.IsNullOrEmpty(i.Language))
                 {
-                    var image = images.FirstOrDefault(i => i.Type == ImageType.Banner);
+                    return isLanguageEn ? 3 : 2;
+                }
+                return 0;
+            })
+                .ThenByDescending(i => i.CommunityRating ?? 0)
+                .ThenByDescending(i => i.VoteCount ?? 0)
+                .ToList();
+        }
+
+        private void AddImage(XmlReader reader, List<RemoteImageInfo> images)
+        {
+            reader.MoveToContent();
 
-                    if (image != null)
+            string bannerType = null;
+            string url = null;
+            int? bannerSeason = null;
+            int? width = null;
+            int? height = null;
+            string language = null;
+            double? rating = null;
+            int? voteCount = null;
+            string thumbnailUrl = null;
+
+            while (reader.Read())
+            {
+                if (reader.NodeType == XmlNodeType.Element)
+                {
+                    switch (reader.Name)
                     {
-                        await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken)
-                            .ConfigureAwait(false);
+                        case "Rating":
+                            {
+                                var val = reader.ReadElementContentAsString() ?? string.Empty;
+
+                                double rval;
+
+                                if (double.TryParse(val, NumberStyles.Any, _usCulture, out rval))
+                                {
+                                    rating = rval;
+                                }
+
+                                break;
+                            }
+
+                        case "RatingCount":
+                            {
+                                var val = reader.ReadElementContentAsString() ?? string.Empty;
+
+                                int rval;
+
+                                if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
+                                {
+                                    voteCount = rval;
+                                }
+
+                                break;
+                            }
+
+                        case "Language":
+                            {
+                                language = reader.ReadElementContentAsString() ?? string.Empty;
+                                break;
+                            }
+
+                        case "ThumbnailPath":
+                            {
+                                thumbnailUrl = reader.ReadElementContentAsString() ?? string.Empty;
+                                break;
+                            }
+
+                        case "BannerType":
+                            {
+                                bannerType = reader.ReadElementContentAsString() ?? string.Empty;
+
+                                break;
+                            }
+
+                        case "BannerPath":
+                            {
+                                url = reader.ReadElementContentAsString() ?? string.Empty;
+                                break;
+                            }
+
+                        case "BannerType2":
+                            {
+                                var bannerType2 = reader.ReadElementContentAsString() ?? string.Empty;
+
+                                // Sometimes the resolution is stuffed in here
+                                var resolutionParts = bannerType2.Split('x');
+
+                                if (resolutionParts.Length == 2)
+                                {
+                                    int rval;
+
+                                    if (int.TryParse(resolutionParts[0], NumberStyles.Integer, _usCulture, out rval))
+                                    {
+                                        width = rval;
+                                    }
+
+                                    if (int.TryParse(resolutionParts[1], NumberStyles.Integer, _usCulture, out rval))
+                                    {
+                                        height = rval;
+                                    }
+
+                                }
+
+                                break;
+                            }
+
+                        case "Season":
+                            {
+                                var val = reader.ReadElementContentAsString();
+
+                                if (!string.IsNullOrWhiteSpace(val))
+                                {
+                                    bannerSeason = int.Parse(val);
+                                }
+                                break;
+                            }
+
+
+                        default:
+                            reader.Skip();
+                            break;
                     }
                 }
             }
 
-            if (options.IsEnabled(ImageType.Backdrop) && item.BackdropImagePaths.Count < backdropLimit && !item.LockedFields.Contains(MetadataFields.Backdrops))
+            if (!string.IsNullOrEmpty(url) && !bannerSeason.HasValue)
             {
-                foreach (var backdrop in images.Where(i => i.Type == ImageType.Backdrop && 
-                    (!i.Width.HasValue || 
-                    i.Width.Value >= options.GetMinWidth(ImageType.Backdrop))))
+                var imageInfo = new RemoteImageInfo
                 {
-                    var url = backdrop.Url;
+                    RatingType = RatingType.Score,
+                    CommunityRating = rating,
+                    VoteCount = voteCount,
+                    Url = TVUtils.BannerUrl + url,
+                    ProviderName = Name,
+                    Language = language,
+                    Width = width,
+                    Height = height
+                };
 
-                    await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, null, cancellationToken).ConfigureAwait(false);
+                if (!string.IsNullOrEmpty(thumbnailUrl))
+                {
+                    imageInfo.ThumbnailUrl = TVUtils.BannerUrl + thumbnailUrl;
+                }
 
-                    if (item.BackdropImagePaths.Count >= backdropLimit) break;
+                if (string.Equals(bannerType, "poster", StringComparison.OrdinalIgnoreCase))
+                {
+                    imageInfo.Type = ImageType.Primary;
+                    images.Add(imageInfo);
+                }
+                else if (string.Equals(bannerType, "series", StringComparison.OrdinalIgnoreCase))
+                {
+                    imageInfo.Type = ImageType.Banner;
+                    images.Add(imageInfo);
+                }
+                else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase))
+                {
+                    imageInfo.Type = ImageType.Backdrop;
+                    images.Add(imageInfo);
                 }
             }
+
+        }
+
+        public int Order
+        {
+            get { return 0; }
+        }
+
+        public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
+        {
+            return _httpClient.GetResponse(new HttpRequestOptions
+            {
+                CancellationToken = cancellationToken,
+                Url = url,
+                ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
+            });
+        }
+
+        public bool HasChanged(IHasMetadata item, DateTime date)
+        {
+            var tvdbId = item.GetProviderId(MetadataProviders.Tvdb);
+
+            if (!String.IsNullOrEmpty(tvdbId))
+            {
+                // Process images
+                var imagesXmlPath = Path.Combine(TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, tvdbId), "banners.xml");
+
+                var fileInfo = new FileInfo(imagesXmlPath);
+
+                return fileInfo.Exists && _fileSystem.GetLastWriteTimeUtc(fileInfo) > date;
+            }
+
+            return false;
         }
     }
 }

File diff suppressed because it is too large
+ 289 - 456
MediaBrowser.Providers/TV/TvdbSeriesProvider.cs


+ 5 - 4
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -208,7 +208,6 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <param name="prescanTasks">The prescan tasks.</param>
         /// <param name="postscanTasks">The postscan tasks.</param>
         /// <param name="peoplePrescanTasks">The people prescan tasks.</param>
-        /// <param name="savers">The savers.</param>
         public void AddParts(IEnumerable<IResolverIgnoreRule> rules,
             IEnumerable<IVirtualFolderCreator> pluginFolders,
             IEnumerable<IItemResolver> resolvers,
@@ -277,7 +276,7 @@ namespace MediaBrowser.Server.Implementations.Library
         /// <param name="configuration">The configuration.</param>
         private void RecordConfigurationValues(ServerConfiguration configuration)
         {
-            _seasonZeroDisplayName = ConfigurationManager.Configuration.SeasonZeroDisplayName;
+            _seasonZeroDisplayName = configuration.SeasonZeroDisplayName;
             _itemsByNamePath = ConfigurationManager.ApplicationPaths.ItemsByNamePath;
         }
 
@@ -309,8 +308,10 @@ namespace MediaBrowser.Server.Implementations.Library
                     await UpdateSeasonZeroNames(newSeasonZeroName, CancellationToken.None).ConfigureAwait(false);
                 }
 
-                // Any number of configuration settings could change the way the library is refreshed, so do that now
-                _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+                if (seasonZeroNameChanged || ibnPathChanged)
+                {
+                    _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
+                }
             });
         }
 

Some files were not shown because too many files changed in this diff