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(); + } }); }