From 403534f80ecb13dfffbcbd34bee19bfd1aa885f7 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 4 Nov 2013 15:49:36 -0500 Subject: fixes #605 - Add manual image selection for Series --- MediaBrowser.Api/RemoteImageService.cs | 2 +- .../MediaBrowser.Providers.csproj | 7 +- .../TV/ManualFanartSeasonProvider.cs | 2 +- .../TV/ManualFanartSeriesProvider.cs | 2 +- .../TV/ManualTvdbEpisodeImageProvider.cs | 4 +- .../TV/ManualTvdbSeasonImageProvider.cs | 12 +- .../TV/ManualTvdbSeriesImageProvider.cs | 312 +++++ MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs | 720 ------------ MediaBrowser.Providers/TV/RemoteSeasonProvider.cs | 204 ---- MediaBrowser.Providers/TV/RemoteSeriesProvider.cs | 1228 -------------------- MediaBrowser.Providers/TV/SeriesPostScanTask.cs | 2 +- MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs | 720 ++++++++++++ .../TV/TvdbPersonImageProvider.cs | 4 +- MediaBrowser.Providers/TV/TvdbPrescanTask.cs | 8 +- MediaBrowser.Providers/TV/TvdbSeasonProvider.cs | 204 ++++ .../TV/TvdbSeriesImageProvider.cs | 246 +--- MediaBrowser.Providers/TV/TvdbSeriesProvider.cs | 1228 ++++++++++++++++++++ 17 files changed, 2509 insertions(+), 2396 deletions(-) create mode 100644 MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs delete mode 100644 MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs delete mode 100644 MediaBrowser.Providers/TV/RemoteSeasonProvider.cs delete mode 100644 MediaBrowser.Providers/TV/RemoteSeriesProvider.cs create mode 100644 MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs create mode 100644 MediaBrowser.Providers/TV/TvdbSeasonProvider.cs create mode 100644 MediaBrowser.Providers/TV/TvdbSeriesProvider.cs diff --git a/MediaBrowser.Api/RemoteImageService.cs b/MediaBrowser.Api/RemoteImageService.cs index c5f1005c7..e64badd05 100644 --- a/MediaBrowser.Api/RemoteImageService.cs +++ b/MediaBrowser.Api/RemoteImageService.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Api [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } - [ApiMember(Name = "Type", Description = "The image type", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "Type", Description = "The image type", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public ImageType? Type { get; set; } /// diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 4f0f2a7d9..128db86b2 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -114,9 +114,10 @@ - - - + + + + diff --git a/MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs b/MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs index b66d9faf5..06115a6de 100644 --- a/MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs +++ b/MediaBrowser.Providers/TV/ManualFanartSeasonProvider.cs @@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.TV public int Priority { - get { return 1; } + get { return 0; } } } } diff --git a/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs b/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs index 21b8e278c..718e7849e 100644 --- a/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/ManualFanartSeriesProvider.cs @@ -303,7 +303,7 @@ namespace MediaBrowser.Providers.TV public int Priority { - get { return 1; } + get { return 0; } } } } diff --git a/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs index 003b40f6d..1b6d37639 100644 --- a/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs @@ -52,9 +52,9 @@ namespace MediaBrowser.Providers.TV if (!string.IsNullOrEmpty(seriesId)) { // Process images - var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); - var files = RemoteEpisodeProvider.Current.GetEpisodeXmlFiles(episode, seriesDataPath); + var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode, seriesDataPath); var result = files.Select(i => GetImageInfo(i, cancellationToken)).Where(i => i != null); diff --git a/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs index 211ec5a84..9c4b31d09 100644 --- a/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.Providers.TV if (!string.IsNullOrEmpty(seriesId) && season.IndexNumber.HasValue) { // Process images - var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); var path = Path.Combine(seriesDataPath, "banners.xml"); @@ -279,10 +279,14 @@ namespace MediaBrowser.Providers.TV ProviderName = Name, Language = language, Width = width, - Height = height, - ThumbnailUrl = thumbnailUrl + Height = height }; + if (!string.IsNullOrEmpty(thumbnailUrl)) + { + imageInfo.ThumbnailUrl = TVUtils.BannerUrl + thumbnailUrl; + } + if (string.Equals(bannerType, "season", StringComparison.OrdinalIgnoreCase)) { if (string.Equals(bannerType2, "season", StringComparison.OrdinalIgnoreCase)) @@ -307,7 +311,7 @@ namespace MediaBrowser.Providers.TV public int Priority { - get { return 0; } + get { return 1; } } } } diff --git a/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs new file mode 100644 index 000000000..fa09681e7 --- /dev/null +++ b/MediaBrowser.Providers/TV/ManualTvdbSeriesImageProvider.cs @@ -0,0 +1,312 @@ +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 : IImageProvider + { + private readonly IServerConfigurationManager _config; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public ManualTvdbSeriesImageProvider(IServerConfigurationManager config) + { + _config = config; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "TvDb"; } + } + + public bool Supports(BaseItem item) + { + return item is Series; + } + + public async Task> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + { + var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); + + return images.Where(i => i.Type == imageType); + } + + public Task> GetAllImages(BaseItem item, CancellationToken cancellationToken) + { + var seriesId = item.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, 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, 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 language = _config.Configuration.PreferredMetadataLanguage; + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + return list.OrderByDescending(i => + { + if (string.Equals(language, 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) + .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 Priority + { + get { return 1; } + } + } +} diff --git a/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs b/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs deleted file mode 100644 index 63b755bf9..000000000 --- a/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs +++ /dev/null @@ -1,720 +0,0 @@ -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.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; - -namespace MediaBrowser.Providers.TV -{ - - /// - /// Class RemoteEpisodeProvider - /// - class RemoteEpisodeProvider : BaseMetadataProvider - { - /// - /// The _provider manager - /// - private readonly IProviderManager _providerManager; - - /// - /// Gets the HTTP client. - /// - /// The HTTP client. - protected IHttpClient HttpClient { get; private set; } - private readonly IFileSystem _fileSystem; - - internal static RemoteEpisodeProvider Current; - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP client. - /// The log manager. - /// The configuration manager. - /// The provider manager. - public RemoteEpisodeProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem) - : base(logManager, configurationManager) - { - HttpClient = httpClient; - _providerManager = providerManager; - _fileSystem = fileSystem; - Current = this; - } - - /// - /// Supportses the specified item. - /// - /// The item. - /// true if XXXX, false otherwise - public override bool Supports(BaseItem item) - { - return item is Episode; - } - - public override ItemUpdateType ItemUpdateType - { - get - { - return ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataDownload; - } - } - - /// - /// Gets the priority. - /// - /// The priority. - public override MetadataProviderPriority Priority - { - get { return MetadataProviderPriority.Third; } - } - - /// - /// 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 "1"; - } - } - - /// - /// Needses the refresh internal. - /// - /// The item. - /// The provider info. - /// true if XXXX, false otherwise - protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) - { - var locationType = item.LocationType; - - // Always use tvdb updates for non-file system episodes - if (locationType != LocationType.Remote && locationType != LocationType.Virtual) - { - // Don't proceed if there's local metadata - if (!ConfigurationManager.Configuration.EnableTvDbUpdates && HasLocalMeta(item)) - { - return false; - } - } - - return base.NeedsRefreshInternal(item, providerInfo); - } - - protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) - { - var episode = (Episode)item; - - var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; - - if (!string.IsNullOrEmpty(seriesId)) - { - // Process images - var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); - - var files = GetEpisodeXmlFiles(episode, seriesDataPath); - - if (files.Count > 0) - { - return files.Select(i => _fileSystem.GetLastWriteTimeUtc(i)).Max() > providerInfo.LastRefreshed; - } - } - - return false; - } - - /// - /// Gets the episode XML files. - /// - /// The episode. - /// The series data path. - /// List{FileInfo}. - internal List GetEpisodeXmlFiles(Episode episode, string seriesDataPath) - { - var files = new List(); - - if (episode.IndexNumber == null) - { - return files; - } - - var episodeNumber = episode.IndexNumber.Value; - var seasonNumber = episode.ParentIndexNumber; - - if (seasonNumber == null) - { - return files; - } - - var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); - - var fileInfo = new FileInfo(file); - var usingAbsoluteData = false; - - if (fileInfo.Exists) - { - files.Add(fileInfo); - } - else - { - file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); - fileInfo = new FileInfo(file); - if (fileInfo.Exists) - { - files.Add(fileInfo); - usingAbsoluteData = true; - } - } - - var end = episode.IndexNumberEnd ?? episodeNumber; - episodeNumber++; - - while (episodeNumber <= end) - { - if (usingAbsoluteData) - { - file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); - } - else - { - file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); - } - - fileInfo = new FileInfo(file); - if (fileInfo.Exists) - { - files.Add(fileInfo); - } - else - { - break; - } - - episodeNumber++; - } - - return files; - } - - /// - /// 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, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var status = ProviderRefreshStatus.Success; - - var episode = (Episode)item; - - var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; - - if (!string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); - - try - { - status = await FetchEpisodeData(episode, seriesDataPath, cancellationToken).ConfigureAwait(false); - } - catch (FileNotFoundException) - { - // Don't fail the provider because this will just keep on going and going. - } - } - - SetLastRefreshed(item, DateTime.UtcNow, status); - return true; - } - - - /// - /// Fetches the episode data. - /// - /// The episode. - /// The series data path. - /// The cancellation token. - /// Task{System.Boolean}. - private async Task FetchEpisodeData(Episode episode, string seriesDataPath, CancellationToken cancellationToken) - { - var status = ProviderRefreshStatus.Success; - - if (episode.IndexNumber == null) - { - return status; - } - - var episodeNumber = episode.IndexNumber.Value; - var seasonNumber = episode.ParentIndexNumber; - - if (seasonNumber == null) - { - return status; - } - - var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); - var success = false; - var usingAbsoluteData = false; - - try - { - status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); - - success = true; - } - catch (FileNotFoundException) - { - // Could be using absolute numbering - if (seasonNumber.Value != 1) - { - throw; - } - } - - if (!success) - { - file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); - - status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); - usingAbsoluteData = true; - } - - var end = episode.IndexNumberEnd ?? episodeNumber; - episodeNumber++; - - while (episodeNumber <= end) - { - if (usingAbsoluteData) - { - file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); - } - else - { - file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); - } - - try - { - FetchAdditionalPartInfo(episode, file, cancellationToken); - } - catch (FileNotFoundException) - { - break; - } - - episodeNumber++; - } - - return status; - } - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private async Task FetchMainEpisodeInfo(Episode item, string xmlFile, CancellationToken cancellationToken) - { - var status = ProviderRefreshStatus.Success; - - using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - item.People.Clear(); - } - - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - })) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "id": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.SetProviderId(MetadataProviders.Tvdb, val); - } - break; - } - - case "IMDB_ID": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.SetProviderId(MetadataProviders.Imdb, val); - } - break; - } - - case "EpisodeName": - { - if (!item.LockedFields.Contains(MetadataFields.Name)) - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.Name = val; - } - } - break; - } - - case "Language": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.Language = val; - } - break; - } - - case "filename": - { - if (string.IsNullOrEmpty(item.PrimaryImagePath)) - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - try - { - var url = TVUtils.BannerUrl + val; - - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); - } - catch (HttpException) - { - status = ProviderRefreshStatus.CompletedWithErrors; - } - } - } - break; - } - - case "Overview": - { - if (!item.LockedFields.Contains(MetadataFields.Overview)) - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.Overview = val; - } - } - break; - } - case "Rating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - float rval; - - // float.TryParse is local aware, so it can be probamatic, force us culture - if (float.TryParse(val, NumberStyles.AllowDecimalPoint, _usCulture, out rval)) - { - item.CommunityRating = rval; - } - } - break; - } - case "RatingCount": - { - 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)) - { - item.VoteCount = rval; - } - } - - break; - } - - case "FirstAired": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - DateTime date; - if (DateTime.TryParse(val, out date)) - { - date = date.ToUniversalTime(); - - item.PremiereDate = date; - item.ProductionYear = date.Year; - } - } - - break; - } - - case "Director": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddPeople(item, val, PersonType.Director); - } - } - - break; - } - case "GuestStars": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddGuestStars(item, val); - } - } - - break; - } - case "Writer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddPeople(item, val, PersonType.Writer); - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - } - - return status; - } - - private void AddPeople(BaseItem item, string val, string personType) - { - // Sometimes tvdb actors have leading spaces - foreach (var person in val.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Select(str => new PersonInfo { Type = personType, Name = str.Trim() })) - { - item.AddPerson(person); - } - } - - private void AddGuestStars(BaseItem item, string val) - { - // Sometimes tvdb actors have leading spaces - //Regex Info: - //The first block are the posible delimitators (open-parentheses should be there cause if dont the next block will fail) - //The second block Allow the delimitators to be part of the text if they're inside parentheses - var persons = Regex.Matches(val, @"(?([^|,(])|(?\([^)]*\)*))+") - .Cast() - .Select(m => m.Value) - .Where(i => !string.IsNullOrWhiteSpace(i) && !string.IsNullOrEmpty(i)); - - foreach (var person in persons.Select(str => - { - var nameGroup = str.Split(new[] { '(' }, 2, StringSplitOptions.RemoveEmptyEntries); - var name = nameGroup[0].Trim(); - var roles = nameGroup.Count() > 1 ? nameGroup[1].Trim() : null; - if (roles != null) - roles = roles.EndsWith(")") ? roles.Substring(0, roles.Length - 1) : roles; - return new PersonInfo { Type = PersonType.GuestStar, Name = name, Role = roles }; - })) - { - item.AddPerson(person); - } - } - - private void FetchAdditionalPartInfo(Episode item, string xmlFile, CancellationToken cancellationToken) - { - using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - })) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "EpisodeName": - { - if (!item.LockedFields.Contains(MetadataFields.Name)) - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.Name += ", " + val; - } - } - break; - } - - case "Overview": - { - if (!item.LockedFields.Contains(MetadataFields.Overview)) - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - item.Overview += Environment.NewLine + Environment.NewLine + val; - } - } - break; - } - case "Director": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddPeople(item, val, PersonType.Director); - } - } - - break; - } - case "GuestStars": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddGuestStars(item, val); - } - } - - break; - } - case "Writer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddPeople(item, val, PersonType.Writer); - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - } - } - - /// - /// Determines whether [has local meta] [the specified episode]. - /// - /// The episode. - /// true if [has local meta] [the specified episode]; otherwise, false. - private bool HasLocalMeta(BaseItem episode) - { - return (episode.Parent.ResolveArgs.ContainsMetaFileByName(Path.GetFileNameWithoutExtension(episode.Path) + ".xml")); - } - } -} diff --git a/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs b/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs deleted file mode 100644 index 744469847..000000000 --- a/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs +++ /dev/null @@ -1,204 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Providers; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - /// - /// Class RemoteSeasonProvider - /// - class RemoteSeasonProvider : BaseMetadataProvider - { - /// - /// The _provider manager - /// - private readonly IProviderManager _providerManager; - private readonly IFileSystem _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The log manager. - /// The configuration manager. - /// The provider manager. - /// httpClient - public RemoteSeasonProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem) - : base(logManager, configurationManager) - { - _providerManager = providerManager; - _fileSystem = fileSystem; - } - - /// - /// Supportses the specified item. - /// - /// The item. - /// true if XXXX, false otherwise - public override bool Supports(BaseItem item) - { - return item is Season; - } - - /// - /// Gets the priority. - /// - /// The priority. - public override MetadataProviderPriority Priority - { - // Run after fanart - get { return MetadataProviderPriority.Fourth; } - } - - /// - /// Gets a value indicating whether [requires internet]. - /// - /// true if [requires internet]; otherwise, false. - public override bool RequiresInternet - { - get - { - return true; - } - } - - public override ItemUpdateType ItemUpdateType - { - get - { - return ItemUpdateType.ImageUpdate; - } - } - - /// - /// 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"; - } - } - - protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) - { - var season = (Season)item; - var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; - - if (!string.IsNullOrEmpty(seriesId)) - { - // Process images - var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); - - var imagesFileInfo = new FileInfo(imagesXmlPath); - - if (imagesFileInfo.Exists) - { - return _fileSystem.GetLastWriteTimeUtc(imagesFileInfo) > providerInfo.LastRefreshed; - } - } - return false; - } - - protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) - { - if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Banner) && item.BackdropImagePaths.Count > 0) - { - return false; - } - return base.NeedsRefreshInternal(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, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartSeriesImageProvider.ProviderName).ConfigureAwait(false); - - const int backdropLimit = 1; - - await DownloadImages(item, images.ToList(), backdropLimit, cancellationToken).ConfigureAwait(false); - - SetLastRefreshed(item, DateTime.UtcNow); - return true; - } - - private async Task DownloadImages(BaseItem item, List images, int backdropLimit, CancellationToken cancellationToken) - { - if (!item.HasImage(ImageType.Primary)) - { - var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); - - if (image != null) - { - await _providerManager.SaveImage(item, image.Url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) - .ConfigureAwait(false); - } - } - - if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !item.HasImage(ImageType.Banner)) - { - var image = images.FirstOrDefault(i => i.Type == ImageType.Banner); - - if (image != null) - { - await _providerManager.SaveImage(item, image.Url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) - .ConfigureAwait(false); - } - } - - if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit) - { - var bdNo = item.BackdropImagePaths.Count; - - foreach (var backdrop in images.Where(i => i.Type == ImageType.Backdrop)) - { - var url = backdrop.Url; - - if (item.ContainsImageWithSourceUrl(url)) - { - continue; - } - - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken).ConfigureAwait(false); - - bdNo++; - - if (item.BackdropImagePaths.Count >= backdropLimit) break; - } - } - } - } -} diff --git a/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs b/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs deleted file mode 100644 index 3e2736cbc..000000000 --- a/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs +++ /dev/null @@ -1,1228 +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.IO; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; - -namespace MediaBrowser.Providers.TV -{ - /// - /// Class RemoteSeriesProvider - /// - class RemoteSeriesProvider : BaseMetadataProvider, IDisposable - { - /// - /// The tv db - /// - internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2); - - /// - /// Gets the current. - /// - /// The current. - internal static RemoteSeriesProvider 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 IFileSystem _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP client. - /// The log manager. - /// The configuration manager. - /// The zip client. - /// httpClient - public RemoteSeriesProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IZipClient zipClient, IFileSystem fileSystem) - : base(logManager, configurationManager) - { - if (httpClient == null) - { - throw new ArgumentNullException("httpClient"); - } - HttpClient = httpClient; - _zipClient = zipClient; - _fileSystem = fileSystem; - 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, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var series = (Series)item; - - var seriesId = series.GetProviderId(MetadataProviders.Tvdb); - - if (string.IsNullOrEmpty(seriesId)) - { - seriesId = await FindSeries(series.Name, cancellationToken).ConfigureAwait(false); - } - - cancellationToken.ThrowIfCancellationRequested(); - - if (!string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); - - await FetchSeriesData(series, seriesId, seriesDataPath, force, cancellationToken).ConfigureAwait(false); - } - - SetLastRefreshed(item, DateTime.UtcNow); - return true; - } - - /// - /// Fetches the series data. - /// - /// 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) - { - Directory.CreateDirectory(seriesDataPath); - - var files = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly) - .Select(Path.GetFileName) - .ToList(); - - var seriesXmlFilename = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"; - - // Only download if not already there - // The prescan task will take care of updates so we don't need to re-download here - if (!files.Contains("banners.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains("actors.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains(seriesXmlFilename, StringComparer.OrdinalIgnoreCase)) - { - await DownloadSeriesZip(seriesId, seriesDataPath, null, 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"); - - FetchSeriesInfo(series, seriesXmlPath, cancellationToken); - - if (!series.LockedFields.Contains(MetadataFields.Cast)) - { - series.People.Clear(); - - FetchActors(series, actorsXmlPath, cancellationToken); - } - } - } - } - - /// - /// Downloads the series zip. - /// - /// The series id. - /// The series data path. - /// The cancellation token. - /// Task. - internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, long? lastTvDbUpdateTime, CancellationToken cancellationToken) - { - var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage); - - using (var zipStream = await HttpClient.Get(new HttpRequestOptions - { - Url = url, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken - - }).ConfigureAwait(false)) - { - // Delete existing files - DeleteXmlFiles(seriesDataPath); - - // Copy to memory stream because we need a seekable stream - using (var ms = new MemoryStream()) - { - await zipStream.CopyToAsync(ms).ConfigureAwait(false); - - ms.Position = 0; - _zipClient.ExtractAll(ms, seriesDataPath, true); - } - } - - // Sanitize all files, except for extracted episode files - foreach (var file in Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.AllDirectories).ToList() - .Where(i => !Path.GetFileName(i).StartsWith("episode-", StringComparison.OrdinalIgnoreCase))) - { - await SanitizeXmlFile(file).ConfigureAwait(false); - } - - await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, ConfigurationManager.Configuration.PreferredMetadataLanguage + ".xml"), lastTvDbUpdateTime).ConfigureAwait(false); - } - - private void DeleteXmlFiles(string path) - { - try - { - foreach (var file in new DirectoryInfo(path) - .EnumerateFiles("*.xml", SearchOption.AllDirectories) - .ToList()) - { - file.Delete(); - } - } - catch (DirectoryNotFoundException) - { - // No biggie - } - } - - /// - /// 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)) - { - using (var reader = new StreamReader(fileStream)) - { - var xml = await reader.ReadToEndAsync().ConfigureAwait(false); - - validXml = StripInvalidXmlCharacters(xml); - } - } - - using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, true)) - { - using (var writer = new StreamWriter(fileStream)) - { - await writer.WriteAsync(validXml).ConfigureAwait(false); - } - } - } - - /// - /// Strips the invalid XML characters. - /// - /// The in string. - /// System.String. - public static string StripInvalidXmlCharacters(string inString) - { - if (inString == null) return null; - - var sbOutput = new StringBuilder(); - char ch; - - for (int i = 0; i < inString.Length; i++) - { - ch = inString[i]; - if ((ch >= 0x0020 && ch <= 0xD7FF) || - (ch >= 0xE000 && ch <= 0xFFFD) || - ch == 0x0009 || - ch == 0x000A || - ch == 0x000D) - { - sbOutput.Append(ch); - } - } - 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 - }; - - using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8)) - { - // 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; - } - - default: - reader.Skip(); - break; - } - } - } - } - } - } - - private async Task SaveEpsiodeXml(string seriesDataPath, string xml, long? lastTvDbUpdateTime) - { - var settings = new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }; - - 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)) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "lastupdated": - { - lastUpdateString = reader.ReadElementContentAsString(); - break; - } - - case "EpisodeNumber": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - 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; - } - } - } - } - } - - var hasEpisodeChanged = true; - if (!string.IsNullOrEmpty(lastUpdateString) && lastTvDbUpdateTime.HasValue) - { - long num; - if (long.TryParse(lastUpdateString, NumberStyles.Any, UsCulture, out num)) - { - hasEpisodeChanged = num >= lastTvDbUpdateTime.Value; - } - } - - var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber)); - - // 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); - } - } - - if (absoluteNumber != -1) - { - 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); - } - } - } - } - - /// - /// 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, "tvdb-v3"); - - return dataPath; - } - - private void FetchSeriesInfo(Series item, string seriesXmlPath, CancellationToken cancellationToken) - { - var settings = new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }; - - var episiodeAirDates = new List(); - - using (var streamReader = new StreamReader(seriesXmlPath, 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 "Series": - { - using (var subtree = reader.ReadSubtree()) - { - FetchDataFromSeriesNode(item, subtree, cancellationToken); - } - break; - } - - case "Episode": - { - using (var subtree = reader.ReadSubtree()) - { - var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken); - - if (date.HasValue) - { - episiodeAirDates.Add(date.Value); - } - } - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - } - - 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) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "SeriesName": - { - if (!item.LockedFields.Contains(MetadataFields.Name)) - { - item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - } - break; - } - - case "Overview": - { - if (!item.LockedFields.Contains(MetadataFields.Overview)) - { - item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - } - break; - } - - case "Airs_DayOfWeek": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.AirDays = TVUtils.GetAirDays(val); - } - break; - } - - case "Airs_Time": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.AirTime = val; - } - break; - } - - case "ContentRating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.OfficialRating)) - { - item.OfficialRating = val; - } - } - break; - } - - case "Rating": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - // Only fill this if it doesn't already have a value, since we get it from imdb which has better data - if (!item.CommunityRating.HasValue || string.IsNullOrWhiteSpace(item.GetProviderId(MetadataProviders.Imdb))) - { - float rval; - - // float.TryParse is local aware, so it can be probamatic, force us culture - if (float.TryParse(val, NumberStyles.AllowDecimalPoint, UsCulture, out rval)) - { - item.CommunityRating = rval; - } - } - } - break; - } - case "RatingCount": - { - 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)) - { - item.VoteCount = rval; - } - } - - break; - } - - case "IMDB_ID": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.SetProviderId(MetadataProviders.Imdb, val); - } - - break; - } - - case "zap2it_id": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - item.SetProviderId(MetadataProviders.Zap2It, val); - } - - break; - } - - case "Status": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - SeriesStatus seriesStatus; - - if (Enum.TryParse(val, true, out seriesStatus)) - item.Status = seriesStatus; - } - - break; - } - - case "FirstAired": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - DateTime date; - if (DateTime.TryParse(val, out date)) - { - date = date.ToUniversalTime(); - - item.PremiereDate = date; - item.ProductionYear = date.Year; - } - } - - break; - } - - case "Runtime": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val) && !item.LockedFields.Contains(MetadataFields.Runtime)) - { - int rval; - - // int.TryParse is local aware, so it can be probamatic, force us culture - if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) - { - item.RunTimeTicks = TimeSpan.FromMinutes(rval).Ticks; - } - } - - break; - } - - case "Genre": - { - var val = reader.ReadElementContentAsString(); - - 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(ConfigurationManager.Configuration.PreferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase))) - { - var vals = val - .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) - .Select(i => i.Trim()) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .ToList(); - - if (vals.Count > 0) - { - item.Genres.Clear(); - - foreach (var genre in vals) - { - item.AddGenre(genre); - } - } - } - } - - break; - } - - case "Network": - { - var val = reader.ReadElementContentAsString(); - - 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) - { - item.Studios.Clear(); - - foreach (var genre in vals) - { - item.AddStudio(genre); - } - } - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - - private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken) - { - DateTime? airDate = null; - int? seasonNumber = null; - - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - 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; - } - } - } - - 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) - { - var settings = new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }; - - using (var streamReader = new StreamReader(actorsXmlPath, 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 "Actor": - { - using (var subtree = reader.ReadSubtree()) - { - FetchDataFromActorNode(series, subtree); - } - break; - } - default: - reader.Skip(); - break; - } - } - } - } - } - } - - /// - /// 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()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Name": - { - personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "Role": - { - personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - default: - reader.Skip(); - break; - } - } - } - - personInfo.Type = PersonType.Actor; - - if (!string.IsNullOrEmpty(personInfo.Name)) - { - series.AddPerson(personInfo); - } - } - - /// - /// The us culture - /// - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - /// - /// Determines whether [has local meta] [the specified item]. - /// - /// The item. - /// true if [has local meta] [the specified item]; otherwise, false. - private bool HasLocalMeta(BaseItem item) - { - return item.ResolveArgs.ContainsMetaFileByName(LocalMetaFileName); - } - - /// - /// Finds the series. - /// - /// The name. - /// The cancellation token. - /// Task{System.String}. - private async Task FindSeries(string name, CancellationToken cancellationToken) - { - var url = string.Format(RootUrl + SeriesQuery, WebUtility.UrlEncode(name)); - var doc = new XmlDocument(); - - using (var results = await HttpClient.Get(new HttpRequestOptions - { - Url = url, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken - - }).ConfigureAwait(false)) - { - doc.Load(results); - } - - if (doc.HasChildNodes) - { - 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)); - } - - 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); - } - } - } - } - - // Try stripping off the year if it was supplied - var parenthIndex = name.LastIndexOf('('); - - if (parenthIndex != -1) - { - 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; - } - - /// - /// 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. - /// - /// The name. - /// System.String. - internal static string GetComparableName(string name) - { - name = name.ToLower(); - name = name.Normalize(NormalizationForm.FormKD); - var sb = new StringBuilder(); - foreach (var c in name) - { - 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) - { - sb.Append(" "); - } - else if (c == '&') - { - sb.Append(" and "); - } - else - { - sb.Append(c); - } - } - 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); - } - } -} diff --git a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs index 22ff91761..7ad564d0f 100644 --- a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs +++ b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs @@ -111,7 +111,7 @@ namespace MediaBrowser.Providers.TV return; } - var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, tvdbId); + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, tvdbId); var episodeFiles = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly) .Select(Path.GetFileNameWithoutExtension) diff --git a/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs new file mode 100644 index 000000000..c47640ad9 --- /dev/null +++ b/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs @@ -0,0 +1,720 @@ +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.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +namespace MediaBrowser.Providers.TV +{ + + /// + /// Class RemoteEpisodeProvider + /// + class TvdbEpisodeProvider : BaseMetadataProvider + { + /// + /// The _provider manager + /// + private readonly IProviderManager _providerManager; + + /// + /// Gets the HTTP client. + /// + /// The HTTP client. + protected IHttpClient HttpClient { get; private set; } + private readonly IFileSystem _fileSystem; + + internal static TvdbEpisodeProvider Current; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + /// The log manager. + /// The configuration manager. + /// The provider manager. + public TvdbEpisodeProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem) + : base(logManager, configurationManager) + { + HttpClient = httpClient; + _providerManager = providerManager; + _fileSystem = fileSystem; + Current = this; + } + + /// + /// Supportses the specified item. + /// + /// The item. + /// true if XXXX, false otherwise + public override bool Supports(BaseItem item) + { + return item is Episode; + } + + public override ItemUpdateType ItemUpdateType + { + get + { + return ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataDownload; + } + } + + /// + /// Gets the priority. + /// + /// The priority. + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.Third; } + } + + /// + /// 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 "1"; + } + } + + /// + /// Needses the refresh internal. + /// + /// The item. + /// The provider info. + /// true if XXXX, false otherwise + protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) + { + var locationType = item.LocationType; + + // Always use tvdb updates for non-file system episodes + if (locationType != LocationType.Remote && locationType != LocationType.Virtual) + { + // Don't proceed if there's local metadata + if (!ConfigurationManager.Configuration.EnableTvDbUpdates && HasLocalMeta(item)) + { + return false; + } + } + + return base.NeedsRefreshInternal(item, providerInfo); + } + + protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) + { + var episode = (Episode)item; + + var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId)) + { + // Process images + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); + + var files = GetEpisodeXmlFiles(episode, seriesDataPath); + + if (files.Count > 0) + { + return files.Select(i => _fileSystem.GetLastWriteTimeUtc(i)).Max() > providerInfo.LastRefreshed; + } + } + + return false; + } + + /// + /// Gets the episode XML files. + /// + /// The episode. + /// The series data path. + /// List{FileInfo}. + internal List GetEpisodeXmlFiles(Episode episode, string seriesDataPath) + { + var files = new List(); + + if (episode.IndexNumber == null) + { + return files; + } + + var episodeNumber = episode.IndexNumber.Value; + var seasonNumber = episode.ParentIndexNumber; + + if (seasonNumber == null) + { + return files; + } + + var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); + + var fileInfo = new FileInfo(file); + var usingAbsoluteData = false; + + if (fileInfo.Exists) + { + files.Add(fileInfo); + } + else + { + file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); + fileInfo = new FileInfo(file); + if (fileInfo.Exists) + { + files.Add(fileInfo); + usingAbsoluteData = true; + } + } + + var end = episode.IndexNumberEnd ?? episodeNumber; + episodeNumber++; + + while (episodeNumber <= end) + { + if (usingAbsoluteData) + { + file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); + } + else + { + file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); + } + + fileInfo = new FileInfo(file); + if (fileInfo.Exists) + { + files.Add(fileInfo); + } + else + { + break; + } + + episodeNumber++; + } + + return files; + } + + /// + /// 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, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var status = ProviderRefreshStatus.Success; + + var episode = (Episode)item; + + var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId)) + { + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); + + try + { + status = await FetchEpisodeData(episode, seriesDataPath, cancellationToken).ConfigureAwait(false); + } + catch (FileNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + } + + SetLastRefreshed(item, DateTime.UtcNow, status); + return true; + } + + + /// + /// Fetches the episode data. + /// + /// The episode. + /// The series data path. + /// The cancellation token. + /// Task{System.Boolean}. + private async Task FetchEpisodeData(Episode episode, string seriesDataPath, CancellationToken cancellationToken) + { + var status = ProviderRefreshStatus.Success; + + if (episode.IndexNumber == null) + { + return status; + } + + var episodeNumber = episode.IndexNumber.Value; + var seasonNumber = episode.ParentIndexNumber; + + if (seasonNumber == null) + { + return status; + } + + var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); + var success = false; + var usingAbsoluteData = false; + + try + { + status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); + + success = true; + } + catch (FileNotFoundException) + { + // Could be using absolute numbering + if (seasonNumber.Value != 1) + { + throw; + } + } + + if (!success) + { + file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); + + status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); + usingAbsoluteData = true; + } + + var end = episode.IndexNumberEnd ?? episodeNumber; + episodeNumber++; + + while (episodeNumber <= end) + { + if (usingAbsoluteData) + { + file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); + } + else + { + file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); + } + + try + { + FetchAdditionalPartInfo(episode, file, cancellationToken); + } + catch (FileNotFoundException) + { + break; + } + + episodeNumber++; + } + + return status; + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private async Task FetchMainEpisodeInfo(Episode item, string xmlFile, CancellationToken cancellationToken) + { + var status = ProviderRefreshStatus.Success; + + using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + item.People.Clear(); + } + + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + })) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "id": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.Tvdb, val); + } + break; + } + + case "IMDB_ID": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.Imdb, val); + } + break; + } + + case "EpisodeName": + { + if (!item.LockedFields.Contains(MetadataFields.Name)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Name = val; + } + } + break; + } + + case "Language": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Language = val; + } + break; + } + + case "filename": + { + if (string.IsNullOrEmpty(item.PrimaryImagePath)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + try + { + var url = TVUtils.BannerUrl + val; + + await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); + } + catch (HttpException) + { + status = ProviderRefreshStatus.CompletedWithErrors; + } + } + } + break; + } + + case "Overview": + { + if (!item.LockedFields.Contains(MetadataFields.Overview)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Overview = val; + } + } + break; + } + case "Rating": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + float rval; + + // float.TryParse is local aware, so it can be probamatic, force us culture + if (float.TryParse(val, NumberStyles.AllowDecimalPoint, _usCulture, out rval)) + { + item.CommunityRating = rval; + } + } + break; + } + case "RatingCount": + { + 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)) + { + item.VoteCount = rval; + } + } + + break; + } + + case "FirstAired": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + DateTime date; + if (DateTime.TryParse(val, out date)) + { + date = date.ToUniversalTime(); + + item.PremiereDate = date; + item.ProductionYear = date.Year; + } + } + + break; + } + + case "Director": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(item, val, PersonType.Director); + } + } + + break; + } + case "GuestStars": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddGuestStars(item, val); + } + } + + break; + } + case "Writer": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(item, val, PersonType.Writer); + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + } + + return status; + } + + private void AddPeople(BaseItem item, string val, string personType) + { + // Sometimes tvdb actors have leading spaces + foreach (var person in val.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(str => new PersonInfo { Type = personType, Name = str.Trim() })) + { + item.AddPerson(person); + } + } + + private void AddGuestStars(BaseItem item, string val) + { + // Sometimes tvdb actors have leading spaces + //Regex Info: + //The first block are the posible delimitators (open-parentheses should be there cause if dont the next block will fail) + //The second block Allow the delimitators to be part of the text if they're inside parentheses + var persons = Regex.Matches(val, @"(?([^|,(])|(?\([^)]*\)*))+") + .Cast() + .Select(m => m.Value) + .Where(i => !string.IsNullOrWhiteSpace(i) && !string.IsNullOrEmpty(i)); + + foreach (var person in persons.Select(str => + { + var nameGroup = str.Split(new[] { '(' }, 2, StringSplitOptions.RemoveEmptyEntries); + var name = nameGroup[0].Trim(); + var roles = nameGroup.Count() > 1 ? nameGroup[1].Trim() : null; + if (roles != null) + roles = roles.EndsWith(")") ? roles.Substring(0, roles.Length - 1) : roles; + return new PersonInfo { Type = PersonType.GuestStar, Name = name, Role = roles }; + })) + { + item.AddPerson(person); + } + } + + private void FetchAdditionalPartInfo(Episode item, string xmlFile, CancellationToken cancellationToken) + { + using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8)) + { + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + })) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "EpisodeName": + { + if (!item.LockedFields.Contains(MetadataFields.Name)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Name += ", " + val; + } + } + break; + } + + case "Overview": + { + if (!item.LockedFields.Contains(MetadataFields.Overview)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Overview += Environment.NewLine + Environment.NewLine + val; + } + } + break; + } + case "Director": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(item, val, PersonType.Director); + } + } + + break; + } + case "GuestStars": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddGuestStars(item, val); + } + } + + break; + } + case "Writer": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(item, val, PersonType.Writer); + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + } + } + + /// + /// Determines whether [has local meta] [the specified episode]. + /// + /// The episode. + /// true if [has local meta] [the specified episode]; otherwise, false. + private bool HasLocalMeta(BaseItem episode) + { + return (episode.Parent.ResolveArgs.ContainsMetaFileByName(Path.GetFileNameWithoutExtension(episode.Path) + ".xml")); + } + } +} diff --git a/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs index 10ca47c41..5882d74cf 100644 --- a/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs @@ -107,7 +107,7 @@ namespace MediaBrowser.Providers.TV /// Task. private async Task DownloadImageFromSeries(BaseItem item, Series series, CancellationToken cancellationToken) { - var tvdbPath = RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, series.GetProviderId(MetadataProviders.Tvdb)); + var tvdbPath = TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, series.GetProviderId(MetadataProviders.Tvdb)); var actorXmlPath = Path.Combine(tvdbPath, "actors.xml"); @@ -117,7 +117,7 @@ namespace MediaBrowser.Providers.TV { url = TVUtils.BannerUrl + url; - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, + await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/TV/TvdbPrescanTask.cs b/MediaBrowser.Providers/TV/TvdbPrescanTask.cs index d77698725..df5c39b65 100644 --- a/MediaBrowser.Providers/TV/TvdbPrescanTask.cs +++ b/MediaBrowser.Providers/TV/TvdbPrescanTask.cs @@ -77,7 +77,7 @@ namespace MediaBrowser.Providers.TV return; } - var path = RemoteSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); + var path = TvdbSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); Directory.CreateDirectory(path); @@ -107,7 +107,7 @@ namespace MediaBrowser.Providers.TV Url = ServerTimeUrl, CancellationToken = cancellationToken, EnableHttpCompression = true, - ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool + ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool }).ConfigureAwait(false)) { @@ -195,7 +195,7 @@ namespace MediaBrowser.Providers.TV Url = string.Format(UpdatesUrl, lastUpdateTime), CancellationToken = cancellationToken, EnableHttpCompression = true, - ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool + ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool }).ConfigureAwait(false)) { @@ -315,7 +315,7 @@ namespace MediaBrowser.Providers.TV Directory.CreateDirectory(seriesDataPath); - return RemoteSeriesProvider.Current.DownloadSeriesZip(id, seriesDataPath, lastTvDbUpdateTime, cancellationToken); + return TvdbSeriesProvider.Current.DownloadSeriesZip(id, seriesDataPath, lastTvDbUpdateTime, cancellationToken); } } } diff --git a/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs b/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs new file mode 100644 index 000000000..77a432add --- /dev/null +++ b/MediaBrowser.Providers/TV/TvdbSeasonProvider.cs @@ -0,0 +1,204 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + /// + /// Class RemoteSeasonProvider + /// + class TvdbSeasonProvider : BaseMetadataProvider + { + /// + /// The _provider manager + /// + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The log manager. + /// The configuration manager. + /// The provider manager. + /// httpClient + public TvdbSeasonProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem) + : base(logManager, configurationManager) + { + _providerManager = providerManager; + _fileSystem = fileSystem; + } + + /// + /// Supportses the specified item. + /// + /// The item. + /// true if XXXX, false otherwise + public override bool Supports(BaseItem item) + { + return item is Season; + } + + /// + /// Gets the priority. + /// + /// The priority. + public override MetadataProviderPriority Priority + { + // Run after fanart + get { return MetadataProviderPriority.Fourth; } + } + + /// + /// Gets a value indicating whether [requires internet]. + /// + /// true if [requires internet]; otherwise, false. + public override bool RequiresInternet + { + get + { + return true; + } + } + + public override ItemUpdateType ItemUpdateType + { + get + { + return ItemUpdateType.ImageUpdate; + } + } + + /// + /// 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"; + } + } + + protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) + { + var season = (Season)item; + var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId)) + { + // Process images + var imagesXmlPath = Path.Combine(TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); + + var imagesFileInfo = new FileInfo(imagesXmlPath); + + if (imagesFileInfo.Exists) + { + return _fileSystem.GetLastWriteTimeUtc(imagesFileInfo) > providerInfo.LastRefreshed; + } + } + return false; + } + + protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) + { + if (item.HasImage(ImageType.Primary) && item.HasImage(ImageType.Banner) && item.BackdropImagePaths.Count > 0) + { + return false; + } + return base.NeedsRefreshInternal(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, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualTvdbSeasonImageProvider.ProviderName).ConfigureAwait(false); + + const int backdropLimit = 1; + + await DownloadImages(item, images.ToList(), backdropLimit, cancellationToken).ConfigureAwait(false); + + SetLastRefreshed(item, DateTime.UtcNow); + return true; + } + + private async Task DownloadImages(BaseItem item, List images, int backdropLimit, CancellationToken cancellationToken) + { + if (!item.HasImage(ImageType.Primary)) + { + var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); + + if (image != null) + { + await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) + .ConfigureAwait(false); + } + } + + if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !item.HasImage(ImageType.Banner)) + { + var image = images.FirstOrDefault(i => i.Type == ImageType.Banner); + + if (image != null) + { + await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) + .ConfigureAwait(false); + } + } + + if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && item.BackdropImagePaths.Count < backdropLimit) + { + var bdNo = item.BackdropImagePaths.Count; + + foreach (var backdrop in images.Where(i => i.Type == ImageType.Backdrop)) + { + var url = backdrop.Url; + + if (item.ContainsImageWithSourceUrl(url)) + { + continue; + } + + await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken).ConfigureAwait(false); + + bdNo++; + + if (item.BackdropImagePaths.Count >= backdropLimit) break; + } + } + } + } +} diff --git a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs index 5fc7b50f9..42118c063 100644 --- a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs @@ -3,20 +3,17 @@ 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; 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 { @@ -125,7 +122,7 @@ namespace MediaBrowser.Providers.TV if (!string.IsNullOrEmpty(seriesId)) { // Process images - var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); + var imagesXmlPath = Path.Combine(TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); var imagesFileInfo = new FileInfo(imagesXmlPath); @@ -158,64 +155,36 @@ namespace MediaBrowser.Providers.TV { cancellationToken.ThrowIfCancellationRequested(); - var series = (Series)item; - var seriesId = series.GetProviderId(MetadataProviders.Tvdb); + var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualTvdbSeriesImageProvider.ProviderName).ConfigureAwait(false); - if (!string.IsNullOrEmpty(seriesId)) - { - // Process images - var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); - - var imagesXmlPath = Path.Combine(seriesDataPath, "banners.xml"); - - var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops; - - if (!series.HasImage(ImageType.Primary) || !series.HasImage(ImageType.Banner) || series.BackdropImagePaths.Count < backdropLimit) - { - Directory.CreateDirectory(seriesDataPath); - - try - { - var fanartData = FetchFanartXmlData(imagesXmlPath, backdropLimit, cancellationToken); - await DownloadImages(item, fanartData, backdropLimit, cancellationToken).ConfigureAwait(false); - } - catch (FileNotFoundException) - { - // No biggie. Not all series have images - } - } + const int backdropLimit = 1; - SetLastRefreshed(item, DateTime.UtcNow); - return true; - } + await DownloadImages(item, images.ToList(), backdropLimit, cancellationToken).ConfigureAwait(false); - return false; + SetLastRefreshed(item, DateTime.UtcNow); + return true; } - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - private async Task DownloadImages(BaseItem item, FanartXmlData data, int backdropLimit, CancellationToken cancellationToken) + private async Task DownloadImages(BaseItem item, List images, int backdropLimit, CancellationToken cancellationToken) { if (!item.HasImage(ImageType.Primary)) { - var url = data.LanguagePoster ?? data.Poster; - if (!string.IsNullOrEmpty(url)) - { - url = TVUtils.BannerUrl + url; + var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) + if (image != null) + { + await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) .ConfigureAwait(false); } } if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !item.HasImage(ImageType.Banner)) { - var url = data.LanguageBanner ?? data.Banner; - if (!string.IsNullOrEmpty(url)) - { - url = TVUtils.BannerUrl + url; + var image = images.FirstOrDefault(i => i.Type == ImageType.Banner); - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) + if (image != null) + { + await _providerManager.SaveImage(item, image.Url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) .ConfigureAwait(false); } } @@ -224,38 +193,18 @@ namespace MediaBrowser.Providers.TV { var bdNo = item.BackdropImagePaths.Count; - var eligibleBackdrops = data.Backdrops - .Where(i => - { - if (string.IsNullOrEmpty(i.Resolution)) - { - return true; - } - - var parts = i.Resolution.Split('x'); - - int width; - - if (int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width)) - { - return width >= ConfigurationManager.Configuration.MinSeriesBackdropDownloadWidth; - } - - return true; - }) - .ToList(); - - foreach (var backdrop in eligibleBackdrops) + foreach (var backdrop in images.Where(i => i.Type == ImageType.Backdrop && + (!i.Width.HasValue || + i.Width.Value >= ConfigurationManager.Configuration.MinSeriesBackdropDownloadWidth))) { - var url = TVUtils.BannerUrl + backdrop.Url; + var url = backdrop.Url; if (item.ContainsImageWithSourceUrl(url)) { continue; } - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken) - .ConfigureAwait(false); + await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken).ConfigureAwait(false); bdNo++; @@ -263,158 +212,5 @@ namespace MediaBrowser.Providers.TV } } } - - private FanartXmlData FetchFanartXmlData(string bannersXmlPath, int backdropLimit, CancellationToken cancellationToken) - { - var settings = new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }; - - var data = new FanartXmlData(); - - using (var streamReader = new StreamReader(bannersXmlPath, 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()) - { - FetchInfoFromBannerNode(data, subtree, backdropLimit); - } - break; - } - default: - reader.Skip(); - break; - } - } - } - } - } - - return data; - } - - private void FetchInfoFromBannerNode(FanartXmlData data, XmlReader reader, int backdropLimit) - { - reader.MoveToContent(); - - string type = null; - string url = null; - string resolution = null; - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "BannerType": - { - type = reader.ReadElementContentAsString() ?? string.Empty; - - if (string.Equals(type, "poster", StringComparison.OrdinalIgnoreCase)) - { - // Already got it - if (!string.IsNullOrEmpty(data.Poster)) - { - return; - } - } - else if (string.Equals(type, "series", StringComparison.OrdinalIgnoreCase)) - { - // Already got it - if (!string.IsNullOrEmpty(data.Banner)) - { - return; - } - } - else - { - return; - } - - break; - } - - case "BannerPath": - { - url = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType2": - { - resolution = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - default: - reader.Skip(); - break; - } - } - } - - if (!string.IsNullOrEmpty(url)) - { - if (string.Equals(type, "poster", StringComparison.OrdinalIgnoreCase)) - { - // Just grab the first - if (string.IsNullOrWhiteSpace(data.Poster)) - { - data.Poster = url; - } - } - else if (string.Equals(type, "series", StringComparison.OrdinalIgnoreCase)) - { - // Just grab the first - if (string.IsNullOrWhiteSpace(data.Banner)) - { - data.Banner = url; - } - } - else if (string.Equals(type, "fanart", StringComparison.OrdinalIgnoreCase)) - { - data.Backdrops.Add(new ImageInfo - { - Url = url, - Resolution = resolution - }); - } - } - } - } - - internal class FanartXmlData - { - public string LanguagePoster { get; set; } - public string LanguageBanner { get; set; } - public string Poster { get; set; } - public string Banner { get; set; } - public List Backdrops = new List(); - } - - internal class ImageInfo - { - public string Url { get; set; } - public string Resolution { get; set; } } } diff --git a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs new file mode 100644 index 000000000..a22f4f1c3 --- /dev/null +++ b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs @@ -0,0 +1,1228 @@ +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.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +namespace MediaBrowser.Providers.TV +{ + /// + /// Class RemoteSeriesProvider + /// + class TvdbSeriesProvider : BaseMetadataProvider, IDisposable + { + /// + /// 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 IFileSystem _fileSystem; + + /// + /// 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) + { + if (httpClient == null) + { + throw new ArgumentNullException("httpClient"); + } + HttpClient = httpClient; + _zipClient = zipClient; + _fileSystem = fileSystem; + 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, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var series = (Series)item; + + var seriesId = series.GetProviderId(MetadataProviders.Tvdb); + + if (string.IsNullOrEmpty(seriesId)) + { + seriesId = await FindSeries(series.Name, cancellationToken).ConfigureAwait(false); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (!string.IsNullOrEmpty(seriesId)) + { + var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); + + await FetchSeriesData(series, seriesId, seriesDataPath, force, cancellationToken).ConfigureAwait(false); + } + + SetLastRefreshed(item, DateTime.UtcNow); + return true; + } + + /// + /// Fetches the series data. + /// + /// 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) + { + Directory.CreateDirectory(seriesDataPath); + + var files = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly) + .Select(Path.GetFileName) + .ToList(); + + var seriesXmlFilename = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"; + + // Only download if not already there + // The prescan task will take care of updates so we don't need to re-download here + if (!files.Contains("banners.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains("actors.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains(seriesXmlFilename, StringComparer.OrdinalIgnoreCase)) + { + await DownloadSeriesZip(seriesId, seriesDataPath, null, 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"); + + FetchSeriesInfo(series, seriesXmlPath, cancellationToken); + + if (!series.LockedFields.Contains(MetadataFields.Cast)) + { + series.People.Clear(); + + FetchActors(series, actorsXmlPath, cancellationToken); + } + } + } + } + + /// + /// Downloads the series zip. + /// + /// The series id. + /// The series data path. + /// The cancellation token. + /// Task. + internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, long? lastTvDbUpdateTime, CancellationToken cancellationToken) + { + var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage); + + using (var zipStream = await HttpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvDbResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + // Delete existing files + DeleteXmlFiles(seriesDataPath); + + // Copy to memory stream because we need a seekable stream + using (var ms = new MemoryStream()) + { + await zipStream.CopyToAsync(ms).ConfigureAwait(false); + + ms.Position = 0; + _zipClient.ExtractAll(ms, seriesDataPath, true); + } + } + + // Sanitize all files, except for extracted episode files + foreach (var file in Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.AllDirectories).ToList() + .Where(i => !Path.GetFileName(i).StartsWith("episode-", StringComparison.OrdinalIgnoreCase))) + { + await SanitizeXmlFile(file).ConfigureAwait(false); + } + + await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, ConfigurationManager.Configuration.PreferredMetadataLanguage + ".xml"), lastTvDbUpdateTime).ConfigureAwait(false); + } + + private void DeleteXmlFiles(string path) + { + try + { + foreach (var file in new DirectoryInfo(path) + .EnumerateFiles("*.xml", SearchOption.AllDirectories) + .ToList()) + { + file.Delete(); + } + } + catch (DirectoryNotFoundException) + { + // No biggie + } + } + + /// + /// 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)) + { + using (var reader = new StreamReader(fileStream)) + { + var xml = await reader.ReadToEndAsync().ConfigureAwait(false); + + validXml = StripInvalidXmlCharacters(xml); + } + } + + using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, true)) + { + using (var writer = new StreamWriter(fileStream)) + { + await writer.WriteAsync(validXml).ConfigureAwait(false); + } + } + } + + /// + /// Strips the invalid XML characters. + /// + /// The in string. + /// System.String. + public static string StripInvalidXmlCharacters(string inString) + { + if (inString == null) return null; + + var sbOutput = new StringBuilder(); + char ch; + + for (int i = 0; i < inString.Length; i++) + { + ch = inString[i]; + if ((ch >= 0x0020 && ch <= 0xD7FF) || + (ch >= 0xE000 && ch <= 0xFFFD) || + ch == 0x0009 || + ch == 0x000A || + ch == 0x000D) + { + sbOutput.Append(ch); + } + } + 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 + }; + + using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8)) + { + // 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; + } + + default: + reader.Skip(); + break; + } + } + } + } + } + } + + private async Task SaveEpsiodeXml(string seriesDataPath, string xml, long? lastTvDbUpdateTime) + { + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + 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)) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "lastupdated": + { + lastUpdateString = reader.ReadElementContentAsString(); + break; + } + + case "EpisodeNumber": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + 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; + } + } + } + } + } + + var hasEpisodeChanged = true; + if (!string.IsNullOrEmpty(lastUpdateString) && lastTvDbUpdateTime.HasValue) + { + long num; + if (long.TryParse(lastUpdateString, NumberStyles.Any, UsCulture, out num)) + { + hasEpisodeChanged = num >= lastTvDbUpdateTime.Value; + } + } + + var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber)); + + // 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); + } + } + + if (absoluteNumber != -1) + { + 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); + } + } + } + } + + /// + /// 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, "tvdb-v3"); + + return dataPath; + } + + private void FetchSeriesInfo(Series item, string seriesXmlPath, CancellationToken cancellationToken) + { + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + var episiodeAirDates = new List(); + + using (var streamReader = new StreamReader(seriesXmlPath, 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 "Series": + { + using (var subtree = reader.ReadSubtree()) + { + FetchDataFromSeriesNode(item, subtree, cancellationToken); + } + break; + } + + case "Episode": + { + using (var subtree = reader.ReadSubtree()) + { + var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken); + + if (date.HasValue) + { + episiodeAirDates.Add(date.Value); + } + } + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + } + + 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) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "SeriesName": + { + if (!item.LockedFields.Contains(MetadataFields.Name)) + { + item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + } + break; + } + + case "Overview": + { + if (!item.LockedFields.Contains(MetadataFields.Overview)) + { + item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + } + break; + } + + case "Airs_DayOfWeek": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.AirDays = TVUtils.GetAirDays(val); + } + break; + } + + case "Airs_Time": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.AirTime = val; + } + break; + } + + case "ContentRating": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.OfficialRating)) + { + item.OfficialRating = val; + } + } + break; + } + + case "Rating": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + // Only fill this if it doesn't already have a value, since we get it from imdb which has better data + if (!item.CommunityRating.HasValue || string.IsNullOrWhiteSpace(item.GetProviderId(MetadataProviders.Imdb))) + { + float rval; + + // float.TryParse is local aware, so it can be probamatic, force us culture + if (float.TryParse(val, NumberStyles.AllowDecimalPoint, UsCulture, out rval)) + { + item.CommunityRating = rval; + } + } + } + break; + } + case "RatingCount": + { + 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)) + { + item.VoteCount = rval; + } + } + + break; + } + + case "IMDB_ID": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.Imdb, val); + } + + break; + } + + case "zap2it_id": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.Zap2It, val); + } + + break; + } + + case "Status": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + SeriesStatus seriesStatus; + + if (Enum.TryParse(val, true, out seriesStatus)) + item.Status = seriesStatus; + } + + break; + } + + case "FirstAired": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + DateTime date; + if (DateTime.TryParse(val, out date)) + { + date = date.ToUniversalTime(); + + item.PremiereDate = date; + item.ProductionYear = date.Year; + } + } + + break; + } + + case "Runtime": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val) && !item.LockedFields.Contains(MetadataFields.Runtime)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.RunTimeTicks = TimeSpan.FromMinutes(rval).Ticks; + } + } + + break; + } + + case "Genre": + { + var val = reader.ReadElementContentAsString(); + + 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(ConfigurationManager.Configuration.PreferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase))) + { + var vals = val + .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(i => i.Trim()) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .ToList(); + + if (vals.Count > 0) + { + item.Genres.Clear(); + + foreach (var genre in vals) + { + item.AddGenre(genre); + } + } + } + } + + break; + } + + case "Network": + { + var val = reader.ReadElementContentAsString(); + + 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) + { + item.Studios.Clear(); + + foreach (var genre in vals) + { + item.AddStudio(genre); + } + } + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + + private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken) + { + DateTime? airDate = null; + int? seasonNumber = null; + + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + 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; + } + } + } + + 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) + { + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + using (var streamReader = new StreamReader(actorsXmlPath, 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 "Actor": + { + using (var subtree = reader.ReadSubtree()) + { + FetchDataFromActorNode(series, subtree); + } + break; + } + default: + reader.Skip(); + break; + } + } + } + } + } + } + + /// + /// 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()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Name": + { + personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + + case "Role": + { + personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + + default: + reader.Skip(); + break; + } + } + } + + personInfo.Type = PersonType.Actor; + + if (!string.IsNullOrEmpty(personInfo.Name)) + { + series.AddPerson(personInfo); + } + } + + /// + /// The us culture + /// + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// + /// Determines whether [has local meta] [the specified item]. + /// + /// The item. + /// true if [has local meta] [the specified item]; otherwise, false. + private bool HasLocalMeta(BaseItem item) + { + return item.ResolveArgs.ContainsMetaFileByName(LocalMetaFileName); + } + + /// + /// Finds the series. + /// + /// The name. + /// The cancellation token. + /// Task{System.String}. + private async Task FindSeries(string name, CancellationToken cancellationToken) + { + var url = string.Format(RootUrl + SeriesQuery, WebUtility.UrlEncode(name)); + var doc = new XmlDocument(); + + using (var results = await HttpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvDbResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + doc.Load(results); + } + + if (doc.HasChildNodes) + { + 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)); + } + + 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); + } + } + } + } + + // Try stripping off the year if it was supplied + var parenthIndex = name.LastIndexOf('('); + + if (parenthIndex != -1) + { + 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; + } + + /// + /// 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. + /// + /// The name. + /// System.String. + internal static string GetComparableName(string name) + { + name = name.ToLower(); + name = name.Normalize(NormalizationForm.FormKD); + var sb = new StringBuilder(); + foreach (var c in name) + { + 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) + { + sb.Append(" "); + } + else if (c == '&') + { + sb.Append(" and "); + } + else + { + sb.Append(c); + } + } + 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); + } + } +} -- cgit v1.2.3