diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs
index aeae9c507e..728ceeea96 100644
--- a/MediaBrowser.Api/Images/ImageService.cs
+++ b/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))
{
}
}
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index a7cc205baa..df80b465f3 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/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;
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetXmlProvider.cs b/MediaBrowser.Providers/BoxSets/BoxSetXmlProvider.cs
index 64de1c37f9..eee6f3b487 100644
--- a/MediaBrowser.Providers/BoxSets/BoxSetXmlProvider.cs
+++ b/MediaBrowser.Providers/BoxSets/BoxSetXmlProvider.cs
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace MediaBrowser.Providers.BoxSets
{
///
- /// Class SeriesProviderFromXml
+ /// Class BoxSetXmlProvider.
///
public class BoxSetXmlProvider : BaseXmlProvider, ILocalMetadataProvider
{
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 282facfc8b..0ac26330a6 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -107,6 +107,8 @@
+
+
@@ -152,22 +154,20 @@
-
-
-
+
+
+
-
+
-
-
-
+
diff --git a/MediaBrowser.Providers/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Omdb/OmdbProvider.cs
new file mode 100644
index 0000000000..70dea5db4b
--- /dev/null
+++ b/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(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; }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Providers/Omdb/OmdbSeriesProvider.cs b/MediaBrowser.Providers/Omdb/OmdbSeriesProvider.cs
new file mode 100644
index 0000000000..3659868c7f
--- /dev/null
+++ b/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
+ {
+ 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"; }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs b/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs
index 60643252b4..b268c08a5e 100644
--- a/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs
+++ b/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);
diff --git a/MediaBrowser.Providers/TV/FanArtTVProvider.cs b/MediaBrowser.Providers/TV/FanArtTVProvider.cs
deleted file mode 100644
index db71d0db8f..0000000000
--- a/MediaBrowser.Providers/TV/FanArtTVProvider.cs
+++ /dev/null
@@ -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; }
-
- ///
- /// Gets the HTTP client.
- ///
- /// The HTTP client.
- 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;
- }
-
- ///
- /// Gets the priority.
- ///
- /// The priority.
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.Third; }
- }
-
- public override ItemUpdateType ItemUpdateType
- {
- get
- {
- return ItemUpdateType.ImageUpdate;
- }
- }
-
- ///
- /// Needses the refresh internal.
- ///
- /// The item.
- /// The provider info.
- /// true if XXXX, false otherwise
- 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);
- }
-
- ///
- /// Gets a value indicating whether [refresh on version change].
- ///
- /// true if [refresh on version change]; otherwise, false.
- protected override bool RefreshOnVersionChange
- {
- get
- {
- return true;
- }
- }
-
- ///
- /// Gets the provider version.
- ///
- /// The provider version.
- protected override string ProviderVersion
- {
- get
- {
- return "1";
- }
- }
-
- ///
- /// Gets the series data path.
- ///
- /// The app paths.
- /// The series id.
- /// System.String.
- internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId)
- {
- var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
-
- return seriesDataPath;
- }
-
- ///
- /// Gets the series data path.
- ///
- /// The app paths.
- /// System.String.
- 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 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;
- }
-
- ///
- /// Fetches from XML.
- ///
- /// The item.
- /// The images.
- /// The cancellation token.
- /// Task.
- private async Task FetchFromXml(BaseItem item, List 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 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);
- }
-
- ///
- /// Downloads the series XML.
- ///
- /// The TVDB id.
- /// The cancellation token.
- /// Task.
- 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);
- }
- }
- }
-
- }
-}
diff --git a/MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs b/MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs
index 6b005c9dc3..db546f3a34 100644
--- a/MediaBrowser.Providers/TV/FanArtTvUpdatesPrescanTask.cs
+++ b/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;
diff --git a/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs b/MediaBrowser.Providers/TV/FanartSeriesProvider.cs
similarity index 73%
rename from MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs
rename to MediaBrowser.Providers/TV/FanartSeriesProvider.cs
index 9e492d8ead..66742cb994 100644
--- a/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs
+++ b/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> GetAllImages(IHasImages item, CancellationToken cancellationToken)
+ public async Task> GetAllImages(IHasImages item, CancellationToken cancellationToken)
{
var list = new List();
@@ -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>(list);
+ .ThenByDescending(i => i.VoteCount ?? 0);
}
private void AddImages(List list, string xmlPath, CancellationToken cancellationToken)
@@ -333,5 +342,102 @@ namespace MediaBrowser.Providers.TV
ResourcePool = FanartArtistProvider.FanArtResourcePool
});
}
+
+ ///
+ /// Gets the series data path.
+ ///
+ /// The app paths.
+ /// The series id.
+ /// System.String.
+ internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId)
+ {
+ var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
+
+ return seriesDataPath;
+ }
+
+ ///
+ /// Gets the series data path.
+ ///
+ /// The app paths.
+ /// System.String.
+ 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);
+ }
+
+ ///
+ /// Downloads the series XML.
+ ///
+ /// The TVDB id.
+ /// The cancellation token.
+ /// Task.
+ 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;
+ }
}
}
diff --git a/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs
deleted file mode 100644
index 0cc2d88994..0000000000
--- a/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs
+++ /dev/null
@@ -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 GetSupportedImages(IHasImages item)
- {
- return new List
- {
- ImageType.Primary,
- ImageType.Banner,
- ImageType.Backdrop
- };
- }
-
- public async Task> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
- {
- var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
-
- return images.Where(i => i.Type == imageType);
- }
-
- public Task> 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>(new RemoteImageInfo[] { });
- }
-
- private IEnumerable GetImages(string xmlPath, string preferredLanguage, CancellationToken cancellationToken)
- {
- var settings = new XmlReaderSettings
- {
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true,
- ValidationType = ValidationType.None
- };
-
- var list = new List();
-
- 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 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 GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClient.GetResponse(new HttpRequestOptions
- {
- CancellationToken = cancellationToken,
- Url = url,
- ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
- });
- }
- }
-}
diff --git a/MediaBrowser.Providers/TV/SeriesDynamicInfoProvider.cs b/MediaBrowser.Providers/TV/SeriesDynamicInfoProvider.cs
deleted file mode 100644
index ff31ce4aa4..0000000000
--- a/MediaBrowser.Providers/TV/SeriesDynamicInfoProvider.cs
+++ /dev/null
@@ -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 FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
- {
- var series = (Series)item;
-
- var episodes = series.RecursiveChildren
- .OfType()
- .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; }
- }
- }
-}
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
new file mode 100644
index 0000000000..ffd6d17b29
--- /dev/null
+++ b/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
+ {
+ 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;
+ }
+
+ ///
+ /// Merges the specified source.
+ ///
+ /// The source.
+ /// The target.
+ /// The locked fields.
+ /// if set to true [replace data].
+ /// if set to true [merge metadata settings].
+ protected override void MergeData(Series source, Series target, List 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()
+ .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;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/TV/SeriesProviderFromXml.cs b/MediaBrowser.Providers/TV/SeriesProviderFromXml.cs
deleted file mode 100644
index ff99a95c1f..0000000000
--- a/MediaBrowser.Providers/TV/SeriesProviderFromXml.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Class SeriesProviderFromXml
- ///
- public class SeriesProviderFromXml : BaseMetadataProvider
- {
- private readonly IFileSystem _fileSystem;
-
- public SeriesProviderFromXml(ILogManager logManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem)
- : base(logManager, configurationManager)
- {
- _fileSystem = fileSystem;
- }
-
- ///
- /// Supportses the specified item.
- ///
- /// The item.
- /// true if XXXX, false otherwise
- public override bool Supports(BaseItem item)
- {
- return item is Series && item.LocationType == LocationType.FileSystem;
- }
-
- ///
- /// Gets the priority.
- ///
- /// The priority.
- 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;
- }
-
- ///
- /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
- ///
- /// The item.
- /// if set to true [force].
- /// The cancellation token.
- /// Task{System.Boolean}.
- public override async Task 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;
- }
- }
-}
diff --git a/MediaBrowser.Providers/TV/SeriesXmlProvider.cs b/MediaBrowser.Providers/TV/SeriesXmlProvider.cs
new file mode 100644
index 0000000000..8f0c631368
--- /dev/null
+++ b/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
+{
+ ///
+ /// Class SeriesProviderFromXml
+ ///
+ public class SeriesXmlProvider : BaseXmlProvider, ILocalMetadataProvider
+ {
+ private readonly ILogger _logger;
+
+ public SeriesXmlProvider(IFileSystem fileSystem, ILogger logger)
+ : base(fileSystem)
+ {
+ _logger = logger;
+ }
+
+ public async Task> GetMetadata(string path, CancellationToken cancellationToken)
+ {
+ path = GetXmlFile(path).FullName;
+
+ var result = new MetadataResult();
+
+ 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"));
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs
similarity index 97%
rename from MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs
rename to MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs
index abccc29479..85353bad51 100644
--- a/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs
+++ b/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;
diff --git a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs
index 73db2680e8..e568306442 100644
--- a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs
+++ b/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
{
- ///
- /// Gets the HTTP client.
- ///
- /// The HTTP client.
- protected IHttpClient HttpClient { get; private set; }
-
- ///
- /// The _provider manager
- ///
- private readonly IProviderManager _providerManager;
+ private readonly IServerConfigurationManager _config;
+ private readonly IHttpClient _httpClient;
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IFileSystem _fileSystem;
- ///
- /// Initializes a new instance of the class.
- ///
- /// The HTTP client.
- /// The log manager.
- /// The configuration manager.
- /// The provider manager.
- /// httpClient
- 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;
}
- ///
- /// Supportses the specified item.
- ///
- /// The item.
- /// true if XXXX, false otherwise
- public override bool Supports(BaseItem item)
+ public string Name
{
- return item is Series;
+ get { return ProviderName; }
}
- ///
- /// Gets the priority.
- ///
- /// The priority.
- public override MetadataProviderPriority Priority
+ public static string ProviderName
{
- // Run after fanart
- get { return MetadataProviderPriority.Fourth; }
+ get { return "TheTVDB"; }
}
- ///
- /// Gets a value indicating whether [requires internet].
- ///
- /// true if [requires internet]; otherwise, false.
- public override bool RequiresInternet
+ public bool Supports(IHasImages item)
{
- get
- {
- return true;
- }
+ return item is Series;
}
- public override ItemUpdateType ItemUpdateType
+ public IEnumerable GetSupportedImages(IHasImages item)
{
- get
+ return new List
{
- return ItemUpdateType.ImageUpdate;
- }
+ ImageType.Primary,
+ ImageType.Banner,
+ ImageType.Backdrop
+ };
}
- ///
- /// Gets a value indicating whether [refresh on version change].
- ///
- /// true if [refresh on version change]; otherwise, false.
- protected override bool RefreshOnVersionChange
+ public async Task> GetImages(IHasImages item, ImageType imageType, CancellationToken cancellationToken)
{
- get
- {
- return true;
- }
- }
+ var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false);
- ///
- /// Gets the provider version.
- ///
- /// The provider version.
- protected override string ProviderVersion
- {
- get
- {
- return "1";
- }
+ return images.Where(i => i.Type == imageType);
}
- protected override DateTime CompareDate(BaseItem item)
+ public async Task> 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 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
+ };
- ///
- /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
- ///
- /// The item.
- /// if set to true [force].
- /// The cancellation token.
- /// Task{System.Boolean}.
- public override async Task FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
+ var list = new List();
- 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 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 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 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;
}
}
}
diff --git a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs
index 8d7ef5af99..e4d86f550b 100644
--- a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -23,212 +22,55 @@ using System.Xml;
namespace MediaBrowser.Providers.TV
{
- ///
- /// Class RemoteSeriesProvider
- ///
- class TvdbSeriesProvider : BaseMetadataProvider, IDisposable
+ public class TvdbSeriesProvider : IRemoteMetadataProvider, IHasChangeMonitor
{
- ///
- /// The tv db
- ///
internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2);
-
- ///
- /// Gets the current.
- ///
- /// The current.
internal static TvdbSeriesProvider Current { get; private set; }
-
- ///
- /// The _zip client
- ///
private readonly IZipClient _zipClient;
-
- ///
- /// Gets the HTTP client.
- ///
- /// The HTTP client.
- protected IHttpClient HttpClient { get; private set; }
-
+ private readonly IHttpClient _httpClient;
private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private readonly ILogger _logger;
- ///
- /// Initializes a new instance of the class.
- ///
- /// The HTTP client.
- /// The log manager.
- /// The configuration manager.
- /// The zip client.
- /// httpClient
- public TvdbSeriesProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IZipClient zipClient, IFileSystem fileSystem)
- : base(logManager, configurationManager)
+ public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger)
{
- if (httpClient == null)
- {
- throw new ArgumentNullException("httpClient");
- }
- HttpClient = httpClient;
_zipClient = zipClient;
+ _httpClient = httpClient;
_fileSystem = fileSystem;
+ _config = config;
+ _logger = logger;
Current = this;
}
- ///
- /// Releases unmanaged and - optionally - managed resources.
- ///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
- protected virtual void Dispose(bool dispose)
- {
- if (dispose)
- {
- TvDbResourcePool.Dispose();
- }
- }
-
- ///
- /// The root URL
- ///
private const string RootUrl = "http://www.thetvdb.com/api/";
- ///
- /// The series query
- ///
private const string SeriesQuery = "GetSeries.php?seriesname={0}";
- ///
- /// The series get zip
- ///
private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip";
- ///
- /// The LOCA l_ MET a_ FIL e_ NAME
- ///
- protected const string LocalMetaFileName = "series.xml";
-
- ///
- /// Supportses the specified item.
- ///
- /// The item.
- /// true if XXXX, false otherwise
- public override bool Supports(BaseItem item)
- {
- return item is Series;
- }
-
- ///
- /// Gets the priority.
- ///
- /// The priority.
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.Second; }
- }
-
- ///
- /// Gets a value indicating whether [requires internet].
- ///
- /// true if [requires internet]; otherwise, false.
- public override bool RequiresInternet
- {
- get
- {
- return true;
- }
- }
-
- ///
- /// Gets a value indicating whether [refresh on version change].
- ///
- /// true if [refresh on version change]; otherwise, false.
- protected override bool RefreshOnVersionChange
- {
- get
- {
- return true;
- }
- }
-
- ///
- /// Gets the provider version.
- ///
- /// The provider version.
- protected override string ProviderVersion
- {
- get
- {
- return "2";
- }
- }
-
- public override bool EnforceDontFetchMetadata
- {
- get
- {
- // Other providers depend on the xml downloaded here
- return false;
- }
- }
-
- protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
- {
- var seriesId = item.GetProviderId(MetadataProviders.Tvdb);
-
- if (!string.IsNullOrEmpty(seriesId))
- {
- // Process images
- var path = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId);
-
- try
- {
- var files = new DirectoryInfo(path)
- .EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)
- .Select(i => _fileSystem.GetLastWriteTimeUtc(i))
- .ToList();
-
- if (files.Count > 0)
- {
- return files.Max() > providerInfo.LastRefreshed;
- }
- }
- catch (DirectoryNotFoundException)
- {
- // Don't blow up
- return true;
- }
- }
-
- return base.NeedsRefreshBasedOnCompareDate(item, providerInfo);
- }
-
- ///
- /// Fetches metadata and returns true or false indicating if any work that requires persistence was done
- ///
- /// The item.
- /// if set to true [force].
- /// The cancellation token.
- /// Task{System.Boolean}.
- public override async Task FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
+ public async Task> GetMetadata(ItemId itemId, CancellationToken cancellationToken)
{
- cancellationToken.ThrowIfCancellationRequested();
+ var result = new MetadataResult();
- var series = (Series)item;
-
- var seriesId = series.GetProviderId(MetadataProviders.Tvdb);
+ var seriesId = itemId.GetProviderId(MetadataProviders.Tvdb);
if (string.IsNullOrEmpty(seriesId))
{
- seriesId = await FindSeries(series.Name, cancellationToken).ConfigureAwait(false);
+ seriesId = await FindSeries(itemId.Name, cancellationToken).ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
if (!string.IsNullOrEmpty(seriesId))
{
- var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId);
+ await EnsureSeriesInfo(seriesId, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ result.Item = new Series();
+ result.HasMetadata = true;
- await FetchSeriesData(series, seriesId, seriesDataPath, force, cancellationToken).ConfigureAwait(false);
+ FetchSeriesData(result.Item, seriesId, cancellationToken);
}
- SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
- return true;
+ return result;
}
///
@@ -236,48 +78,24 @@ namespace MediaBrowser.Providers.TV
///
/// The series.
/// The series id.
- /// The series data path.
- /// if set to true [is forced refresh].
/// The cancellation token.
/// Task{System.Boolean}.
- private async Task FetchSeriesData(Series series, string seriesId, string seriesDataPath, bool isForcedRefresh, CancellationToken cancellationToken)
+ private void FetchSeriesData(Series series, string seriesId, CancellationToken cancellationToken)
{
- Directory.CreateDirectory(seriesDataPath);
+ series.SetProviderId(MetadataProviders.Tvdb, seriesId);
- var files = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly)
- .Select(Path.GetFileName)
- .ToList();
+ var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesId);
var seriesXmlFilename = series.GetPreferredMetadataLanguage().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, null, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false);
- }
-
- // Have to check this here since we prevent the normal enforcement through ProviderManager
- if (!series.DontFetchMeta)
- {
- // Examine if there's no local metadata, or save local is on (to get updates)
- if (isForcedRefresh || ConfigurationManager.Configuration.EnableTvDbUpdates || !HasLocalMeta(series))
- {
- series.SetProviderId(MetadataProviders.Tvdb, seriesId);
-
- var seriesXmlPath = Path.Combine(seriesDataPath, seriesXmlFilename);
- var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml");
+ var seriesXmlPath = Path.Combine(seriesDataPath, seriesXmlFilename);
+ var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml");
- FetchSeriesInfo(series, seriesXmlPath, cancellationToken);
+ FetchSeriesInfo(series, seriesXmlPath, cancellationToken);
- if (!series.LockedFields.Contains(MetadataFields.Cast))
- {
- series.People.Clear();
+ cancellationToken.ThrowIfCancellationRequested();
- FetchActors(series, actorsXmlPath, cancellationToken);
- }
- }
- }
+ FetchActors(series, actorsXmlPath);
}
///
@@ -293,7 +111,7 @@ namespace MediaBrowser.Providers.TV
{
var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, preferredMetadataLanguage);
- using (var zipStream = await HttpClient.Get(new HttpRequestOptions
+ using (var zipStream = await _httpClient.Get(new HttpRequestOptions
{
Url = url,
ResourcePool = TvDbResourcePool,
@@ -326,7 +144,7 @@ namespace MediaBrowser.Providers.TV
internal async Task EnsureSeriesInfo(string seriesId, string preferredMetadataLanguage, CancellationToken cancellationToken)
{
- var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId);
+ var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesId);
Directory.CreateDirectory(seriesDataPath);
@@ -344,128 +162,138 @@ namespace MediaBrowser.Providers.TV
}
}
- private void DeleteXmlFiles(string path)
+ ///
+ /// Finds the series.
+ ///
+ /// The name.
+ /// The cancellation token.
+ /// Task{System.String}.
+ private async Task FindSeries(string name, CancellationToken cancellationToken)
{
- try
+ var url = string.Format(RootUrl + SeriesQuery, WebUtility.UrlEncode(name));
+ var doc = new XmlDocument();
+
+ using (var results = await _httpClient.Get(new HttpRequestOptions
{
- foreach (var file in new DirectoryInfo(path)
- .EnumerateFiles("*.xml", SearchOption.AllDirectories)
- .ToList())
- {
- file.Delete();
- }
- }
- catch (DirectoryNotFoundException)
+ Url = url,
+ ResourcePool = TvDbResourcePool,
+ CancellationToken = cancellationToken
+
+ }).ConfigureAwait(false))
{
- // No biggie
+ doc.Load(results);
}
- }
-
- ///
- /// Sanitizes the XML file.
- ///
- /// The file.
- /// Task.
- private async Task SanitizeXmlFile(string file)
- {
- string validXml;
- using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, true))
+ if (doc.HasChildNodes)
{
- using (var reader = new StreamReader(fileStream))
+ var nodes = doc.SelectNodes("//Series");
+ var comparableName = GetComparableName(name);
+ if (nodes != null)
{
- var xml = await reader.ReadToEndAsync().ConfigureAwait(false);
+ foreach (XmlNode node in nodes)
+ {
+ var titles = new List();
- validXml = StripInvalidXmlCharacters(xml);
+ var nameNode = node.SelectSingleNode("./SeriesName");
+ if (nameNode != null)
+ {
+ titles.Add(GetComparableName(nameNode.InnerText));
+ }
+
+ var aliasNode = node.SelectSingleNode("./AliasNames");
+ if (aliasNode != null)
+ {
+ var alias = aliasNode.InnerText.Split('|').Select(GetComparableName);
+ titles.AddRange(alias);
+ }
+
+ if (titles.Any(t => string.Equals(t, comparableName, StringComparison.OrdinalIgnoreCase)))
+ {
+ var id = node.SelectSingleNode("./seriesid");
+ if (id != null)
+ return id.InnerText;
+ }
+
+ foreach (var title in titles)
+ {
+ _logger.Info("TVDb Provider - " + title + " did not match " + comparableName);
+ }
+ }
}
}
- using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, true))
+ // Try stripping off the year if it was supplied
+ var parenthIndex = name.LastIndexOf('(');
+
+ if (parenthIndex != -1)
{
- using (var writer = new StreamWriter(fileStream))
- {
- await writer.WriteAsync(validXml).ConfigureAwait(false);
- }
+ var newName = name.Substring(0, parenthIndex);
+
+ return await FindSeries(newName, cancellationToken);
}
+
+ _logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org.");
+ return null;
}
///
- /// Strips the invalid XML characters.
+ /// The remove
///
- /// The in string.
+ const string remove = "\"'!`?";
+ ///
+ /// The spacers
+ ///
+ const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
+
+ ///
+ /// Gets the name of the comparable.
+ ///
+ /// The name.
/// System.String.
- public static string StripInvalidXmlCharacters(string inString)
+ internal static string GetComparableName(string name)
{
- if (inString == null) return null;
-
- var sbOutput = new StringBuilder();
- char ch;
-
- for (int i = 0; i < inString.Length; i++)
+ name = name.ToLower();
+ name = name.Normalize(NormalizationForm.FormKD);
+ var sb = new StringBuilder();
+ foreach (var c in name)
{
- ch = inString[i];
- if ((ch >= 0x0020 && ch <= 0xD7FF) ||
- (ch >= 0xE000 && ch <= 0xFFFD) ||
- ch == 0x0009 ||
- ch == 0x000A ||
- ch == 0x000D)
+ if ((int)c >= 0x2B0 && (int)c <= 0x0333)
{
- sbOutput.Append(ch);
+ // skip char modifier and diacritics
+ }
+ else if (remove.IndexOf(c) > -1)
+ {
+ // skip chars we are removing
+ }
+ else if (spacers.IndexOf(c) > -1)
+ {
+ sb.Append(" ");
+ }
+ else if (c == '&')
+ {
+ sb.Append(" and ");
+ }
+ else
+ {
+ sb.Append(c);
}
}
- return sbOutput.ToString();
- }
-
- ///
- /// Extracts info for each episode into invididual xml files so that they can be easily accessed without having to step through the entire series xml
- ///
- /// The series data path.
- /// The XML file.
- /// The last tv db update time.
- /// Task.
- private async Task ExtractEpisodes(string seriesDataPath, string xmlFile, long? lastTvDbUpdateTime)
- {
- var settings = new XmlReaderSettings
- {
- CheckCharacters = false,
- IgnoreProcessingInstructions = true,
- IgnoreComments = true,
- ValidationType = ValidationType.None
- };
+ name = sb.ToString();
+ name = name.Replace(", the", "");
+ name = name.Replace("the ", " ");
+ name = name.Replace(" the ", " ");
- using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8))
+ string prevName;
+ do
{
- // Use XmlReader for best performance
- using (var reader = XmlReader.Create(streamReader, settings))
- {
- reader.MoveToContent();
-
- // Loop through each element
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Episode":
- {
- var outerXml = reader.ReadOuterXml();
-
- await SaveEpsiodeXml(seriesDataPath, outerXml, lastTvDbUpdateTime).ConfigureAwait(false);
- break;
- }
+ prevName = name;
+ name = name.Replace(" ", " ");
+ } while (name.Length != prevName.Length);
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
- }
+ return name.Trim();
}
- private async Task SaveEpsiodeXml(string seriesDataPath, string xml, long? lastTvDbUpdateTime)
+ private void FetchSeriesInfo(Series item, string seriesXmlPath, CancellationToken cancellationToken)
{
var settings = new XmlReaderSettings
{
@@ -475,12 +303,9 @@ namespace MediaBrowser.Providers.TV
ValidationType = ValidationType.None
};
- var seasonNumber = -1;
- var episodeNumber = -1;
- var absoluteNumber = -1;
- var lastUpdateString = string.Empty;
+ var episiodeAirDates = new List();
- using (var streamReader = new StringReader(xml))
+ using (var streamReader = new StreamReader(seriesXmlPath, Encoding.UTF8))
{
// Use XmlReader for best performance
using (var reader = XmlReader.Create(streamReader, settings))
@@ -490,53 +315,30 @@ namespace MediaBrowser.Providers.TV
// Loop through each element
while (reader.Read())
{
+ cancellationToken.ThrowIfCancellationRequested();
+
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
- case "lastupdated":
- {
- lastUpdateString = reader.ReadElementContentAsString();
- break;
- }
-
- case "EpisodeNumber":
+ case "Series":
{
- var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ using (var subtree = reader.ReadSubtree())
{
- int num;
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out num))
- {
- episodeNumber = num;
- }
+ FetchDataFromSeriesNode(item, subtree, cancellationToken);
}
break;
}
- case "absolute_number":
+ case "Episode":
{
- var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
+ using (var subtree = reader.ReadSubtree())
{
- int num;
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out num))
- {
- absoluteNumber = num;
- }
- }
- break;
- }
+ var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken);
- case "SeasonNumber":
- {
- var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
- {
- int num;
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out num))
+ if (date.HasValue)
{
- seasonNumber = num;
+ episiodeAirDates.Add(date.Value);
}
}
break;
@@ -551,76 +353,83 @@ namespace MediaBrowser.Providers.TV
}
}
- var hasEpisodeChanged = true;
- if (!string.IsNullOrEmpty(lastUpdateString) && lastTvDbUpdateTime.HasValue)
+ if (item.Status.HasValue && item.Status.Value == SeriesStatus.Ended && episiodeAirDates.Count > 0)
{
- long num;
- if (long.TryParse(lastUpdateString, NumberStyles.Any, UsCulture, out num))
- {
- hasEpisodeChanged = num >= lastTvDbUpdateTime.Value;
- }
+ item.EndDate = episiodeAirDates.Max();
}
+ }
- var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber));
+ private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken)
+ {
+ DateTime? airDate = null;
+ int? seasonNumber = null;
- // Only save the file if not already there, or if the episode has changed
- if (hasEpisodeChanged || !File.Exists(file))
- {
- using (var writer = XmlWriter.Create(file, new XmlWriterSettings
- {
- Encoding = Encoding.UTF8,
- Async = true
- }))
- {
- await writer.WriteRawAsync(xml).ConfigureAwait(false);
- }
- }
+ reader.MoveToContent();
- if (absoluteNumber != -1)
+ // Loop through each element
+ while (reader.Read())
{
- file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", absoluteNumber));
+ cancellationToken.ThrowIfCancellationRequested();
- // Only save the file if not already there, or if the episode has changed
- if (hasEpisodeChanged || !File.Exists(file))
+ if (reader.NodeType == XmlNodeType.Element)
{
- using (var writer = XmlWriter.Create(file, new XmlWriterSettings
- {
- Encoding = Encoding.UTF8,
- Async = true
- }))
+ switch (reader.Name)
{
- await writer.WriteRawAsync(xml).ConfigureAwait(false);
+ case "FirstAired":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ DateTime date;
+ if (DateTime.TryParse(val, out date))
+ {
+ airDate = date.ToUniversalTime();
+ }
+ }
+
+ break;
+ }
+
+ case "SeasonNumber":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ int rval;
+
+ // int.TryParse is local aware, so it can be probamatic, force us culture
+ if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
+ {
+ seasonNumber = rval;
+ }
+ }
+
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
}
}
}
- }
- ///
- /// Gets the series data path.
- ///
- /// The app paths.
- /// The series id.
- /// System.String.
- internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId)
- {
- var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
+ if (seasonNumber.HasValue && seasonNumber.Value != 0)
+ {
+ return airDate;
+ }
- return seriesDataPath;
+ return null;
}
///
- /// Gets the series data path.
+ /// Fetches the actors.
///
- /// The app paths.
- /// System.String.
- internal static string GetSeriesDataPath(IApplicationPaths appPaths)
- {
- var dataPath = Path.Combine(appPaths.DataPath, "tvdb-v3");
-
- return dataPath;
- }
-
- private void FetchSeriesInfo(Series item, string seriesXmlPath, CancellationToken cancellationToken)
+ /// The series.
+ /// The actors XML path.
+ private void FetchActors(Series series, string actorsXmlPath)
{
var settings = new XmlReaderSettings
{
@@ -630,9 +439,7 @@ namespace MediaBrowser.Providers.TV
ValidationType = ValidationType.None
};
- var episiodeAirDates = new List();
-
- using (var streamReader = new StreamReader(seriesXmlPath, Encoding.UTF8))
+ using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8))
{
// Use XmlReader for best performance
using (var reader = XmlReader.Create(streamReader, settings))
@@ -642,35 +449,18 @@ namespace MediaBrowser.Providers.TV
// Loop through each element
while (reader.Read())
{
- cancellationToken.ThrowIfCancellationRequested();
-
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
- case "Series":
- {
- using (var subtree = reader.ReadSubtree())
- {
- FetchDataFromSeriesNode(item, subtree, cancellationToken);
- }
- break;
- }
-
- case "Episode":
+ case "Actor":
{
using (var subtree = reader.ReadSubtree())
{
- var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken);
-
- if (date.HasValue)
- {
- episiodeAirDates.Add(date.Value);
- }
+ FetchDataFromActorNode(series, subtree);
}
break;
}
-
default:
reader.Skip();
break;
@@ -679,44 +469,94 @@ namespace MediaBrowser.Providers.TV
}
}
}
-
- if (item.Status.HasValue && item.Status.Value == SeriesStatus.Ended && episiodeAirDates.Count > 0)
- {
- item.EndDate = episiodeAirDates.Max();
- }
}
- private void FetchDataFromSeriesNode(Series item, XmlReader reader, CancellationToken cancellationToken)
+ ///
+ /// Fetches the data from actor node.
+ ///
+ /// The series.
+ /// The reader.
+ private void FetchDataFromActorNode(Series series, XmlReader reader)
{
reader.MoveToContent();
- // Loop through each element
+ var personInfo = new PersonInfo();
+
while (reader.Read())
{
- cancellationToken.ThrowIfCancellationRequested();
-
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
- case "SeriesName":
+ case "Name":
{
- if (!item.LockedFields.Contains(MetadataFields.Name))
- {
- item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
- }
+ personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
break;
}
- case "Overview":
+ case "Role":
+ {
+ personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
+ break;
+ }
+
+ case "SortOrder":
{
- if (!item.LockedFields.Contains(MetadataFields.Overview))
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
{
- item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
+ int rval;
+
+ // int.TryParse is local aware, so it can be probamatic, force us culture
+ if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
+ {
+ personInfo.SortOrder = rval;
+ }
}
break;
}
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ personInfo.Type = PersonType.Actor;
+
+ if (!string.IsNullOrEmpty(personInfo.Name))
+ {
+ series.AddPerson(personInfo);
+ }
+ }
+
+ private void FetchDataFromSeriesNode(Series item, XmlReader reader, CancellationToken cancellationToken)
+ {
+ reader.MoveToContent();
+
+ // Loop through each element
+ while (reader.Read())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "SeriesName":
+ {
+ item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
+ break;
+ }
+
+ case "Overview":
+ {
+ item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
+ break;
+ }
+
case "Airs_DayOfWeek":
{
var val = reader.ReadElementContentAsString();
@@ -745,10 +585,7 @@ namespace MediaBrowser.Providers.TV
if (!string.IsNullOrWhiteSpace(val))
{
- if (!item.LockedFields.Contains(MetadataFields.OfficialRating))
- {
- item.OfficialRating = val;
- }
+ item.OfficialRating = val;
}
break;
}
@@ -765,7 +602,7 @@ namespace MediaBrowser.Providers.TV
float rval;
// float.TryParse is local aware, so it can be probamatic, force us culture
- if (float.TryParse(val, NumberStyles.AllowDecimalPoint, UsCulture, out rval))
+ if (float.TryParse(val, NumberStyles.AllowDecimalPoint, _usCulture, out rval))
{
item.CommunityRating = rval;
}
@@ -782,7 +619,7 @@ namespace MediaBrowser.Providers.TV
int rval;
// int.TryParse is local aware, so it can be probamatic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval))
+ if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
{
item.VoteCount = rval;
}
@@ -853,12 +690,12 @@ namespace MediaBrowser.Providers.TV
{
var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val) && !item.LockedFields.Contains(MetadataFields.Runtime))
+ if (!string.IsNullOrWhiteSpace(val))
{
int rval;
// int.TryParse is local aware, so it can be probamatic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval))
+ if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
{
item.RunTimeTicks = TimeSpan.FromMinutes(rval).Ticks;
}
@@ -873,23 +710,19 @@ namespace MediaBrowser.Providers.TV
if (!string.IsNullOrWhiteSpace(val))
{
- // Only fill this in if there's no existing genres, because Imdb data from Omdb is preferred
- if (!item.LockedFields.Contains(MetadataFields.Genres) && (item.Genres.Count == 0 || !string.Equals(item.GetPreferredMetadataLanguage(), "en", StringComparison.OrdinalIgnoreCase)))
+ var vals = val
+ .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(i => i.Trim())
+ .Where(i => !string.IsNullOrWhiteSpace(i))
+ .ToList();
+
+ if (vals.Count > 0)
{
- var vals = val
- .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(i => i.Trim())
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .ToList();
+ item.Genres.Clear();
- if (vals.Count > 0)
+ foreach (var genre in vals)
{
- item.Genres.Clear();
-
- foreach (var genre in vals)
- {
- item.AddGenre(genre);
- }
+ item.AddGenre(genre);
}
}
}
@@ -903,22 +736,19 @@ namespace MediaBrowser.Providers.TV
if (!string.IsNullOrWhiteSpace(val))
{
- if (!item.LockedFields.Contains(MetadataFields.Studios))
+ var vals = val
+ .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(i => i.Trim())
+ .Where(i => !string.IsNullOrWhiteSpace(i))
+ .ToList();
+
+ if (vals.Count > 0)
{
- var vals = val
- .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(i => i.Trim())
- .Where(i => !string.IsNullOrWhiteSpace(i))
- .ToList();
+ item.Studios.Clear();
- if (vals.Count > 0)
+ foreach (var genre in vals)
{
- item.Studios.Clear();
-
- foreach (var genre in vals)
- {
- item.AddStudio(genre);
- }
+ item.AddStudio(genre);
}
}
}
@@ -934,78 +764,56 @@ namespace MediaBrowser.Providers.TV
}
}
- private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken)
+ ///
+ /// Extracts info for each episode into invididual xml files so that they can be easily accessed without having to step through the entire series xml
+ ///
+ /// The series data path.
+ /// The XML file.
+ /// The last tv db update time.
+ /// Task.
+ private async Task ExtractEpisodes(string seriesDataPath, string xmlFile, long? lastTvDbUpdateTime)
{
- DateTime? airDate = null;
- int? seasonNumber = null;
-
- reader.MoveToContent();
-
- // Loop through each element
- while (reader.Read())
+ var settings = new XmlReaderSettings
{
- cancellationToken.ThrowIfCancellationRequested();
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true,
+ ValidationType = ValidationType.None
+ };
- if (reader.NodeType == XmlNodeType.Element)
+ using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8))
+ {
+ // Use XmlReader for best performance
+ using (var reader = XmlReader.Create(streamReader, settings))
{
- switch (reader.Name)
+ reader.MoveToContent();
+
+ // Loop through each element
+ while (reader.Read())
{
- case "FirstAired":
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- DateTime date;
- if (DateTime.TryParse(val, out date))
+ case "Episode":
{
- airDate = date.ToUniversalTime();
- }
- }
-
- break;
- }
-
- case "SeasonNumber":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- int rval;
+ var outerXml = reader.ReadOuterXml();
- // int.TryParse is local aware, so it can be probamatic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval))
- {
- seasonNumber = rval;
+ await SaveEpsiodeXml(seriesDataPath, outerXml, lastTvDbUpdateTime).ConfigureAwait(false);
+ break;
}
- }
- break;
+ default:
+ reader.Skip();
+ break;
}
-
- default:
- reader.Skip();
- break;
+ }
}
}
}
-
- if (seasonNumber.HasValue && seasonNumber.Value != 0)
- {
- return airDate;
- }
-
- return null;
}
- ///
- /// Fetches the actors.
- ///
- /// The series.
- /// The actors XML path.
- /// The cancellation token.
- private void FetchActors(Series series, string actorsXmlPath, CancellationToken cancellationToken)
+ private async Task SaveEpsiodeXml(string seriesDataPath, string xml, long? lastTvDbUpdateTime)
{
var settings = new XmlReaderSettings
{
@@ -1015,7 +823,12 @@ namespace MediaBrowser.Providers.TV
ValidationType = ValidationType.None
};
- using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8))
+ var seasonNumber = -1;
+ var episodeNumber = -1;
+ var absoluteNumber = -1;
+ var lastUpdateString = string.Empty;
+
+ using (var streamReader = new StringReader(xml))
{
// Use XmlReader for best performance
using (var reader = XmlReader.Create(streamReader, settings))
@@ -1025,20 +838,58 @@ namespace MediaBrowser.Providers.TV
// Loop through each element
while (reader.Read())
{
- cancellationToken.ThrowIfCancellationRequested();
-
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
- case "Actor":
+ case "lastupdated":
{
- using (var subtree = reader.ReadSubtree())
+ lastUpdateString = reader.ReadElementContentAsString();
+ break;
+ }
+
+ case "EpisodeNumber":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(val))
{
- FetchDataFromActorNode(series, subtree);
+ int num;
+ if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
+ {
+ episodeNumber = num;
+ }
+ }
+ break;
+ }
+
+ case "absolute_number":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ int num;
+ if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
+ {
+ absoluteNumber = num;
+ }
+ }
+ break;
+ }
+
+ case "SeasonNumber":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ int num;
+ if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
+ {
+ seasonNumber = num;
+ }
}
break;
}
+
default:
reader.Skip();
break;
@@ -1047,221 +898,189 @@ namespace MediaBrowser.Providers.TV
}
}
}
- }
-
- ///
- /// Fetches the data from actor node.
- ///
- /// The series.
- /// The reader.
- private void FetchDataFromActorNode(Series series, XmlReader reader)
- {
- reader.MoveToContent();
-
- var personInfo = new PersonInfo();
- while (reader.Read())
+ var hasEpisodeChanged = true;
+ if (!string.IsNullOrEmpty(lastUpdateString) && lastTvDbUpdateTime.HasValue)
{
- if (reader.NodeType == XmlNodeType.Element)
+ long num;
+ if (long.TryParse(lastUpdateString, NumberStyles.Any, _usCulture, out num))
{
- switch (reader.Name)
- {
- case "Name":
- {
- personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
- break;
- }
-
- case "Role":
- {
- personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
- break;
- }
-
- case "SortOrder":
- {
- var val = reader.ReadElementContentAsString();
+ hasEpisodeChanged = num >= lastTvDbUpdateTime.Value;
+ }
+ }
- if (!string.IsNullOrWhiteSpace(val))
- {
- int rval;
+ var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber));
- // int.TryParse is local aware, so it can be probamatic, force us culture
- if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval))
- {
- personInfo.SortOrder = rval;
- }
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
+ // Only save the file if not already there, or if the episode has changed
+ if (hasEpisodeChanged || !File.Exists(file))
+ {
+ using (var writer = XmlWriter.Create(file, new XmlWriterSettings
+ {
+ Encoding = Encoding.UTF8,
+ Async = true
+ }))
+ {
+ await writer.WriteRawAsync(xml).ConfigureAwait(false);
}
}
- personInfo.Type = PersonType.Actor;
-
- if (!string.IsNullOrEmpty(personInfo.Name))
+ if (absoluteNumber != -1)
{
- series.AddPerson(personInfo);
+ file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", absoluteNumber));
+
+ // Only save the file if not already there, or if the episode has changed
+ if (hasEpisodeChanged || !File.Exists(file))
+ {
+ using (var writer = XmlWriter.Create(file, new XmlWriterSettings
+ {
+ Encoding = Encoding.UTF8,
+ Async = true
+ }))
+ {
+ await writer.WriteRawAsync(xml).ConfigureAwait(false);
+ }
+ }
}
}
///
- /// The us culture
- ///
- protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
- ///
- /// Determines whether [has local meta] [the specified item].
+ /// Gets the series data path.
///
- /// The item.
- /// true if [has local meta] [the specified item]; otherwise, false.
- private bool HasLocalMeta(BaseItem item)
+ /// The app paths.
+ /// The series id.
+ /// System.String.
+ internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId)
{
- return item.ResolveArgs.ContainsMetaFileByName(LocalMetaFileName);
+ var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId);
+
+ return seriesDataPath;
}
///
- /// Finds the series.
+ /// Gets the series data path.
///
- /// The name.
- /// The cancellation token.
- /// Task{System.String}.
- private async Task FindSeries(string name, CancellationToken cancellationToken)
+ /// The app paths.
+ /// System.String.
+ internal static string GetSeriesDataPath(IApplicationPaths appPaths)
{
- var url = string.Format(RootUrl + SeriesQuery, WebUtility.UrlEncode(name));
- var doc = new XmlDocument();
+ var dataPath = Path.Combine(appPaths.DataPath, "tvdb-v3");
- using (var results = await HttpClient.Get(new HttpRequestOptions
- {
- Url = url,
- ResourcePool = TvDbResourcePool,
- CancellationToken = cancellationToken
+ return dataPath;
+ }
- }).ConfigureAwait(false))
+ private void DeleteXmlFiles(string path)
+ {
+ try
{
- doc.Load(results);
+ foreach (var file in new DirectoryInfo(path)
+ .EnumerateFiles("*.xml", SearchOption.AllDirectories)
+ .ToList())
+ {
+ file.Delete();
+ }
}
-
- if (doc.HasChildNodes)
+ catch (DirectoryNotFoundException)
{
- var nodes = doc.SelectNodes("//Series");
- var comparableName = GetComparableName(name);
- if (nodes != null)
- {
- foreach (XmlNode node in nodes)
- {
- var titles = new List();
-
- var nameNode = node.SelectSingleNode("./SeriesName");
- if (nameNode != null)
- {
- titles.Add(GetComparableName(nameNode.InnerText));
- }
+ // No biggie
+ }
+ }
- var aliasNode = node.SelectSingleNode("./AliasNames");
- if (aliasNode != null)
- {
- var alias = aliasNode.InnerText.Split('|').Select(GetComparableName);
- titles.AddRange(alias);
- }
+ ///
+ /// Sanitizes the XML file.
+ ///
+ /// The file.
+ /// Task.
+ private async Task SanitizeXmlFile(string file)
+ {
+ string validXml;
- if (titles.Any(t => string.Equals(t, comparableName, StringComparison.OrdinalIgnoreCase)))
- {
- var id = node.SelectSingleNode("./seriesid");
- if (id != null)
- return id.InnerText;
- }
+ using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, true))
+ {
+ using (var reader = new StreamReader(fileStream))
+ {
+ var xml = await reader.ReadToEndAsync().ConfigureAwait(false);
- foreach (var title in titles)
- {
- Logger.Info("TVDb Provider - " + title + " did not match " + comparableName);
- }
- }
+ validXml = StripInvalidXmlCharacters(xml);
}
}
- // Try stripping off the year if it was supplied
- var parenthIndex = name.LastIndexOf('(');
-
- if (parenthIndex != -1)
+ using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, true))
{
- var newName = name.Substring(0, parenthIndex);
-
- return await FindSeries(newName, cancellationToken);
+ using (var writer = new StreamWriter(fileStream))
+ {
+ await writer.WriteAsync(validXml).ConfigureAwait(false);
+ }
}
-
- Logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org.");
- return null;
}
///
- /// The remove
- ///
- const string remove = "\"'!`?";
- ///
- /// The spacers
- ///
- const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
-
- ///
- /// Gets the name of the comparable.
+ /// Strips the invalid XML characters.
///
- /// The name.
+ /// The in string.
/// System.String.
- internal static string GetComparableName(string name)
+ public static string StripInvalidXmlCharacters(string inString)
{
- name = name.ToLower();
- name = name.Normalize(NormalizationForm.FormKD);
- var sb = new StringBuilder();
- foreach (var c in name)
+ if (inString == null) return null;
+
+ var sbOutput = new StringBuilder();
+ char ch;
+
+ for (int i = 0; i < inString.Length; i++)
{
- if ((int)c >= 0x2B0 && (int)c <= 0x0333)
- {
- // skip char modifier and diacritics
- }
- else if (remove.IndexOf(c) > -1)
- {
- // skip chars we are removing
- }
- else if (spacers.IndexOf(c) > -1)
+ ch = inString[i];
+ if ((ch >= 0x0020 && ch <= 0xD7FF) ||
+ (ch >= 0xE000 && ch <= 0xFFFD) ||
+ ch == 0x0009 ||
+ ch == 0x000A ||
+ ch == 0x000D)
{
- sb.Append(" ");
+ sbOutput.Append(ch);
}
- else if (c == '&')
+ }
+ return sbOutput.ToString();
+ }
+
+ public string Name
+ {
+ get { return "TheTVDB"; }
+ }
+
+ public bool HasChanged(IHasMetadata item, DateTime date)
+ {
+ var seriesId = item.GetProviderId(MetadataProviders.Tvdb);
+
+ if (!string.IsNullOrEmpty(seriesId))
+ {
+ var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesId);
+
+ try
{
- sb.Append(" and ");
+ var files = new DirectoryInfo(seriesDataPath).EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly)
+ .ToList();
+
+ var seriesXmlFilename = item.GetPreferredMetadataLanguage() + ".xml";
+
+ var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase));
+
+ if (seriesFile != null && seriesFile.Exists && _fileSystem.GetLastWriteTimeUtc(seriesFile) > date)
+ {
+ return true;
+ }
+
+ var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase));
+
+ if (actorsXml != null && actorsXml.Exists && _fileSystem.GetLastWriteTimeUtc(actorsXml) > date)
+ {
+ return true;
+ }
}
- else
+ catch (DirectoryNotFoundException)
{
- sb.Append(c);
+ // Don't blow up
}
}
- name = sb.ToString();
- name = name.Replace(", the", "");
- name = name.Replace("the ", " ");
- name = name.Replace(" the ", " ");
-
- string prevName;
- do
- {
- prevName = name;
- name = name.Replace(" ", " ");
- } while (name.Length != prevName.Length);
-
- return name.Trim();
- }
- ///
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- ///
- public void Dispose()
- {
- Dispose(true);
+ return false;
}
}
}
diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
index 789cc1100b..1e04f7e097 100644
--- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
+++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
@@ -208,7 +208,6 @@ namespace MediaBrowser.Server.Implementations.Library
/// The prescan tasks.
/// The postscan tasks.
/// The people prescan tasks.
- /// The savers.
public void AddParts(IEnumerable rules,
IEnumerable pluginFolders,
IEnumerable resolvers,
@@ -277,7 +276,7 @@ namespace MediaBrowser.Server.Implementations.Library
/// The configuration.
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();
+ if (seasonZeroNameChanged || ibnPathChanged)
+ {
+ _taskManager.CancelIfRunningAndQueue();
+ }
});
}