From 362839f2536501dfebae293dc10afcb93518019d Mon Sep 17 00:00:00 2001 From: softworkz Date: Fri, 11 Mar 2016 02:51:10 +0100 Subject: Re-Organize TV provider source files --- .../MediaBrowser.Providers.csproj | 34 +- .../TV/FanArt/FanArtSeasonProvider.cs | 260 ++++ .../TV/FanArt/FanArtTvUpdatesPostScanTask.cs | 193 +++ .../TV/FanArt/FanartSeriesProvider.cs | 410 ++++++ MediaBrowser.Providers/TV/FanArtSeasonProvider.cs | 260 ---- .../TV/FanArtTvUpdatesPostScanTask.cs | 193 --- MediaBrowser.Providers/TV/FanartSeriesProvider.cs | 410 ------ .../TV/MovieDbEpisodeImageProvider.cs | 138 -- .../TV/MovieDbEpisodeProvider.cs | 152 -- MediaBrowser.Providers/TV/MovieDbProviderBase.cs | 234 ---- MediaBrowser.Providers/TV/MovieDbSeasonProvider.cs | 308 ---- .../TV/MovieDbSeriesImageProvider.cs | 204 --- MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs | 615 -------- .../TV/Omdb/OmdbEpisodeProvider.cs | 89 ++ MediaBrowser.Providers/TV/OmdbEpisodeProvider.cs | 89 -- .../TV/TheMovieDb/MovieDbEpisodeImageProvider.cs | 138 ++ .../TV/TheMovieDb/MovieDbEpisodeProvider.cs | 152 ++ .../TV/TheMovieDb/MovieDbProviderBase.cs | 234 ++++ .../TV/TheMovieDb/MovieDbSeasonProvider.cs | 308 ++++ .../TV/TheMovieDb/MovieDbSeriesImageProvider.cs | 204 +++ .../TV/TheMovieDb/MovieDbSeriesProvider.cs | 615 ++++++++ .../TV/TheTVDB/TvdbEpisodeImageProvider.cs | 209 +++ .../TV/TheTVDB/TvdbEpisodeProvider.cs | 978 +++++++++++++ .../TV/TheTVDB/TvdbPrescanTask.cs | 366 +++++ .../TV/TheTVDB/TvdbSeasonIdentityProvider.cs | 65 + .../TV/TheTVDB/TvdbSeasonImageProvider.cs | 391 ++++++ .../TV/TheTVDB/TvdbSeriesImageProvider.cs | 356 +++++ .../TV/TheTVDB/TvdbSeriesProvider.cs | 1469 ++++++++++++++++++++ .../TV/TvdbEpisodeImageProvider.cs | 209 --- MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs | 978 ------------- MediaBrowser.Providers/TV/TvdbPrescanTask.cs | 366 ----- .../TV/TvdbSeasonIdentityProvider.cs | 65 - .../TV/TvdbSeasonImageProvider.cs | 391 ------ .../TV/TvdbSeriesImageProvider.cs | 356 ----- MediaBrowser.Providers/TV/TvdbSeriesProvider.cs | 1469 -------------------- 35 files changed, 6454 insertions(+), 6454 deletions(-) create mode 100644 MediaBrowser.Providers/TV/FanArt/FanArtSeasonProvider.cs create mode 100644 MediaBrowser.Providers/TV/FanArt/FanArtTvUpdatesPostScanTask.cs create mode 100644 MediaBrowser.Providers/TV/FanArt/FanartSeriesProvider.cs delete mode 100644 MediaBrowser.Providers/TV/FanArtSeasonProvider.cs delete mode 100644 MediaBrowser.Providers/TV/FanArtTvUpdatesPostScanTask.cs delete mode 100644 MediaBrowser.Providers/TV/FanartSeriesProvider.cs delete mode 100644 MediaBrowser.Providers/TV/MovieDbEpisodeImageProvider.cs delete mode 100644 MediaBrowser.Providers/TV/MovieDbEpisodeProvider.cs delete mode 100644 MediaBrowser.Providers/TV/MovieDbProviderBase.cs delete mode 100644 MediaBrowser.Providers/TV/MovieDbSeasonProvider.cs delete mode 100644 MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs delete mode 100644 MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs create mode 100644 MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs delete mode 100644 MediaBrowser.Providers/TV/OmdbEpisodeProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeImageProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheMovieDb/MovieDbProviderBase.cs create mode 100644 MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeasonProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesImageProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs create mode 100644 MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonIdentityProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs create mode 100644 MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs delete mode 100644 MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs delete mode 100644 MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs delete mode 100644 MediaBrowser.Providers/TV/TvdbPrescanTask.cs delete mode 100644 MediaBrowser.Providers/TV/TvdbSeasonIdentityProvider.cs delete mode 100644 MediaBrowser.Providers/TV/TvdbSeasonImageProvider.cs delete mode 100644 MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs delete mode 100644 MediaBrowser.Providers/TV/TvdbSeriesProvider.cs diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index cf2746d2b8..eca254848d 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -169,28 +169,28 @@ - - - + + + - - - - - - - + + + + + + + - + - - - + + + - - + + - + diff --git a/MediaBrowser.Providers/TV/FanArt/FanArtSeasonProvider.cs b/MediaBrowser.Providers/TV/FanArt/FanArtSeasonProvider.cs new file mode 100644 index 0000000000..35129987d3 --- /dev/null +++ b/MediaBrowser.Providers/TV/FanArt/FanArtSeasonProvider.cs @@ -0,0 +1,260 @@ +using System.Net; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Music; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + public class FanArtSeasonProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _json; + + public FanArtSeasonProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + _json = json; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "FanArt"; } + } + + public bool Supports(IHasImages item) + { + return item is Season; + } + + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Backdrop, + ImageType.Thumb, + ImageType.Banner, + ImageType.Primary + }; + } + + public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) + { + var list = new List(); + + var season = (Season)item; + var series = season.Series; + + if (series != null) + { + var id = series.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(id) && season.IndexNumber.HasValue) + { + // Bad id entered + try + { + await FanartSeriesProvider.Current.EnsureSeriesJson(id, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + var path = FanartSeriesProvider.Current.GetFanartJsonPath(id); + + try + { + int seasonNumber = AdjustForSeriesOffset(series, season.IndexNumber.Value); + AddImages(list, seasonNumber, path, cancellationToken); + } + catch (FileNotFoundException) + { + // No biggie. Don't blow up + } + catch (DirectoryNotFoundException) + { + // No biggie. Don't blow up + } + } + } + + var language = item.GetPreferredMetadataLanguage(); + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + // Sort first by width to prioritize HD versions + return list.OrderByDescending(i => i.Width ?? 0) + .ThenByDescending(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) + .ThenByDescending(i => i.VoteCount ?? 0); + } + + private int AdjustForSeriesOffset(Series series, int seasonNumber) + { + var offset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds); + if (offset != null) + return (int)(seasonNumber + offset); + + return seasonNumber; + } + + private void AddImages(List list, int seasonNumber, string path, CancellationToken cancellationToken) + { + var root = _json.DeserializeFromFile(path); + + AddImages(list, root, seasonNumber, cancellationToken); + } + + private void AddImages(List list, FanartSeriesProvider.RootObject obj, int seasonNumber, CancellationToken cancellationToken) + { + PopulateImages(list, obj.seasonposter, ImageType.Primary, 1000, 1426, seasonNumber); + PopulateImages(list, obj.seasonbanner, ImageType.Banner, 1000, 185, seasonNumber); + PopulateImages(list, obj.seasonthumb, ImageType.Thumb, 500, 281, seasonNumber); + PopulateImages(list, obj.showbackground, ImageType.Backdrop, 1920, 1080, seasonNumber); + } + + private void PopulateImages(List list, + List images, + ImageType type, + int width, + int height, + int seasonNumber) + { + if (images == null) + { + return; + } + + list.AddRange(images.Select(i => + { + var url = i.url; + var season = i.season; + + int imageSeasonNumber; + + if (!string.IsNullOrEmpty(url) && + !string.IsNullOrEmpty(season) && + int.TryParse(season, NumberStyles.Any, _usCulture, out imageSeasonNumber) && + seasonNumber == imageSeasonNumber) + { + var likesString = i.likes; + int likes; + + var info = new RemoteImageInfo + { + RatingType = RatingType.Likes, + Type = type, + Width = width, + Height = height, + ProviderName = Name, + Url = url, + Language = i.lang + }; + + if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Any, _usCulture, out likes)) + { + info.CommunityRating = likes; + } + + return info; + } + + return null; + }).Where(i => i != null)); + } + + public int Order + { + get { return 1; } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = FanartArtistProvider.Current.FanArtResourcePool + }); + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) + { + var options = FanartSeriesProvider.Current.GetFanartOptions(); + if (!options.EnableAutomaticUpdates) + { + return false; + } + + var season = (Season)item; + var series = season.Series; + + if (series == null) + { + return false; + } + + var tvdbId = series.GetProviderId(MetadataProviders.Tvdb); + + if (!String.IsNullOrEmpty(tvdbId)) + { + // Process images + var imagesFilePath = FanartSeriesProvider.Current.GetFanartJsonPath(tvdbId); + + var fileInfo = _fileSystem.GetFileInfo(imagesFilePath); + + return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; + } + + return false; + } + } +} diff --git a/MediaBrowser.Providers/TV/FanArt/FanArtTvUpdatesPostScanTask.cs b/MediaBrowser.Providers/TV/FanArt/FanArtTvUpdatesPostScanTask.cs new file mode 100644 index 0000000000..71f02e028c --- /dev/null +++ b/MediaBrowser.Providers/TV/FanArt/FanArtTvUpdatesPostScanTask.cs @@ -0,0 +1,193 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Music; +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 CommonIO; + +namespace MediaBrowser.Providers.TV +{ + class FanArtTvUpdatesPostScanTask : ILibraryPostScanTask + { + private const string UpdatesUrl = "http://webservice.fanart.tv/v3/tv/latest?api_key={0}&date={1}"; + + /// + /// The _HTTP client + /// + private readonly IHttpClient _httpClient; + /// + /// The _logger + /// + private readonly ILogger _logger; + /// + /// The _config + /// + private readonly IServerConfigurationManager _config; + private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; + + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + public FanArtTvUpdatesPostScanTask(IJsonSerializer jsonSerializer, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IFileSystem fileSystem) + { + _jsonSerializer = jsonSerializer; + _config = config; + _logger = logger; + _httpClient = httpClient; + _fileSystem = fileSystem; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var options = FanartSeriesProvider.Current.GetFanartOptions(); + + if (!options.EnableAutomaticUpdates) + { + progress.Report(100); + return; + } + + var path = FanartSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); + + _fileSystem.CreateDirectory(path); + + var timestampFile = Path.Combine(path, "time.txt"); + + var timestampFileInfo = _fileSystem.GetFileInfo(timestampFile); + + // Don't check for updates every single time + if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 3) + { + return; + } + + // Find out the last time we queried for updates + var lastUpdateTime = timestampFileInfo.Exists ? _fileSystem.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; + + var existingDirectories = Directory.EnumerateDirectories(path).Select(Path.GetFileName).ToList(); + + // If this is our first time, don't do any updates and just record the timestamp + if (!string.IsNullOrEmpty(lastUpdateTime)) + { + var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, options, cancellationToken).ConfigureAwait(false); + + progress.Report(5); + + await UpdateSeries(seriesToUpdate, progress, cancellationToken).ConfigureAwait(false); + } + + var newUpdateTime = Convert.ToInt64(DateTimeToUnixTimestamp(DateTime.UtcNow)).ToString(UsCulture); + + _fileSystem.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8); + + progress.Report(100); + } + + /// + /// Gets the series ids to update. + /// + /// The existing series ids. + /// The last update time. + /// The cancellation token. + /// Task{IEnumerable{System.String}}. + private async Task> GetSeriesIdsToUpdate(IEnumerable existingSeriesIds, string lastUpdateTime, FanartOptions options, CancellationToken cancellationToken) + { + var url = string.Format(UpdatesUrl, FanartArtistProvider.ApiKey, lastUpdateTime); + + if (!string.IsNullOrWhiteSpace(options.UserApiKey)) + { + url += "&client_key=" + options.UserApiKey; + } + + // First get last time + using (var stream = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + EnableHttpCompression = true, + ResourcePool = FanartArtistProvider.Current.FanArtResourcePool + + }).ConfigureAwait(false)) + { + // If empty fanart will return a string of "null", rather than an empty list + using (var reader = new StreamReader(stream)) + { + var json = await reader.ReadToEndAsync().ConfigureAwait(false); + + if (string.Equals(json, "null", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(json)) + { + return new List(); + } + + var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + + var updates = _jsonSerializer.DeserializeFromString>(json); + + return updates.Select(i => i.id).Where(existingDictionary.ContainsKey); + } + } + } + + /// + /// Updates the series. + /// + /// The id list. + /// The progress. + /// The cancellation token. + /// Task. + private async Task UpdateSeries(IEnumerable idList, IProgress progress, CancellationToken cancellationToken) + { + var list = idList.ToList(); + var numComplete = 0; + + foreach (var id in list) + { + _logger.Info("Updating series " + id); + + await FanartSeriesProvider.Current.DownloadSeriesJson(id, cancellationToken).ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= list.Count; + percent *= 95; + + progress.Report(percent + 5); + } + } + + /// + /// Dates the time to unix timestamp. + /// + /// The date time. + /// System.Double. + private static double DateTimeToUnixTimestamp(DateTime dateTime) + { + return (dateTime - new DateTime(1970, 1, 1).ToUniversalTime()).TotalSeconds; + } + + public class FanArtUpdate + { + public string id { get; set; } + public string name { get; set; } + public string new_images { get; set; } + public string total_images { get; set; } + } + } +} diff --git a/MediaBrowser.Providers/TV/FanArt/FanartSeriesProvider.cs b/MediaBrowser.Providers/TV/FanArt/FanartSeriesProvider.cs new file mode 100644 index 0000000000..5600c165a1 --- /dev/null +++ b/MediaBrowser.Providers/TV/FanArt/FanartSeriesProvider.cs @@ -0,0 +1,410 @@ +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.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Music; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + public class FanartSeriesProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IJsonSerializer _json; + + private const string FanArtBaseUrl = "http://webservice.fanart.tv/v3/tv/{1}?api_key={0}"; + // &client_key=52c813aa7b8c8b3bb87f4797532a2f8c + + internal static FanartSeriesProvider Current { get; private set; } + + public FanartSeriesProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + _json = json; + + Current = this; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "FanArt"; } + } + + public bool Supports(IHasImages item) + { + return item is Series; + } + + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary, + ImageType.Thumb, + ImageType.Art, + ImageType.Logo, + ImageType.Backdrop, + ImageType.Banner + }; + } + + public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) + { + var list = new List(); + + var series = (Series)item; + + var id = series.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(id)) + { + // Bad id entered + try + { + await EnsureSeriesJson(id, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + var path = GetFanartJsonPath(id); + + try + { + AddImages(list, path, cancellationToken); + } + catch (FileNotFoundException) + { + // No biggie. Don't blow up + } + catch (DirectoryNotFoundException) + { + // No biggie. Don't blow up + } + } + + var language = item.GetPreferredMetadataLanguage(); + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + // Sort first by width to prioritize HD versions + return list.OrderByDescending(i => i.Width ?? 0) + .ThenByDescending(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) + .ThenByDescending(i => i.VoteCount ?? 0); + } + + private void AddImages(List list, string path, CancellationToken cancellationToken) + { + var root = _json.DeserializeFromFile(path); + + AddImages(list, root, cancellationToken); + } + + private void AddImages(List list, RootObject obj, CancellationToken cancellationToken) + { + PopulateImages(list, obj.hdtvlogo, ImageType.Logo, 800, 310); + PopulateImages(list, obj.hdclearart, ImageType.Art, 1000, 562); + PopulateImages(list, obj.clearlogo, ImageType.Logo, 400, 155); + PopulateImages(list, obj.clearart, ImageType.Art, 500, 281); + PopulateImages(list, obj.showbackground, ImageType.Backdrop, 1920, 1080, true); + PopulateImages(list, obj.seasonthumb, ImageType.Thumb, 500, 281); + PopulateImages(list, obj.tvthumb, ImageType.Thumb, 500, 281); + PopulateImages(list, obj.tvbanner, ImageType.Banner, 1000, 185); + PopulateImages(list, obj.tvposter, ImageType.Primary, 1000, 1426); + } + + private void PopulateImages(List list, + List images, + ImageType type, + int width, + int height, + bool allowSeasonAll = false) + { + if (images == null) + { + return; + } + + list.AddRange(images.Select(i => + { + var url = i.url; + var season = i.season; + + var isSeasonValid = string.IsNullOrEmpty(season) || + (allowSeasonAll && string.Equals(season, "all", StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(url) && isSeasonValid) + { + var likesString = i.likes; + int likes; + + var info = new RemoteImageInfo + { + RatingType = RatingType.Likes, + Type = type, + Width = width, + Height = height, + ProviderName = Name, + Url = url, + Language = i.lang + }; + + if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Any, _usCulture, out likes)) + { + info.CommunityRating = likes; + } + + return info; + } + + return null; + }).Where(i => i != null)); + } + + public int Order + { + get { return 1; } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = FanartArtistProvider.Current.FanArtResourcePool + }); + } + + /// + /// Gets the series data path. + /// + /// The app paths. + /// The series id. + /// System.String. + internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId) + { + var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); + + return seriesDataPath; + } + + /// + /// Gets the series data path. + /// + /// The app paths. + /// System.String. + internal static string GetSeriesDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.CachePath, "fanart-tv"); + + return dataPath; + } + + public string GetFanartJsonPath(string tvdbId) + { + var dataPath = GetSeriesDataPath(_config.ApplicationPaths, tvdbId); + return Path.Combine(dataPath, "fanart.json"); + } + + private readonly SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); + internal async Task EnsureSeriesJson(string tvdbId, CancellationToken cancellationToken) + { + var path = GetFanartJsonPath(tvdbId); + + // Only allow one thread in here at a time since every season will be calling this method, possibly concurrently + await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + if (fileInfo.Exists) + { + if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3) + { + return; + } + } + + await DownloadSeriesJson(tvdbId, cancellationToken).ConfigureAwait(false); + } + finally + { + _ensureSemaphore.Release(); + } + } + + public FanartOptions GetFanartOptions() + { + return _config.GetConfiguration("fanart"); + } + + /// + /// Downloads the series json. + /// + /// The TVDB identifier. + /// The cancellation token. + /// Task. + internal async Task DownloadSeriesJson(string tvdbId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var url = string.Format(FanArtBaseUrl, FanartArtistProvider.ApiKey, tvdbId); + + var clientKey = GetFanartOptions().UserApiKey; + if (!string.IsNullOrWhiteSpace(clientKey)) + { + url += "&client_key=" + clientKey; + } + + var path = GetFanartJsonPath(tvdbId); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + try + { + using (var response = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = FanartArtistProvider.Current.FanArtResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + using (var fileStream = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true)) + { + await response.CopyToAsync(fileStream).ConfigureAwait(false); + } + } + } + catch (HttpException exception) + { + if (exception.StatusCode.HasValue && exception.StatusCode.Value == HttpStatusCode.NotFound) + { + // If the user has automatic updates enabled, save a dummy object to prevent repeated download attempts + _json.SerializeToFile(new RootObject(), path); + + return; + } + + throw; + } + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) + { + var options = GetFanartOptions(); + if (!options.EnableAutomaticUpdates) + { + return false; + } + + var tvdbId = item.GetProviderId(MetadataProviders.Tvdb); + + if (!String.IsNullOrEmpty(tvdbId)) + { + // Process images + var imagesFilePath = GetFanartJsonPath(tvdbId); + + var fileInfo = _fileSystem.GetFileInfo(imagesFilePath); + + return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; + } + + return false; + } + + public class Image + { + public string id { get; set; } + public string url { get; set; } + public string lang { get; set; } + public string likes { get; set; } + public string season { get; set; } + } + + public class RootObject + { + public string name { get; set; } + public string thetvdb_id { get; set; } + public List clearlogo { get; set; } + public List hdtvlogo { get; set; } + public List clearart { get; set; } + public List showbackground { get; set; } + public List tvthumb { get; set; } + public List seasonposter { get; set; } + public List seasonthumb { get; set; } + public List hdclearart { get; set; } + public List tvbanner { get; set; } + public List characterart { get; set; } + public List tvposter { get; set; } + public List seasonbanner { get; set; } + } + } + + public class FanartConfigStore : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new List + { + new ConfigurationStore + { + Key = "fanart", + ConfigurationType = typeof(FanartOptions) + } + }; + } + } +} diff --git a/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs b/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs deleted file mode 100644 index 35129987d3..0000000000 --- a/MediaBrowser.Providers/TV/FanArtSeasonProvider.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System.Net; -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Music; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - public class FanArtSeasonProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _json; - - public FanArtSeasonProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - _json = json; - } - - public string Name - { - get { return ProviderName; } - } - - public static string ProviderName - { - get { return "FanArt"; } - } - - public bool Supports(IHasImages item) - { - return item is Season; - } - - public IEnumerable GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Backdrop, - ImageType.Thumb, - ImageType.Banner, - ImageType.Primary - }; - } - - public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) - { - var list = new List(); - - var season = (Season)item; - var series = season.Series; - - if (series != null) - { - var id = series.GetProviderId(MetadataProviders.Tvdb); - - if (!string.IsNullOrEmpty(id) && season.IndexNumber.HasValue) - { - // Bad id entered - try - { - await FanartSeriesProvider.Current.EnsureSeriesJson(id, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) - { - throw; - } - } - - var path = FanartSeriesProvider.Current.GetFanartJsonPath(id); - - try - { - int seasonNumber = AdjustForSeriesOffset(series, season.IndexNumber.Value); - AddImages(list, seasonNumber, path, cancellationToken); - } - catch (FileNotFoundException) - { - // No biggie. Don't blow up - } - catch (DirectoryNotFoundException) - { - // No biggie. Don't blow up - } - } - } - - var language = item.GetPreferredMetadataLanguage(); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - // Sort first by width to prioritize HD versions - return list.OrderByDescending(i => i.Width ?? 0) - .ThenByDescending(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) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - private int AdjustForSeriesOffset(Series series, int seasonNumber) - { - var offset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds); - if (offset != null) - return (int)(seasonNumber + offset); - - return seasonNumber; - } - - private void AddImages(List list, int seasonNumber, string path, CancellationToken cancellationToken) - { - var root = _json.DeserializeFromFile(path); - - AddImages(list, root, seasonNumber, cancellationToken); - } - - private void AddImages(List list, FanartSeriesProvider.RootObject obj, int seasonNumber, CancellationToken cancellationToken) - { - PopulateImages(list, obj.seasonposter, ImageType.Primary, 1000, 1426, seasonNumber); - PopulateImages(list, obj.seasonbanner, ImageType.Banner, 1000, 185, seasonNumber); - PopulateImages(list, obj.seasonthumb, ImageType.Thumb, 500, 281, seasonNumber); - PopulateImages(list, obj.showbackground, ImageType.Backdrop, 1920, 1080, seasonNumber); - } - - private void PopulateImages(List list, - List images, - ImageType type, - int width, - int height, - int seasonNumber) - { - if (images == null) - { - return; - } - - list.AddRange(images.Select(i => - { - var url = i.url; - var season = i.season; - - int imageSeasonNumber; - - if (!string.IsNullOrEmpty(url) && - !string.IsNullOrEmpty(season) && - int.TryParse(season, NumberStyles.Any, _usCulture, out imageSeasonNumber) && - seasonNumber == imageSeasonNumber) - { - var likesString = i.likes; - int likes; - - var info = new RemoteImageInfo - { - RatingType = RatingType.Likes, - Type = type, - Width = width, - Height = height, - ProviderName = Name, - Url = url, - Language = i.lang - }; - - if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Any, _usCulture, out likes)) - { - info.CommunityRating = likes; - } - - return info; - } - - return null; - }).Where(i => i != null)); - } - - public int Order - { - get { return 1; } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = FanartArtistProvider.Current.FanArtResourcePool - }); - } - - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) - { - var options = FanartSeriesProvider.Current.GetFanartOptions(); - if (!options.EnableAutomaticUpdates) - { - return false; - } - - var season = (Season)item; - var series = season.Series; - - if (series == null) - { - return false; - } - - var tvdbId = series.GetProviderId(MetadataProviders.Tvdb); - - if (!String.IsNullOrEmpty(tvdbId)) - { - // Process images - var imagesFilePath = FanartSeriesProvider.Current.GetFanartJsonPath(tvdbId); - - var fileInfo = _fileSystem.GetFileInfo(imagesFilePath); - - return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; - } - - return false; - } - } -} diff --git a/MediaBrowser.Providers/TV/FanArtTvUpdatesPostScanTask.cs b/MediaBrowser.Providers/TV/FanArtTvUpdatesPostScanTask.cs deleted file mode 100644 index 71f02e028c..0000000000 --- a/MediaBrowser.Providers/TV/FanArtTvUpdatesPostScanTask.cs +++ /dev/null @@ -1,193 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Music; -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 CommonIO; - -namespace MediaBrowser.Providers.TV -{ - class FanArtTvUpdatesPostScanTask : ILibraryPostScanTask - { - private const string UpdatesUrl = "http://webservice.fanart.tv/v3/tv/latest?api_key={0}&date={1}"; - - /// - /// The _HTTP client - /// - private readonly IHttpClient _httpClient; - /// - /// The _logger - /// - private readonly ILogger _logger; - /// - /// The _config - /// - private readonly IServerConfigurationManager _config; - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - public FanArtTvUpdatesPostScanTask(IJsonSerializer jsonSerializer, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IFileSystem fileSystem) - { - _jsonSerializer = jsonSerializer; - _config = config; - _logger = logger; - _httpClient = httpClient; - _fileSystem = fileSystem; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var options = FanartSeriesProvider.Current.GetFanartOptions(); - - if (!options.EnableAutomaticUpdates) - { - progress.Report(100); - return; - } - - var path = FanartSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); - - _fileSystem.CreateDirectory(path); - - var timestampFile = Path.Combine(path, "time.txt"); - - var timestampFileInfo = _fileSystem.GetFileInfo(timestampFile); - - // Don't check for updates every single time - if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 3) - { - return; - } - - // Find out the last time we queried for updates - var lastUpdateTime = timestampFileInfo.Exists ? _fileSystem.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; - - var existingDirectories = Directory.EnumerateDirectories(path).Select(Path.GetFileName).ToList(); - - // If this is our first time, don't do any updates and just record the timestamp - if (!string.IsNullOrEmpty(lastUpdateTime)) - { - var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, options, cancellationToken).ConfigureAwait(false); - - progress.Report(5); - - await UpdateSeries(seriesToUpdate, progress, cancellationToken).ConfigureAwait(false); - } - - var newUpdateTime = Convert.ToInt64(DateTimeToUnixTimestamp(DateTime.UtcNow)).ToString(UsCulture); - - _fileSystem.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8); - - progress.Report(100); - } - - /// - /// Gets the series ids to update. - /// - /// The existing series ids. - /// The last update time. - /// The cancellation token. - /// Task{IEnumerable{System.String}}. - private async Task> GetSeriesIdsToUpdate(IEnumerable existingSeriesIds, string lastUpdateTime, FanartOptions options, CancellationToken cancellationToken) - { - var url = string.Format(UpdatesUrl, FanartArtistProvider.ApiKey, lastUpdateTime); - - if (!string.IsNullOrWhiteSpace(options.UserApiKey)) - { - url += "&client_key=" + options.UserApiKey; - } - - // First get last time - using (var stream = await _httpClient.Get(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - EnableHttpCompression = true, - ResourcePool = FanartArtistProvider.Current.FanArtResourcePool - - }).ConfigureAwait(false)) - { - // If empty fanart will return a string of "null", rather than an empty list - using (var reader = new StreamReader(stream)) - { - var json = await reader.ReadToEndAsync().ConfigureAwait(false); - - if (string.Equals(json, "null", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(json)) - { - return new List(); - } - - var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - var updates = _jsonSerializer.DeserializeFromString>(json); - - return updates.Select(i => i.id).Where(existingDictionary.ContainsKey); - } - } - } - - /// - /// Updates the series. - /// - /// The id list. - /// The progress. - /// The cancellation token. - /// Task. - private async Task UpdateSeries(IEnumerable idList, IProgress progress, CancellationToken cancellationToken) - { - var list = idList.ToList(); - var numComplete = 0; - - foreach (var id in list) - { - _logger.Info("Updating series " + id); - - await FanartSeriesProvider.Current.DownloadSeriesJson(id, cancellationToken).ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= list.Count; - percent *= 95; - - progress.Report(percent + 5); - } - } - - /// - /// Dates the time to unix timestamp. - /// - /// The date time. - /// System.Double. - private static double DateTimeToUnixTimestamp(DateTime dateTime) - { - return (dateTime - new DateTime(1970, 1, 1).ToUniversalTime()).TotalSeconds; - } - - public class FanArtUpdate - { - public string id { get; set; } - public string name { get; set; } - public string new_images { get; set; } - public string total_images { get; set; } - } - } -} diff --git a/MediaBrowser.Providers/TV/FanartSeriesProvider.cs b/MediaBrowser.Providers/TV/FanartSeriesProvider.cs deleted file mode 100644 index 5600c165a1..0000000000 --- a/MediaBrowser.Providers/TV/FanartSeriesProvider.cs +++ /dev/null @@ -1,410 +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.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Music; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - public class FanartSeriesProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _json; - - private const string FanArtBaseUrl = "http://webservice.fanart.tv/v3/tv/{1}?api_key={0}"; - // &client_key=52c813aa7b8c8b3bb87f4797532a2f8c - - internal static FanartSeriesProvider Current { get; private set; } - - public FanartSeriesProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem, IJsonSerializer json) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - _json = json; - - Current = this; - } - - public string Name - { - get { return ProviderName; } - } - - public static string ProviderName - { - get { return "FanArt"; } - } - - public bool Supports(IHasImages item) - { - return item is Series; - } - - public IEnumerable GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary, - ImageType.Thumb, - ImageType.Art, - ImageType.Logo, - ImageType.Backdrop, - ImageType.Banner - }; - } - - public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) - { - var list = new List(); - - var series = (Series)item; - - var id = series.GetProviderId(MetadataProviders.Tvdb); - - if (!string.IsNullOrEmpty(id)) - { - // Bad id entered - try - { - await EnsureSeriesJson(id, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) - { - throw; - } - } - - var path = GetFanartJsonPath(id); - - try - { - AddImages(list, path, cancellationToken); - } - catch (FileNotFoundException) - { - // No biggie. Don't blow up - } - catch (DirectoryNotFoundException) - { - // No biggie. Don't blow up - } - } - - var language = item.GetPreferredMetadataLanguage(); - - var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); - - // Sort first by width to prioritize HD versions - return list.OrderByDescending(i => i.Width ?? 0) - .ThenByDescending(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) - .ThenByDescending(i => i.VoteCount ?? 0); - } - - private void AddImages(List list, string path, CancellationToken cancellationToken) - { - var root = _json.DeserializeFromFile(path); - - AddImages(list, root, cancellationToken); - } - - private void AddImages(List list, RootObject obj, CancellationToken cancellationToken) - { - PopulateImages(list, obj.hdtvlogo, ImageType.Logo, 800, 310); - PopulateImages(list, obj.hdclearart, ImageType.Art, 1000, 562); - PopulateImages(list, obj.clearlogo, ImageType.Logo, 400, 155); - PopulateImages(list, obj.clearart, ImageType.Art, 500, 281); - PopulateImages(list, obj.showbackground, ImageType.Backdrop, 1920, 1080, true); - PopulateImages(list, obj.seasonthumb, ImageType.Thumb, 500, 281); - PopulateImages(list, obj.tvthumb, ImageType.Thumb, 500, 281); - PopulateImages(list, obj.tvbanner, ImageType.Banner, 1000, 185); - PopulateImages(list, obj.tvposter, ImageType.Primary, 1000, 1426); - } - - private void PopulateImages(List list, - List images, - ImageType type, - int width, - int height, - bool allowSeasonAll = false) - { - if (images == null) - { - return; - } - - list.AddRange(images.Select(i => - { - var url = i.url; - var season = i.season; - - var isSeasonValid = string.IsNullOrEmpty(season) || - (allowSeasonAll && string.Equals(season, "all", StringComparison.OrdinalIgnoreCase)); - - if (!string.IsNullOrEmpty(url) && isSeasonValid) - { - var likesString = i.likes; - int likes; - - var info = new RemoteImageInfo - { - RatingType = RatingType.Likes, - Type = type, - Width = width, - Height = height, - ProviderName = Name, - Url = url, - Language = i.lang - }; - - if (!string.IsNullOrEmpty(likesString) && int.TryParse(likesString, NumberStyles.Any, _usCulture, out likes)) - { - info.CommunityRating = likes; - } - - return info; - } - - return null; - }).Where(i => i != null)); - } - - public int Order - { - get { return 1; } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = FanartArtistProvider.Current.FanArtResourcePool - }); - } - - /// - /// Gets the series data path. - /// - /// The app paths. - /// The series id. - /// System.String. - internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId) - { - var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); - - return seriesDataPath; - } - - /// - /// Gets the series data path. - /// - /// The app paths. - /// System.String. - internal static string GetSeriesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "fanart-tv"); - - return dataPath; - } - - public string GetFanartJsonPath(string tvdbId) - { - var dataPath = GetSeriesDataPath(_config.ApplicationPaths, tvdbId); - return Path.Combine(dataPath, "fanart.json"); - } - - private readonly SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); - internal async Task EnsureSeriesJson(string tvdbId, CancellationToken cancellationToken) - { - var path = GetFanartJsonPath(tvdbId); - - // Only allow one thread in here at a time since every season will be calling this method, possibly concurrently - await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3) - { - return; - } - } - - await DownloadSeriesJson(tvdbId, cancellationToken).ConfigureAwait(false); - } - finally - { - _ensureSemaphore.Release(); - } - } - - public FanartOptions GetFanartOptions() - { - return _config.GetConfiguration("fanart"); - } - - /// - /// Downloads the series json. - /// - /// The TVDB identifier. - /// The cancellation token. - /// Task. - internal async Task DownloadSeriesJson(string tvdbId, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var url = string.Format(FanArtBaseUrl, FanartArtistProvider.ApiKey, tvdbId); - - var clientKey = GetFanartOptions().UserApiKey; - if (!string.IsNullOrWhiteSpace(clientKey)) - { - url += "&client_key=" + clientKey; - } - - var path = GetFanartJsonPath(tvdbId); - - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - - try - { - using (var response = await _httpClient.Get(new HttpRequestOptions - { - Url = url, - ResourcePool = FanartArtistProvider.Current.FanArtResourcePool, - CancellationToken = cancellationToken - - }).ConfigureAwait(false)) - { - using (var fileStream = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true)) - { - await response.CopyToAsync(fileStream).ConfigureAwait(false); - } - } - } - catch (HttpException exception) - { - if (exception.StatusCode.HasValue && exception.StatusCode.Value == HttpStatusCode.NotFound) - { - // If the user has automatic updates enabled, save a dummy object to prevent repeated download attempts - _json.SerializeToFile(new RootObject(), path); - - return; - } - - throw; - } - } - - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) - { - var options = GetFanartOptions(); - if (!options.EnableAutomaticUpdates) - { - return false; - } - - var tvdbId = item.GetProviderId(MetadataProviders.Tvdb); - - if (!String.IsNullOrEmpty(tvdbId)) - { - // Process images - var imagesFilePath = GetFanartJsonPath(tvdbId); - - var fileInfo = _fileSystem.GetFileInfo(imagesFilePath); - - return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; - } - - return false; - } - - public class Image - { - public string id { get; set; } - public string url { get; set; } - public string lang { get; set; } - public string likes { get; set; } - public string season { get; set; } - } - - public class RootObject - { - public string name { get; set; } - public string thetvdb_id { get; set; } - public List clearlogo { get; set; } - public List hdtvlogo { get; set; } - public List clearart { get; set; } - public List showbackground { get; set; } - public List tvthumb { get; set; } - public List seasonposter { get; set; } - public List seasonthumb { get; set; } - public List hdclearart { get; set; } - public List tvbanner { get; set; } - public List characterart { get; set; } - public List tvposter { get; set; } - public List seasonbanner { get; set; } - } - } - - public class FanartConfigStore : IConfigurationFactory - { - public IEnumerable GetConfigurations() - { - return new List - { - new ConfigurationStore - { - Key = "fanart", - ConfigurationType = typeof(FanartOptions) - } - }; - } - } -} diff --git a/MediaBrowser.Providers/TV/MovieDbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/MovieDbEpisodeImageProvider.cs deleted file mode 100644 index 9d16849482..0000000000 --- a/MediaBrowser.Providers/TV/MovieDbEpisodeImageProvider.cs +++ /dev/null @@ -1,138 +0,0 @@ -using CommonIO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Localization; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Movies; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - public class MovieDbEpisodeImageProvider : - MovieDbProviderBase, - IRemoteImageProvider, - IHasOrder - { - public MovieDbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogManager logManager) - : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, logManager) - {} - - public IEnumerable GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary - }; - } - - public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) - { - var episode = (Controller.Entities.TV.Episode)item; - var series = episode.Series; - - var seriesId = series != null ? series.GetProviderId(MetadataProviders.Tmdb) : null; - - var list = new List(); - - if (string.IsNullOrEmpty(seriesId)) - { - return list; - } - - var seasonNumber = episode.ParentIndexNumber; - var episodeNumber = episode.IndexNumber; - - if (!seasonNumber.HasValue || !episodeNumber.HasValue) - { - return list; - } - - var response = await GetEpisodeInfo(seriesId, seasonNumber.Value, episodeNumber.Value, - item.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); - - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.base_url + "original"; - - list.AddRange(GetPosters(response.images).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); - - var language = item.GetPreferredMetadataLanguage(); - - 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) - .ThenByDescending(i => i.VoteCount ?? 0) - .ToList(); - - } - - private IEnumerable GetPosters(Images images) - { - return images.stills ?? new List(); - } - - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return GetResponse(url, cancellationToken); - } - - public string Name - { - get { return "TheMovieDb"; } - } - - public bool Supports(IHasImages item) - { - return item is Controller.Entities.TV.Episode; - } - - public int Order - { - get - { - // After tvdb - return 1; - } - } - } -} diff --git a/MediaBrowser.Providers/TV/MovieDbEpisodeProvider.cs b/MediaBrowser.Providers/TV/MovieDbEpisodeProvider.cs deleted file mode 100644 index 6a98fcf612..0000000000 --- a/MediaBrowser.Providers/TV/MovieDbEpisodeProvider.cs +++ /dev/null @@ -1,152 +0,0 @@ -using CommonIO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Localization; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - class MovieDbEpisodeProvider : - MovieDbProviderBase, - IRemoteMetadataProvider, - IHasOrder - { - public MovieDbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogManager logManager) - : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, logManager) - { } - - public Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) - { - return Task.FromResult>(new List()); - } - - public async Task> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) - { - var result = new MetadataResult(); - - string seriesTmdbId; - info.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out seriesTmdbId); - - if (string.IsNullOrEmpty(seriesTmdbId)) - { - return result; - } - - var seasonNumber = info.ParentIndexNumber; - var episodeNumber = info.IndexNumber; - - if (!seasonNumber.HasValue || !episodeNumber.HasValue) - { - return result; - } - - try - { - var response = await GetEpisodeInfo(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - result.HasMetadata = true; - - var item = new Episode(); - result.Item = item; - - item.Name = info.Name; - item.IndexNumber = info.IndexNumber; - item.ParentIndexNumber = info.ParentIndexNumber; - item.IndexNumberEnd = info.IndexNumberEnd; - - if (response.external_ids.tvdb_id > 0) - { - item.SetProviderId(MetadataProviders.Tvdb, response.external_ids.tvdb_id.ToString(CultureInfo.InvariantCulture)); - } - - item.PremiereDate = response.air_date; - item.ProductionYear = result.Item.PremiereDate.Value.Year; - - item.Name = response.name; - item.Overview = response.overview; - - item.CommunityRating = (float)response.vote_average; - item.VoteCount = response.vote_count; - - result.ResetPeople(); - - var credits = response.credits; - if (credits != null) - { - //Actors, Directors, Writers - all in People - //actors come from cast - if (credits.cast != null) - { - foreach (var actor in credits.cast.OrderBy(a => a.order)) - { - result.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order }); - } - } - - // guest stars - if (credits.guest_stars != null) - { - foreach (var guest in credits.guest_stars.OrderBy(a => a.order)) - { - result.AddPerson(new PersonInfo { Name = guest.name.Trim(), Role = guest.character, Type = PersonType.GuestStar, SortOrder = guest.order }); - } - } - - //and the rest from crew - if (credits.crew != null) - { - foreach (var person in credits.crew) - { - result.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department }); - } - } - } - } - catch (HttpException ex) - { - Logger.Error("No metadata found for {0}", seasonNumber.Value); - - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - return result; - } - - throw; - } - - return result; - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return GetResponse(url, cancellationToken); - } - - public int Order - { - get - { - // After TheTvDb - return 1; - } - } - - public string Name - { - get { return "TheMovieDb"; } - } - } -} diff --git a/MediaBrowser.Providers/TV/MovieDbProviderBase.cs b/MediaBrowser.Providers/TV/MovieDbProviderBase.cs deleted file mode 100644 index d22827c25e..0000000000 --- a/MediaBrowser.Providers/TV/MovieDbProviderBase.cs +++ /dev/null @@ -1,234 +0,0 @@ -using CommonIO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Localization; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Movies; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - public abstract class MovieDbProviderBase - { - private const string EpisodeUrlPattern = @"http://api.themoviedb.org/3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos"; - private readonly IHttpClient _httpClient; - private readonly IServerConfigurationManager _configurationManager; - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - private readonly ILogger _logger; - - public MovieDbProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogManager logManager) - { - _httpClient = httpClient; - _configurationManager = configurationManager; - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - _localization = localization; - _logger = logManager.GetLogger(GetType().Name); - } - - protected ILogger Logger - { - get { return _logger; } - } - - protected async Task GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage, - CancellationToken cancellationToken) - { - await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage); - - return _jsonSerializer.DeserializeFromFile(dataFilePath); - } - - internal Task EnsureEpisodeInfo(string tmdbId, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException("tmdbId"); - } - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException("language"); - } - - var path = GetDataFilePath(tmdbId, seasonNumber, episodeNumber, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3) - { - return Task.FromResult(true); - } - } - - return DownloadEpisodeInfo(tmdbId, seasonNumber, episodeNumber, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, int seasonNumber, int episodeNumber, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException("tmdbId"); - } - if (string.IsNullOrEmpty(preferredLanguage)) - { - throw new ArgumentNullException("preferredLanguage"); - } - - var path = MovieDbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - - var filename = string.Format("season-{0}-episode-{1}-{2}.json", - seasonNumber.ToString(CultureInfo.InvariantCulture), - episodeNumber.ToString(CultureInfo.InvariantCulture), - preferredLanguage); - - return Path.Combine(path, filename); - } - - internal async Task DownloadEpisodeInfo(string id, int seasonNumber, int episodeNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(EpisodeUrlPattern, id, seasonNumber, episodeNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(id, seasonNumber, episodeNumber, preferredMetadataLanguage); - - _fileSystem.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal async Task FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) - { - var url = string.Format(urlPattern, id, seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber, MovieDbProvider.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format("&language={0}", language); - } - - var includeImageLanguageParam = MovieDbProvider.GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; - - cancellationToken.ThrowIfCancellationRequested(); - - using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader - - }).ConfigureAwait(false)) - { - return _jsonSerializer.DeserializeFromStream(json); - } - } - - protected Task GetResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = MovieDbProvider.Current.MovieDbResourcePool - }); - } - - public class Still - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string id { get; set; } - public object iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - public class Images - { - public List stills { get; set; } - } - - public class ExternalIds - { - public string imdb_id { get; set; } - public object freebase_id { get; set; } - public string freebase_mid { get; set; } - public int tvdb_id { get; set; } - public int tvrage_id { get; set; } - } - - public class Cast - { - public string character { get; set; } - public string credit_id { get; set; } - public int id { get; set; } - public string name { get; set; } - public string profile_path { get; set; } - public int order { get; set; } - } - - public class Crew - { - public int id { get; set; } - public string credit_id { get; set; } - public string name { get; set; } - public string department { get; set; } - public string job { get; set; } - public string profile_path { get; set; } - } - - public class GuestStar - { - public int id { get; set; } - public string name { get; set; } - public string credit_id { get; set; } - public string character { get; set; } - public int order { get; set; } - public string profile_path { get; set; } - } - - public class Credits - { - public List cast { get; set; } - public List crew { get; set; } - public List guest_stars { get; set; } - } - - public class Videos - { - public List results { get; set; } - } - - public class RootObject - { - public DateTime air_date { get; set; } - public int episode_number { get; set; } - public string name { get; set; } - public string overview { get; set; } - public int id { get; set; } - public object production_code { get; set; } - public int season_number { get; set; } - public string still_path { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public Images images { get; set; } - public ExternalIds external_ids { get; set; } - public Credits credits { get; set; } - public Videos videos { get; set; } - } - } -} diff --git a/MediaBrowser.Providers/TV/MovieDbSeasonProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeasonProvider.cs deleted file mode 100644 index 0033c8a2ff..0000000000 --- a/MediaBrowser.Providers/TV/MovieDbSeasonProvider.cs +++ /dev/null @@ -1,308 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Localization; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Movies; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - public class MovieDbSeasonProvider : IRemoteMetadataProvider - { - private const string GetTvInfo3 = @"http://api.themoviedb.org/3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos"; - private readonly IHttpClient _httpClient; - private readonly IServerConfigurationManager _configurationManager; - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; - private readonly ILogger _logger; - - public MovieDbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogManager logManager) - { - _httpClient = httpClient; - _configurationManager = configurationManager; - _fileSystem = fileSystem; - _localization = localization; - _jsonSerializer = jsonSerializer; - _logger = logManager.GetLogger(GetType().Name); - } - - public async Task> GetMetadata(SeasonInfo info, CancellationToken cancellationToken) - { - var result = new MetadataResult(); - - string seriesTmdbId; - info.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out seriesTmdbId); - - var seasonNumber = info.IndexNumber; - - if (!string.IsNullOrWhiteSpace(seriesTmdbId) && seasonNumber.HasValue) - { - try - { - var seasonInfo = await GetSeasonInfo(seriesTmdbId, seasonNumber.Value, info.MetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - result.HasMetadata = true; - result.Item = new Season(); - result.Item.Name = info.Name; - result.Item.IndexNumber = seasonNumber; - - result.Item.Overview = seasonInfo.overview; - - if (seasonInfo.external_ids.tvdb_id > 0) - { - result.Item.SetProviderId(MetadataProviders.Tvdb, seasonInfo.external_ids.tvdb_id.ToString(CultureInfo.InvariantCulture)); - } - - var credits = seasonInfo.credits; - if (credits != null) - { - //Actors, Directors, Writers - all in People - //actors come from cast - if (credits.cast != null) - { - //foreach (var actor in credits.cast.OrderBy(a => a.order)) result.Item.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order }); - } - - //and the rest from crew - if (credits.crew != null) - { - //foreach (var person in credits.crew) result.Item.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department }); - } - } - - result.Item.PremiereDate = seasonInfo.air_date; - result.Item.ProductionYear = result.Item.PremiereDate.Value.Year; - } - catch (HttpException ex) - { - _logger.Error("No metadata found for {0}", seasonNumber.Value); - - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - return result; - } - - throw; - } - } - - return result; - } - - public string Name - { - get { return "TheMovieDb"; } - } - - public Task> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken) - { - return Task.FromResult>(new List()); - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = MovieDbProvider.Current.MovieDbResourcePool - }); - } - - private async Task GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage, - CancellationToken cancellationToken) - { - await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken) - .ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(seriesTmdbId, season, preferredMetadataLanguage); - - return _jsonSerializer.DeserializeFromFile(dataFilePath); - } - - private readonly Task _cachedTask = Task.FromResult(true); - internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException("tmdbId"); - } - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException("language"); - } - - var path = GetDataFilePath(tmdbId, seasonNumber, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3) - { - return _cachedTask; - } - } - - return DownloadSeasonInfo(tmdbId, seasonNumber, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, int seasonNumber, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException("tmdbId"); - } - if (string.IsNullOrEmpty(preferredLanguage)) - { - throw new ArgumentNullException("preferredLanguage"); - } - - var path = MovieDbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - - var filename = string.Format("season-{0}-{1}.json", - seasonNumber.ToString(CultureInfo.InvariantCulture), - preferredLanguage); - - return Path.Combine(path, filename); - } - - internal async Task DownloadSeasonInfo(string id, int seasonNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(id, seasonNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(id, seasonNumber, preferredMetadataLanguage); - - _fileSystem.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal async Task FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken) - { - var url = string.Format(GetTvInfo3, id, seasonNumber.ToString(CultureInfo.InvariantCulture), MovieDbProvider.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format("&language={0}", MovieDbProvider.NormalizeLanguage(language)); - } - - var includeImageLanguageParam = MovieDbProvider.GetImageLanguagesParam(language); - // Get images in english and with no language - url += "&include_image_language=" + includeImageLanguageParam; - - cancellationToken.ThrowIfCancellationRequested(); - - using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader - - }).ConfigureAwait(false)) - { - return _jsonSerializer.DeserializeFromStream(json); - } - } - - public class Episode - { - public string air_date { get; set; } - public int episode_number { get; set; } - public int id { get; set; } - public string name { get; set; } - public string overview { get; set; } - public string still_path { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - } - - public class Cast - { - public string character { get; set; } - public string credit_id { get; set; } - public int id { get; set; } - public string name { get; set; } - public string profile_path { get; set; } - public int order { get; set; } - } - - public class Crew - { - public string credit_id { get; set; } - public string department { get; set; } - public int id { get; set; } - public string name { get; set; } - public string job { get; set; } - public string profile_path { get; set; } - } - - public class Credits - { - public List cast { get; set; } - public List crew { get; set; } - } - - public class Poster - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string id { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - public class Images - { - public List posters { get; set; } - } - - public class ExternalIds - { - public string freebase_id { get; set; } - public string freebase_mid { get; set; } - public int tvdb_id { get; set; } - public object tvrage_id { get; set; } - } - - public class Videos - { - public List results { get; set; } - } - - public class RootObject - { - public DateTime air_date { get; set; } - public List episodes { get; set; } - public string name { get; set; } - public string overview { get; set; } - public int id { get; set; } - public string poster_path { get; set; } - public int season_number { get; set; } - public Credits credits { get; set; } - public Images images { get; set; } - public ExternalIds external_ids { get; set; } - public Videos videos { get; set; } - } - } -} diff --git a/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs deleted file mode 100644 index f7c19988c3..0000000000 --- a/MediaBrowser.Providers/TV/MovieDbSeriesImageProvider.cs +++ /dev/null @@ -1,204 +0,0 @@ -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Movies; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - public class MovieDbSeriesImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor - { - private readonly IJsonSerializer _jsonSerializer; - private readonly IHttpClient _httpClient; - - public MovieDbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient) - { - _jsonSerializer = jsonSerializer; - _httpClient = httpClient; - } - - public string Name - { - get { return ProviderName; } - } - - public static string ProviderName - { - get { return "TheMovieDb"; } - } - - public bool Supports(IHasImages item) - { - return item is Series; - } - - public IEnumerable GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary, - ImageType.Backdrop - }; - } - - public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) - { - var list = new List(); - - var results = await FetchImages((BaseItem)item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false); - - if (results == null) - { - return list; - } - - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - - var tmdbImageUrl = tmdbSettings.images.base_url + "original"; - - list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, - Language = i.iso_639_1, - ProviderName = Name, - Type = ImageType.Primary, - RatingType = RatingType.Score - })); - - list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo - { - Url = tmdbImageUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, - ProviderName = Name, - Type = ImageType.Backdrop, - RatingType = RatingType.Score - })); - - var language = item.GetPreferredMetadataLanguage(); - - 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) - .ThenByDescending(i => i.VoteCount ?? 0) - .ToList(); - } - - /// - /// Gets the posters. - /// - /// The images. - private IEnumerable GetPosters(MovieDbSeriesProvider.Images images) - { - return images.posters ?? new List(); - } - - /// - /// Gets the backdrops. - /// - /// The images. - private IEnumerable GetBackdrops(MovieDbSeriesProvider.Images images) - { - var eligibleBackdrops = images.backdrops == null ? new List() : - images.backdrops - .ToList(); - - return eligibleBackdrops.OrderByDescending(i => i.vote_average) - .ThenByDescending(i => i.vote_count); - } - - /// - /// Fetches the images. - /// - /// The item. - /// The language. - /// The json serializer. - /// The cancellation token. - /// Task{MovieImages}. - private async Task FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, - CancellationToken cancellationToken) - { - var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); - - if (string.IsNullOrEmpty(tmdbId)) - { - return null; - } - - await MovieDbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - - var path = MovieDbSeriesProvider.Current.GetDataFilePath(tmdbId, language); - - if (!string.IsNullOrEmpty(path)) - { - var fileInfo = new FileInfo(path); - - if (fileInfo.Exists) - { - return jsonSerializer.DeserializeFromFile(path).images; - } - } - - return null; - } - - public int Order - { - get - { - // After tvdb and fanart - return 2; - } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = MovieDbProvider.Current.MovieDbResourcePool - }); - } - - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) - { - return MovieDbSeriesProvider.Current.HasChanged(item, date); - } - } -} diff --git a/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs b/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs deleted file mode 100644 index ad2cfa12b5..0000000000 --- a/MediaBrowser.Providers/TV/MovieDbSeriesProvider.cs +++ /dev/null @@ -1,615 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Movies; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - public class MovieDbSeriesProvider : IRemoteMetadataProvider, IHasOrder - { - private const string GetTvInfo3 = @"http://api.themoviedb.org/3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos"; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - internal static MovieDbSeriesProvider Current { get; private set; } - - private readonly IJsonSerializer _jsonSerializer; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _configurationManager; - private readonly ILogger _logger; - private readonly ILocalizationManager _localization; - private readonly IHttpClient _httpClient; - private readonly ILibraryManager _libraryManager; - - public MovieDbSeriesProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization, IHttpClient httpClient, ILibraryManager libraryManager) - { - _jsonSerializer = jsonSerializer; - _fileSystem = fileSystem; - _configurationManager = configurationManager; - _logger = logger; - _localization = localization; - _httpClient = httpClient; - _libraryManager = libraryManager; - Current = this; - } - - public string Name - { - get { return "TheMovieDb"; } - } - - public async Task> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) - { - var tmdbId = searchInfo.GetProviderId(MetadataProviders.Tmdb); - - if (!string.IsNullOrEmpty(tmdbId)) - { - cancellationToken.ThrowIfCancellationRequested(); - - await EnsureSeriesInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage); - - var obj = _jsonSerializer.DeserializeFromFile(dataFilePath); - - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - var tmdbImageUrl = tmdbSettings.images.base_url + "original"; - - var remoteResult = new RemoteSearchResult - { - Name = obj.name, - SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(obj.poster_path) ? null : tmdbImageUrl + obj.poster_path - }; - - remoteResult.SetProviderId(MetadataProviders.Tmdb, obj.id.ToString(_usCulture)); - remoteResult.SetProviderId(MetadataProviders.Imdb, obj.external_ids.imdb_id); - - if (obj.external_ids.tvdb_id > 0) - { - remoteResult.SetProviderId(MetadataProviders.Tvdb, obj.external_ids.tvdb_id.ToString(_usCulture)); - } - - return new[] { remoteResult }; - } - - var imdbId = searchInfo.GetProviderId(MetadataProviders.Imdb); - - if (!string.IsNullOrEmpty(imdbId)) - { - var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false); - - if (searchResult != null) - { - return new[] { searchResult }; - } - } - - var tvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); - - if (!string.IsNullOrEmpty(tvdbId)) - { - var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false); - - if (searchResult != null) - { - return new[] { searchResult }; - } - } - - return await new MovieDbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) - { - var result = new MetadataResult(); - - var tmdbId = info.GetProviderId(MetadataProviders.Tmdb); - - if (string.IsNullOrEmpty(tmdbId)) - { - var imdbId = info.GetProviderId(MetadataProviders.Imdb); - - if (!string.IsNullOrEmpty(imdbId)) - { - var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false); - - if (searchResult != null) - { - tmdbId = searchResult.GetProviderId(MetadataProviders.Tmdb); - } - } - } - - if (string.IsNullOrEmpty(tmdbId)) - { - var tvdbId = info.GetProviderId(MetadataProviders.Tvdb); - - if (!string.IsNullOrEmpty(tvdbId)) - { - var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false); - - if (searchResult != null) - { - tmdbId = searchResult.GetProviderId(MetadataProviders.Tmdb); - } - } - } - - if (string.IsNullOrEmpty(tmdbId)) - { - var searchResults = await new MovieDbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false); - - var searchResult = searchResults.FirstOrDefault(); - - if (searchResult != null) - { - tmdbId = searchResult.GetProviderId(MetadataProviders.Tmdb); - } - } - - if (!string.IsNullOrEmpty(tmdbId)) - { - cancellationToken.ThrowIfCancellationRequested(); - - result.Item = await FetchMovieData(tmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - result.HasMetadata = result.Item != null; - } - - return result; - } - - private async Task FetchMovieData(string tmdbId, string language, CancellationToken cancellationToken) - { - string dataFilePath = null; - RootObject seriesInfo = null; - - if (!string.IsNullOrEmpty(tmdbId)) - { - seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false); - } - - if (seriesInfo == null) - { - return null; - } - - tmdbId = seriesInfo.id.ToString(_usCulture); - - dataFilePath = GetDataFilePath(tmdbId, language); - _fileSystem.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath); - - await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - - var item = new Series(); - - ProcessMainInfo(item, seriesInfo); - - return item; - } - - private void ProcessMainInfo(Series series, RootObject seriesInfo) - { - series.Name = seriesInfo.name; - series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.id.ToString(_usCulture)); - - series.VoteCount = seriesInfo.vote_count; - - string voteAvg = seriesInfo.vote_average.ToString(CultureInfo.InvariantCulture); - float rating; - - if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out rating)) - { - series.CommunityRating = rating; - } - - series.Overview = seriesInfo.overview; - - if (seriesInfo.networks != null) - { - series.Studios = seriesInfo.networks.Select(i => i.name).ToList(); - } - - if (seriesInfo.genres != null) - { - series.Genres = seriesInfo.genres.Select(i => i.name).ToList(); - } - - series.HomePageUrl = seriesInfo.homepage; - - series.RunTimeTicks = seriesInfo.episode_run_time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); - - if (string.Equals(seriesInfo.status, "Ended", StringComparison.OrdinalIgnoreCase)) - { - series.Status = SeriesStatus.Ended; - series.EndDate = seriesInfo.last_air_date; - } - else - { - series.Status = SeriesStatus.Continuing; - } - - series.PremiereDate = seriesInfo.first_air_date; - - var ids = seriesInfo.external_ids; - if (ids != null) - { - if (!string.IsNullOrWhiteSpace(ids.imdb_id)) - { - series.SetProviderId(MetadataProviders.Imdb, ids.imdb_id); - } - if (ids.tvrage_id > 0) - { - series.SetProviderId(MetadataProviders.TvRage, ids.tvrage_id.ToString(_usCulture)); - } - if (ids.tvdb_id > 0) - { - series.SetProviderId(MetadataProviders.Tvdb, ids.tvdb_id.ToString(_usCulture)); - } - } - } - - internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId) - { - var dataPath = GetSeriesDataPath(appPaths); - - return Path.Combine(dataPath, tmdbId); - } - - internal static string GetSeriesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tmdb-tv"); - - return dataPath; - } - - internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - var mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - - if (mainResult == null) return; - - var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage); - - _fileSystem.CreateDirectory(Path.GetDirectoryName(dataFilePath)); - - _jsonSerializer.SerializeToFile(mainResult, dataFilePath); - } - - internal async Task FetchMainResult(string id, string language, CancellationToken cancellationToken) - { - var url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey); - - if (!string.IsNullOrEmpty(language)) - { - url += string.Format("&language={0}", MovieDbProvider.NormalizeLanguage(language)); - - // Get images in english and with no language - url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); - } - - cancellationToken.ThrowIfCancellationRequested(); - - RootObject mainResult; - - using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader - - }).ConfigureAwait(false)) - { - mainResult = _jsonSerializer.DeserializeFromStream(json); - } - - cancellationToken.ThrowIfCancellationRequested(); - - // If the language preference isn't english, then have the overview fallback to english if it's blank - if (mainResult != null && - string.IsNullOrEmpty(mainResult.overview) && - !string.IsNullOrEmpty(language) && - !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - _logger.Info("MovieDbSeriesProvider couldn't find meta for language " + language + ". Trying English..."); - - url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey) + "&language=en"; - - if (!string.IsNullOrEmpty(language)) - { - // Get images in english and with no language - url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); - } - - using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader - - }).ConfigureAwait(false)) - { - var englishResult = _jsonSerializer.DeserializeFromStream(json); - - mainResult.overview = englishResult.overview; - } - } - - return mainResult; - } - - private readonly Task _cachedTask = Task.FromResult(true); - internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException("tmdbId"); - } - - var path = GetDataFilePath(tmdbId, language); - - var fileInfo = _fileSystem.GetFileSystemInfo(path); - - if (fileInfo.Exists) - { - // If it's recent or automatic updates are enabled, don't re-download - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3) - { - return _cachedTask; - } - } - - return DownloadSeriesInfo(tmdbId, language, cancellationToken); - } - - internal string GetDataFilePath(string tmdbId, string preferredLanguage) - { - if (string.IsNullOrEmpty(tmdbId)) - { - throw new ArgumentNullException("tmdbId"); - } - - var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); - - var filename = string.Format("series-{0}.json", preferredLanguage ?? string.Empty); - - return Path.Combine(path, filename); - } - - public bool HasChanged(IHasMetadata item, DateTime date) - { - if (!MovieDbProvider.Current.GetTheMovieDbOptions().EnableAutomaticUpdates) - { - return false; - } - - var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); - - if (!String.IsNullOrEmpty(tmdbId)) - { - // Process images - var dataFilePath = GetDataFilePath(tmdbId, item.GetPreferredMetadataLanguage()); - - var fileInfo = _fileSystem.GetFileInfo(dataFilePath); - - return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; - } - - return false; - } - - private async Task FindByExternalId(string id, string externalSource, CancellationToken cancellationToken) - { - var url = string.Format("http://api.themoviedb.org/3/tv/find/{0}?api_key={1}&external_source={2}", - id, - MovieDbProvider.ApiKey, - externalSource); - - using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader - - }).ConfigureAwait(false)) - { - var result = _jsonSerializer.DeserializeFromStream(json); - - if (result != null && result.tv_results != null) - { - var tv = result.tv_results.FirstOrDefault(); - - if (tv != null) - { - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); - var tmdbImageUrl = tmdbSettings.images.base_url + "original"; - - var remoteResult = new RemoteSearchResult - { - Name = tv.name, - SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(tv.poster_path) ? null : tmdbImageUrl + tv.poster_path - }; - - remoteResult.SetProviderId(MetadataProviders.Tmdb, tv.id.ToString(_usCulture)); - - return remoteResult; - } - } - } - - return null; - } - - public class CreatedBy - { - public int id { get; set; } - public string name { get; set; } - public string profile_path { get; set; } - } - - public class Genre - { - public int id { get; set; } - public string name { get; set; } - } - - public class Network - { - public int id { get; set; } - public string name { get; set; } - } - - public class Season - { - public string air_date { get; set; } - public int id { get; set; } - public string poster_path { get; set; } - public int season_number { get; set; } - } - - public class Cast - { - public string character { get; set; } - public string credit_id { get; set; } - public int id { get; set; } - public string name { get; set; } - public string profile_path { get; set; } - public int order { get; set; } - } - - public class Crew - { - public string credit_id { get; set; } - public string department { get; set; } - public int id { get; set; } - public string name { get; set; } - public string job { get; set; } - public string profile_path { get; set; } - } - - public class Credits - { - public List cast { get; set; } - public List crew { get; set; } - } - - public class Backdrop - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - public class Poster - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string id { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - public class Images - { - public List backdrops { get; set; } - public List posters { get; set; } - } - - public class Keywords - { - public List results { get; set; } - } - - public class ExternalIds - { - public string imdb_id { get; set; } - public string freebase_id { get; set; } - public string freebase_mid { get; set; } - public int tvdb_id { get; set; } - public int tvrage_id { get; set; } - } - - public class Videos - { - public List results { get; set; } - } - - public class RootObject - { - public string backdrop_path { get; set; } - public List created_by { get; set; } - public List episode_run_time { get; set; } - public DateTime first_air_date { get; set; } - public List genres { get; set; } - public string homepage { get; set; } - public int id { get; set; } - public bool in_production { get; set; } - public List languages { get; set; } - public DateTime last_air_date { get; set; } - public string name { get; set; } - public List networks { get; set; } - public int number_of_episodes { get; set; } - public int number_of_seasons { get; set; } - public string original_name { get; set; } - public List origin_country { get; set; } - public string overview { get; set; } - public string popularity { get; set; } - public string poster_path { get; set; } - public List seasons { get; set; } - public string status { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public Credits credits { get; set; } - public Images images { get; set; } - public Keywords keywords { get; set; } - public ExternalIds external_ids { get; set; } - public Videos videos { get; set; } - } - - public int Order - { - get - { - // After Omdb and Tvdb - return 2; - } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = MovieDbProvider.Current.MovieDbResourcePool - }); - } - } -} diff --git a/MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs new file mode 100644 index 0000000000..5a920c37fa --- /dev/null +++ b/MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs @@ -0,0 +1,89 @@ +using MediaBrowser.Common.Net; +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 MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Omdb; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + class OmdbEpisodeProvider : + IRemoteMetadataProvider, + IHasOrder + { + private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClient _httpClient; + private OmdbItemProvider _itemProvider; + + public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager) + { + _jsonSerializer = jsonSerializer; + _httpClient = httpClient; + _itemProvider = new OmdbItemProvider(jsonSerializer, httpClient, logger, libraryManager); + } + + public Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken); + } + + public async Task> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult + { + Item = new Episode() + }; + + var imdbId = info.GetProviderId(MetadataProviders.Imdb); + if (string.IsNullOrWhiteSpace(imdbId)) + { + imdbId = await GetEpisodeImdbId(info, cancellationToken).ConfigureAwait(false); + } + + if (!string.IsNullOrEmpty(imdbId)) + { + result.Item.SetProviderId(MetadataProviders.Imdb, imdbId); + result.HasMetadata = true; + + await new OmdbProvider(_jsonSerializer, _httpClient).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + } + + return result; + } + + private async Task GetEpisodeImdbId(EpisodeInfo info, CancellationToken cancellationToken) + { + var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); + var first = results.FirstOrDefault(); + return first == null ? null : first.GetProviderId(MetadataProviders.Imdb); + } + + public int Order + { + get + { + // After TheTvDb + return 1; + } + } + + public string Name + { + get { return "The Open Movie Database"; } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _itemProvider.GetImageResponse(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/TV/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/OmdbEpisodeProvider.cs deleted file mode 100644 index 5a920c37fa..0000000000 --- a/MediaBrowser.Providers/TV/OmdbEpisodeProvider.cs +++ /dev/null @@ -1,89 +0,0 @@ -using MediaBrowser.Common.Net; -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 MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Omdb; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Providers.TV -{ - class OmdbEpisodeProvider : - IRemoteMetadataProvider, - IHasOrder - { - private readonly IJsonSerializer _jsonSerializer; - private readonly IHttpClient _httpClient; - private OmdbItemProvider _itemProvider; - - public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager) - { - _jsonSerializer = jsonSerializer; - _httpClient = httpClient; - _itemProvider = new OmdbItemProvider(jsonSerializer, httpClient, logger, libraryManager); - } - - public Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) - { - return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken); - } - - public async Task> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) - { - var result = new MetadataResult - { - Item = new Episode() - }; - - var imdbId = info.GetProviderId(MetadataProviders.Imdb); - if (string.IsNullOrWhiteSpace(imdbId)) - { - imdbId = await GetEpisodeImdbId(info, cancellationToken).ConfigureAwait(false); - } - - if (!string.IsNullOrEmpty(imdbId)) - { - result.Item.SetProviderId(MetadataProviders.Imdb, imdbId); - result.HasMetadata = true; - - await new OmdbProvider(_jsonSerializer, _httpClient).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); - } - - return result; - } - - private async Task GetEpisodeImdbId(EpisodeInfo info, CancellationToken cancellationToken) - { - var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); - var first = results.FirstOrDefault(); - return first == null ? null : first.GetProviderId(MetadataProviders.Imdb); - } - - public int Order - { - get - { - // After TheTvDb - return 1; - } - } - - public string Name - { - get { return "The Open Movie Database"; } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _itemProvider.GetImageResponse(url, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeImageProvider.cs new file mode 100644 index 0000000000..9d16849482 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeImageProvider.cs @@ -0,0 +1,138 @@ +using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + public class MovieDbEpisodeImageProvider : + MovieDbProviderBase, + IRemoteImageProvider, + IHasOrder + { + public MovieDbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogManager logManager) + : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, logManager) + {} + + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary + }; + } + + public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) + { + var episode = (Controller.Entities.TV.Episode)item; + var series = episode.Series; + + var seriesId = series != null ? series.GetProviderId(MetadataProviders.Tmdb) : null; + + var list = new List(); + + if (string.IsNullOrEmpty(seriesId)) + { + return list; + } + + var seasonNumber = episode.ParentIndexNumber; + var episodeNumber = episode.IndexNumber; + + if (!seasonNumber.HasValue || !episodeNumber.HasValue) + { + return list; + } + + var response = await GetEpisodeInfo(seriesId, seasonNumber.Value, episodeNumber.Value, + item.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); + + var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + + var tmdbImageUrl = tmdbSettings.images.base_url + "original"; + + list.AddRange(GetPosters(response.images).Select(i => new RemoteImageInfo + { + Url = tmdbImageUrl + i.file_path, + CommunityRating = i.vote_average, + VoteCount = i.vote_count, + Width = i.width, + Height = i.height, + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + })); + + var language = item.GetPreferredMetadataLanguage(); + + 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) + .ThenByDescending(i => i.VoteCount ?? 0) + .ToList(); + + } + + private IEnumerable GetPosters(Images images) + { + return images.stills ?? new List(); + } + + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return GetResponse(url, cancellationToken); + } + + public string Name + { + get { return "TheMovieDb"; } + } + + public bool Supports(IHasImages item) + { + return item is Controller.Entities.TV.Episode; + } + + public int Order + { + get + { + // After tvdb + return 1; + } + } + } +} diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs new file mode 100644 index 0000000000..6a98fcf612 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs @@ -0,0 +1,152 @@ +using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + class MovieDbEpisodeProvider : + MovieDbProviderBase, + IRemoteMetadataProvider, + IHasOrder + { + public MovieDbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogManager logManager) + : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, logManager) + { } + + public Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + return Task.FromResult>(new List()); + } + + public async Task> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult(); + + string seriesTmdbId; + info.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out seriesTmdbId); + + if (string.IsNullOrEmpty(seriesTmdbId)) + { + return result; + } + + var seasonNumber = info.ParentIndexNumber; + var episodeNumber = info.IndexNumber; + + if (!seasonNumber.HasValue || !episodeNumber.HasValue) + { + return result; + } + + try + { + var response = await GetEpisodeInfo(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + result.HasMetadata = true; + + var item = new Episode(); + result.Item = item; + + item.Name = info.Name; + item.IndexNumber = info.IndexNumber; + item.ParentIndexNumber = info.ParentIndexNumber; + item.IndexNumberEnd = info.IndexNumberEnd; + + if (response.external_ids.tvdb_id > 0) + { + item.SetProviderId(MetadataProviders.Tvdb, response.external_ids.tvdb_id.ToString(CultureInfo.InvariantCulture)); + } + + item.PremiereDate = response.air_date; + item.ProductionYear = result.Item.PremiereDate.Value.Year; + + item.Name = response.name; + item.Overview = response.overview; + + item.CommunityRating = (float)response.vote_average; + item.VoteCount = response.vote_count; + + result.ResetPeople(); + + var credits = response.credits; + if (credits != null) + { + //Actors, Directors, Writers - all in People + //actors come from cast + if (credits.cast != null) + { + foreach (var actor in credits.cast.OrderBy(a => a.order)) + { + result.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order }); + } + } + + // guest stars + if (credits.guest_stars != null) + { + foreach (var guest in credits.guest_stars.OrderBy(a => a.order)) + { + result.AddPerson(new PersonInfo { Name = guest.name.Trim(), Role = guest.character, Type = PersonType.GuestStar, SortOrder = guest.order }); + } + } + + //and the rest from crew + if (credits.crew != null) + { + foreach (var person in credits.crew) + { + result.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department }); + } + } + } + } + catch (HttpException ex) + { + Logger.Error("No metadata found for {0}", seasonNumber.Value); + + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + return result; + } + + throw; + } + + return result; + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return GetResponse(url, cancellationToken); + } + + public int Order + { + get + { + // After TheTvDb + return 1; + } + } + + public string Name + { + get { return "TheMovieDb"; } + } + } +} diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbProviderBase.cs b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbProviderBase.cs new file mode 100644 index 0000000000..d22827c25e --- /dev/null +++ b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbProviderBase.cs @@ -0,0 +1,234 @@ +using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + public abstract class MovieDbProviderBase + { + private const string EpisodeUrlPattern = @"http://api.themoviedb.org/3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos"; + private readonly IHttpClient _httpClient; + private readonly IServerConfigurationManager _configurationManager; + private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + private readonly ILogger _logger; + + public MovieDbProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILogManager logManager) + { + _httpClient = httpClient; + _configurationManager = configurationManager; + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _localization = localization; + _logger = logManager.GetLogger(GetType().Name); + } + + protected ILogger Logger + { + get { return _logger; } + } + + protected async Task GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage, + CancellationToken cancellationToken) + { + await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken) + .ConfigureAwait(false); + + var dataFilePath = GetDataFilePath(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage); + + return _jsonSerializer.DeserializeFromFile(dataFilePath); + } + + internal Task EnsureEpisodeInfo(string tmdbId, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(tmdbId)) + { + throw new ArgumentNullException("tmdbId"); + } + if (string.IsNullOrEmpty(language)) + { + throw new ArgumentNullException("language"); + } + + var path = GetDataFilePath(tmdbId, seasonNumber, episodeNumber, language); + + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + if (fileInfo.Exists) + { + // If it's recent or automatic updates are enabled, don't re-download + if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3) + { + return Task.FromResult(true); + } + } + + return DownloadEpisodeInfo(tmdbId, seasonNumber, episodeNumber, language, cancellationToken); + } + + internal string GetDataFilePath(string tmdbId, int seasonNumber, int episodeNumber, string preferredLanguage) + { + if (string.IsNullOrEmpty(tmdbId)) + { + throw new ArgumentNullException("tmdbId"); + } + if (string.IsNullOrEmpty(preferredLanguage)) + { + throw new ArgumentNullException("preferredLanguage"); + } + + var path = MovieDbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); + + var filename = string.Format("season-{0}-episode-{1}-{2}.json", + seasonNumber.ToString(CultureInfo.InvariantCulture), + episodeNumber.ToString(CultureInfo.InvariantCulture), + preferredLanguage); + + return Path.Combine(path, filename); + } + + internal async Task DownloadEpisodeInfo(string id, int seasonNumber, int episodeNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + var mainResult = await FetchMainResult(EpisodeUrlPattern, id, seasonNumber, episodeNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + + var dataFilePath = GetDataFilePath(id, seasonNumber, episodeNumber, preferredMetadataLanguage); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(dataFilePath)); + _jsonSerializer.SerializeToFile(mainResult, dataFilePath); + } + + internal async Task FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) + { + var url = string.Format(urlPattern, id, seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber, MovieDbProvider.ApiKey); + + if (!string.IsNullOrEmpty(language)) + { + url += string.Format("&language={0}", language); + } + + var includeImageLanguageParam = MovieDbProvider.GetImageLanguagesParam(language); + // Get images in english and with no language + url += "&include_image_language=" + includeImageLanguageParam; + + cancellationToken.ThrowIfCancellationRequested(); + + using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + AcceptHeader = MovieDbProvider.AcceptHeader + + }).ConfigureAwait(false)) + { + return _jsonSerializer.DeserializeFromStream(json); + } + } + + protected Task GetResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = MovieDbProvider.Current.MovieDbResourcePool + }); + } + + public class Still + { + public double aspect_ratio { get; set; } + public string file_path { get; set; } + public int height { get; set; } + public string id { get; set; } + public object iso_639_1 { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public int width { get; set; } + } + + public class Images + { + public List stills { get; set; } + } + + public class ExternalIds + { + public string imdb_id { get; set; } + public object freebase_id { get; set; } + public string freebase_mid { get; set; } + public int tvdb_id { get; set; } + public int tvrage_id { get; set; } + } + + public class Cast + { + public string character { get; set; } + public string credit_id { get; set; } + public int id { get; set; } + public string name { get; set; } + public string profile_path { get; set; } + public int order { get; set; } + } + + public class Crew + { + public int id { get; set; } + public string credit_id { get; set; } + public string name { get; set; } + public string department { get; set; } + public string job { get; set; } + public string profile_path { get; set; } + } + + public class GuestStar + { + public int id { get; set; } + public string name { get; set; } + public string credit_id { get; set; } + public string character { get; set; } + public int order { get; set; } + public string profile_path { get; set; } + } + + public class Credits + { + public List cast { get; set; } + public List crew { get; set; } + public List guest_stars { get; set; } + } + + public class Videos + { + public List results { get; set; } + } + + public class RootObject + { + public DateTime air_date { get; set; } + public int episode_number { get; set; } + public string name { get; set; } + public string overview { get; set; } + public int id { get; set; } + public object production_code { get; set; } + public int season_number { get; set; } + public string still_path { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public Images images { get; set; } + public ExternalIds external_ids { get; set; } + public Credits credits { get; set; } + public Videos videos { get; set; } + } + } +} diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeasonProvider.cs b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeasonProvider.cs new file mode 100644 index 0000000000..0033c8a2ff --- /dev/null +++ b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeasonProvider.cs @@ -0,0 +1,308 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + public class MovieDbSeasonProvider : IRemoteMetadataProvider + { + private const string GetTvInfo3 = @"http://api.themoviedb.org/3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos"; + private readonly IHttpClient _httpClient; + private readonly IServerConfigurationManager _configurationManager; + private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + private readonly ILogger _logger; + + public MovieDbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILogManager logManager) + { + _httpClient = httpClient; + _configurationManager = configurationManager; + _fileSystem = fileSystem; + _localization = localization; + _jsonSerializer = jsonSerializer; + _logger = logManager.GetLogger(GetType().Name); + } + + public async Task> GetMetadata(SeasonInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult(); + + string seriesTmdbId; + info.SeriesProviderIds.TryGetValue(MetadataProviders.Tmdb.ToString(), out seriesTmdbId); + + var seasonNumber = info.IndexNumber; + + if (!string.IsNullOrWhiteSpace(seriesTmdbId) && seasonNumber.HasValue) + { + try + { + var seasonInfo = await GetSeasonInfo(seriesTmdbId, seasonNumber.Value, info.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); + + result.HasMetadata = true; + result.Item = new Season(); + result.Item.Name = info.Name; + result.Item.IndexNumber = seasonNumber; + + result.Item.Overview = seasonInfo.overview; + + if (seasonInfo.external_ids.tvdb_id > 0) + { + result.Item.SetProviderId(MetadataProviders.Tvdb, seasonInfo.external_ids.tvdb_id.ToString(CultureInfo.InvariantCulture)); + } + + var credits = seasonInfo.credits; + if (credits != null) + { + //Actors, Directors, Writers - all in People + //actors come from cast + if (credits.cast != null) + { + //foreach (var actor in credits.cast.OrderBy(a => a.order)) result.Item.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order }); + } + + //and the rest from crew + if (credits.crew != null) + { + //foreach (var person in credits.crew) result.Item.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department }); + } + } + + result.Item.PremiereDate = seasonInfo.air_date; + result.Item.ProductionYear = result.Item.PremiereDate.Value.Year; + } + catch (HttpException ex) + { + _logger.Error("No metadata found for {0}", seasonNumber.Value); + + if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + { + return result; + } + + throw; + } + } + + return result; + } + + public string Name + { + get { return "TheMovieDb"; } + } + + public Task> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken) + { + return Task.FromResult>(new List()); + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = MovieDbProvider.Current.MovieDbResourcePool + }); + } + + private async Task GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage, + CancellationToken cancellationToken) + { + await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken) + .ConfigureAwait(false); + + var dataFilePath = GetDataFilePath(seriesTmdbId, season, preferredMetadataLanguage); + + return _jsonSerializer.DeserializeFromFile(dataFilePath); + } + + private readonly Task _cachedTask = Task.FromResult(true); + internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, string language, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(tmdbId)) + { + throw new ArgumentNullException("tmdbId"); + } + if (string.IsNullOrEmpty(language)) + { + throw new ArgumentNullException("language"); + } + + var path = GetDataFilePath(tmdbId, seasonNumber, language); + + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + if (fileInfo.Exists) + { + // If it's recent or automatic updates are enabled, don't re-download + if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3) + { + return _cachedTask; + } + } + + return DownloadSeasonInfo(tmdbId, seasonNumber, language, cancellationToken); + } + + internal string GetDataFilePath(string tmdbId, int seasonNumber, string preferredLanguage) + { + if (string.IsNullOrEmpty(tmdbId)) + { + throw new ArgumentNullException("tmdbId"); + } + if (string.IsNullOrEmpty(preferredLanguage)) + { + throw new ArgumentNullException("preferredLanguage"); + } + + var path = MovieDbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); + + var filename = string.Format("season-{0}-{1}.json", + seasonNumber.ToString(CultureInfo.InvariantCulture), + preferredLanguage); + + return Path.Combine(path, filename); + } + + internal async Task DownloadSeasonInfo(string id, int seasonNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + var mainResult = await FetchMainResult(id, seasonNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + + var dataFilePath = GetDataFilePath(id, seasonNumber, preferredMetadataLanguage); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(dataFilePath)); + _jsonSerializer.SerializeToFile(mainResult, dataFilePath); + } + + internal async Task FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken) + { + var url = string.Format(GetTvInfo3, id, seasonNumber.ToString(CultureInfo.InvariantCulture), MovieDbProvider.ApiKey); + + if (!string.IsNullOrEmpty(language)) + { + url += string.Format("&language={0}", MovieDbProvider.NormalizeLanguage(language)); + } + + var includeImageLanguageParam = MovieDbProvider.GetImageLanguagesParam(language); + // Get images in english and with no language + url += "&include_image_language=" + includeImageLanguageParam; + + cancellationToken.ThrowIfCancellationRequested(); + + using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + AcceptHeader = MovieDbProvider.AcceptHeader + + }).ConfigureAwait(false)) + { + return _jsonSerializer.DeserializeFromStream(json); + } + } + + public class Episode + { + public string air_date { get; set; } + public int episode_number { get; set; } + public int id { get; set; } + public string name { get; set; } + public string overview { get; set; } + public string still_path { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + } + + public class Cast + { + public string character { get; set; } + public string credit_id { get; set; } + public int id { get; set; } + public string name { get; set; } + public string profile_path { get; set; } + public int order { get; set; } + } + + public class Crew + { + public string credit_id { get; set; } + public string department { get; set; } + public int id { get; set; } + public string name { get; set; } + public string job { get; set; } + public string profile_path { get; set; } + } + + public class Credits + { + public List cast { get; set; } + public List crew { get; set; } + } + + public class Poster + { + public double aspect_ratio { get; set; } + public string file_path { get; set; } + public int height { get; set; } + public string id { get; set; } + public string iso_639_1 { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public int width { get; set; } + } + + public class Images + { + public List posters { get; set; } + } + + public class ExternalIds + { + public string freebase_id { get; set; } + public string freebase_mid { get; set; } + public int tvdb_id { get; set; } + public object tvrage_id { get; set; } + } + + public class Videos + { + public List results { get; set; } + } + + public class RootObject + { + public DateTime air_date { get; set; } + public List episodes { get; set; } + public string name { get; set; } + public string overview { get; set; } + public int id { get; set; } + public string poster_path { get; set; } + public int season_number { get; set; } + public Credits credits { get; set; } + public Images images { get; set; } + public ExternalIds external_ids { get; set; } + public Videos videos { get; set; } + } + } +} diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesImageProvider.cs new file mode 100644 index 0000000000..f7c19988c3 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesImageProvider.cs @@ -0,0 +1,204 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + public class MovieDbSeriesImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor + { + private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClient _httpClient; + + public MovieDbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient) + { + _jsonSerializer = jsonSerializer; + _httpClient = httpClient; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "TheMovieDb"; } + } + + public bool Supports(IHasImages item) + { + return item is Series; + } + + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary, + ImageType.Backdrop + }; + } + + public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) + { + var list = new List(); + + var results = await FetchImages((BaseItem)item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false); + + if (results == null) + { + return list; + } + + var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + + var tmdbImageUrl = tmdbSettings.images.base_url + "original"; + + list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo + { + Url = tmdbImageUrl + i.file_path, + CommunityRating = i.vote_average, + VoteCount = i.vote_count, + Width = i.width, + Height = i.height, + Language = i.iso_639_1, + ProviderName = Name, + Type = ImageType.Primary, + RatingType = RatingType.Score + })); + + list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo + { + Url = tmdbImageUrl + i.file_path, + CommunityRating = i.vote_average, + VoteCount = i.vote_count, + Width = i.width, + Height = i.height, + ProviderName = Name, + Type = ImageType.Backdrop, + RatingType = RatingType.Score + })); + + var language = item.GetPreferredMetadataLanguage(); + + 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) + .ThenByDescending(i => i.VoteCount ?? 0) + .ToList(); + } + + /// + /// Gets the posters. + /// + /// The images. + private IEnumerable GetPosters(MovieDbSeriesProvider.Images images) + { + return images.posters ?? new List(); + } + + /// + /// Gets the backdrops. + /// + /// The images. + private IEnumerable GetBackdrops(MovieDbSeriesProvider.Images images) + { + var eligibleBackdrops = images.backdrops == null ? new List() : + images.backdrops + .ToList(); + + return eligibleBackdrops.OrderByDescending(i => i.vote_average) + .ThenByDescending(i => i.vote_count); + } + + /// + /// Fetches the images. + /// + /// The item. + /// The language. + /// The json serializer. + /// The cancellation token. + /// Task{MovieImages}. + private async Task FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, + CancellationToken cancellationToken) + { + var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); + + if (string.IsNullOrEmpty(tmdbId)) + { + return null; + } + + await MovieDbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + + var path = MovieDbSeriesProvider.Current.GetDataFilePath(tmdbId, language); + + if (!string.IsNullOrEmpty(path)) + { + var fileInfo = new FileInfo(path); + + if (fileInfo.Exists) + { + return jsonSerializer.DeserializeFromFile(path).images; + } + } + + return null; + } + + public int Order + { + get + { + // After tvdb and fanart + return 2; + } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = MovieDbProvider.Current.MovieDbResourcePool + }); + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) + { + return MovieDbSeriesProvider.Current.HasChanged(item, date); + } + } +} diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesProvider.cs new file mode 100644 index 0000000000..ad2cfa12b5 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesProvider.cs @@ -0,0 +1,615 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + public class MovieDbSeriesProvider : IRemoteMetadataProvider, IHasOrder + { + private const string GetTvInfo3 = @"http://api.themoviedb.org/3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos"; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + internal static MovieDbSeriesProvider Current { get; private set; } + + private readonly IJsonSerializer _jsonSerializer; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _configurationManager; + private readonly ILogger _logger; + private readonly ILocalizationManager _localization; + private readonly IHttpClient _httpClient; + private readonly ILibraryManager _libraryManager; + + public MovieDbSeriesProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization, IHttpClient httpClient, ILibraryManager libraryManager) + { + _jsonSerializer = jsonSerializer; + _fileSystem = fileSystem; + _configurationManager = configurationManager; + _logger = logger; + _localization = localization; + _httpClient = httpClient; + _libraryManager = libraryManager; + Current = this; + } + + public string Name + { + get { return "TheMovieDb"; } + } + + public async Task> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) + { + var tmdbId = searchInfo.GetProviderId(MetadataProviders.Tmdb); + + if (!string.IsNullOrEmpty(tmdbId)) + { + cancellationToken.ThrowIfCancellationRequested(); + + await EnsureSeriesInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage); + + var obj = _jsonSerializer.DeserializeFromFile(dataFilePath); + + var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbImageUrl = tmdbSettings.images.base_url + "original"; + + var remoteResult = new RemoteSearchResult + { + Name = obj.name, + SearchProviderName = Name, + ImageUrl = string.IsNullOrWhiteSpace(obj.poster_path) ? null : tmdbImageUrl + obj.poster_path + }; + + remoteResult.SetProviderId(MetadataProviders.Tmdb, obj.id.ToString(_usCulture)); + remoteResult.SetProviderId(MetadataProviders.Imdb, obj.external_ids.imdb_id); + + if (obj.external_ids.tvdb_id > 0) + { + remoteResult.SetProviderId(MetadataProviders.Tvdb, obj.external_ids.tvdb_id.ToString(_usCulture)); + } + + return new[] { remoteResult }; + } + + var imdbId = searchInfo.GetProviderId(MetadataProviders.Imdb); + + if (!string.IsNullOrEmpty(imdbId)) + { + var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false); + + if (searchResult != null) + { + return new[] { searchResult }; + } + } + + var tvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(tvdbId)) + { + var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false); + + if (searchResult != null) + { + return new[] { searchResult }; + } + } + + return await new MovieDbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); + } + + public async Task> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult(); + + var tmdbId = info.GetProviderId(MetadataProviders.Tmdb); + + if (string.IsNullOrEmpty(tmdbId)) + { + var imdbId = info.GetProviderId(MetadataProviders.Imdb); + + if (!string.IsNullOrEmpty(imdbId)) + { + var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false); + + if (searchResult != null) + { + tmdbId = searchResult.GetProviderId(MetadataProviders.Tmdb); + } + } + } + + if (string.IsNullOrEmpty(tmdbId)) + { + var tvdbId = info.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(tvdbId)) + { + var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false); + + if (searchResult != null) + { + tmdbId = searchResult.GetProviderId(MetadataProviders.Tmdb); + } + } + } + + if (string.IsNullOrEmpty(tmdbId)) + { + var searchResults = await new MovieDbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false); + + var searchResult = searchResults.FirstOrDefault(); + + if (searchResult != null) + { + tmdbId = searchResult.GetProviderId(MetadataProviders.Tmdb); + } + } + + if (!string.IsNullOrEmpty(tmdbId)) + { + cancellationToken.ThrowIfCancellationRequested(); + + result.Item = await FetchMovieData(tmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + result.HasMetadata = result.Item != null; + } + + return result; + } + + private async Task FetchMovieData(string tmdbId, string language, CancellationToken cancellationToken) + { + string dataFilePath = null; + RootObject seriesInfo = null; + + if (!string.IsNullOrEmpty(tmdbId)) + { + seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false); + } + + if (seriesInfo == null) + { + return null; + } + + tmdbId = seriesInfo.id.ToString(_usCulture); + + dataFilePath = GetDataFilePath(tmdbId, language); + _fileSystem.CreateDirectory(Path.GetDirectoryName(dataFilePath)); + _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath); + + await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + + var item = new Series(); + + ProcessMainInfo(item, seriesInfo); + + return item; + } + + private void ProcessMainInfo(Series series, RootObject seriesInfo) + { + series.Name = seriesInfo.name; + series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.id.ToString(_usCulture)); + + series.VoteCount = seriesInfo.vote_count; + + string voteAvg = seriesInfo.vote_average.ToString(CultureInfo.InvariantCulture); + float rating; + + if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out rating)) + { + series.CommunityRating = rating; + } + + series.Overview = seriesInfo.overview; + + if (seriesInfo.networks != null) + { + series.Studios = seriesInfo.networks.Select(i => i.name).ToList(); + } + + if (seriesInfo.genres != null) + { + series.Genres = seriesInfo.genres.Select(i => i.name).ToList(); + } + + series.HomePageUrl = seriesInfo.homepage; + + series.RunTimeTicks = seriesInfo.episode_run_time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); + + if (string.Equals(seriesInfo.status, "Ended", StringComparison.OrdinalIgnoreCase)) + { + series.Status = SeriesStatus.Ended; + series.EndDate = seriesInfo.last_air_date; + } + else + { + series.Status = SeriesStatus.Continuing; + } + + series.PremiereDate = seriesInfo.first_air_date; + + var ids = seriesInfo.external_ids; + if (ids != null) + { + if (!string.IsNullOrWhiteSpace(ids.imdb_id)) + { + series.SetProviderId(MetadataProviders.Imdb, ids.imdb_id); + } + if (ids.tvrage_id > 0) + { + series.SetProviderId(MetadataProviders.TvRage, ids.tvrage_id.ToString(_usCulture)); + } + if (ids.tvdb_id > 0) + { + series.SetProviderId(MetadataProviders.Tvdb, ids.tvdb_id.ToString(_usCulture)); + } + } + } + + internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId) + { + var dataPath = GetSeriesDataPath(appPaths); + + return Path.Combine(dataPath, tmdbId); + } + + internal static string GetSeriesDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.CachePath, "tmdb-tv"); + + return dataPath; + } + + internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + var mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + + if (mainResult == null) return; + + var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(dataFilePath)); + + _jsonSerializer.SerializeToFile(mainResult, dataFilePath); + } + + internal async Task FetchMainResult(string id, string language, CancellationToken cancellationToken) + { + var url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey); + + if (!string.IsNullOrEmpty(language)) + { + url += string.Format("&language={0}", MovieDbProvider.NormalizeLanguage(language)); + + // Get images in english and with no language + url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + } + + cancellationToken.ThrowIfCancellationRequested(); + + RootObject mainResult; + + using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + AcceptHeader = MovieDbProvider.AcceptHeader + + }).ConfigureAwait(false)) + { + mainResult = _jsonSerializer.DeserializeFromStream(json); + } + + cancellationToken.ThrowIfCancellationRequested(); + + // If the language preference isn't english, then have the overview fallback to english if it's blank + if (mainResult != null && + string.IsNullOrEmpty(mainResult.overview) && + !string.IsNullOrEmpty(language) && + !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) + { + _logger.Info("MovieDbSeriesProvider couldn't find meta for language " + language + ". Trying English..."); + + url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey) + "&language=en"; + + if (!string.IsNullOrEmpty(language)) + { + // Get images in english and with no language + url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + } + + using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + AcceptHeader = MovieDbProvider.AcceptHeader + + }).ConfigureAwait(false)) + { + var englishResult = _jsonSerializer.DeserializeFromStream(json); + + mainResult.overview = englishResult.overview; + } + } + + return mainResult; + } + + private readonly Task _cachedTask = Task.FromResult(true); + internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(tmdbId)) + { + throw new ArgumentNullException("tmdbId"); + } + + var path = GetDataFilePath(tmdbId, language); + + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + if (fileInfo.Exists) + { + // If it's recent or automatic updates are enabled, don't re-download + if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3) + { + return _cachedTask; + } + } + + return DownloadSeriesInfo(tmdbId, language, cancellationToken); + } + + internal string GetDataFilePath(string tmdbId, string preferredLanguage) + { + if (string.IsNullOrEmpty(tmdbId)) + { + throw new ArgumentNullException("tmdbId"); + } + + var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); + + var filename = string.Format("series-{0}.json", preferredLanguage ?? string.Empty); + + return Path.Combine(path, filename); + } + + public bool HasChanged(IHasMetadata item, DateTime date) + { + if (!MovieDbProvider.Current.GetTheMovieDbOptions().EnableAutomaticUpdates) + { + return false; + } + + var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); + + if (!String.IsNullOrEmpty(tmdbId)) + { + // Process images + var dataFilePath = GetDataFilePath(tmdbId, item.GetPreferredMetadataLanguage()); + + var fileInfo = _fileSystem.GetFileInfo(dataFilePath); + + return !fileInfo.Exists || _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; + } + + return false; + } + + private async Task FindByExternalId(string id, string externalSource, CancellationToken cancellationToken) + { + var url = string.Format("http://api.themoviedb.org/3/tv/find/{0}?api_key={1}&external_source={2}", + id, + MovieDbProvider.ApiKey, + externalSource); + + using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + AcceptHeader = MovieDbProvider.AcceptHeader + + }).ConfigureAwait(false)) + { + var result = _jsonSerializer.DeserializeFromStream(json); + + if (result != null && result.tv_results != null) + { + var tv = result.tv_results.FirstOrDefault(); + + if (tv != null) + { + var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbImageUrl = tmdbSettings.images.base_url + "original"; + + var remoteResult = new RemoteSearchResult + { + Name = tv.name, + SearchProviderName = Name, + ImageUrl = string.IsNullOrWhiteSpace(tv.poster_path) ? null : tmdbImageUrl + tv.poster_path + }; + + remoteResult.SetProviderId(MetadataProviders.Tmdb, tv.id.ToString(_usCulture)); + + return remoteResult; + } + } + } + + return null; + } + + public class CreatedBy + { + public int id { get; set; } + public string name { get; set; } + public string profile_path { get; set; } + } + + public class Genre + { + public int id { get; set; } + public string name { get; set; } + } + + public class Network + { + public int id { get; set; } + public string name { get; set; } + } + + public class Season + { + public string air_date { get; set; } + public int id { get; set; } + public string poster_path { get; set; } + public int season_number { get; set; } + } + + public class Cast + { + public string character { get; set; } + public string credit_id { get; set; } + public int id { get; set; } + public string name { get; set; } + public string profile_path { get; set; } + public int order { get; set; } + } + + public class Crew + { + public string credit_id { get; set; } + public string department { get; set; } + public int id { get; set; } + public string name { get; set; } + public string job { get; set; } + public string profile_path { get; set; } + } + + public class Credits + { + public List cast { get; set; } + public List crew { get; set; } + } + + public class Backdrop + { + public double aspect_ratio { get; set; } + public string file_path { get; set; } + public int height { get; set; } + public string iso_639_1 { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public int width { get; set; } + } + + public class Poster + { + public double aspect_ratio { get; set; } + public string file_path { get; set; } + public int height { get; set; } + public string id { get; set; } + public string iso_639_1 { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public int width { get; set; } + } + + public class Images + { + public List backdrops { get; set; } + public List posters { get; set; } + } + + public class Keywords + { + public List results { get; set; } + } + + public class ExternalIds + { + public string imdb_id { get; set; } + public string freebase_id { get; set; } + public string freebase_mid { get; set; } + public int tvdb_id { get; set; } + public int tvrage_id { get; set; } + } + + public class Videos + { + public List results { get; set; } + } + + public class RootObject + { + public string backdrop_path { get; set; } + public List created_by { get; set; } + public List episode_run_time { get; set; } + public DateTime first_air_date { get; set; } + public List genres { get; set; } + public string homepage { get; set; } + public int id { get; set; } + public bool in_production { get; set; } + public List languages { get; set; } + public DateTime last_air_date { get; set; } + public string name { get; set; } + public List networks { get; set; } + public int number_of_episodes { get; set; } + public int number_of_seasons { get; set; } + public string original_name { get; set; } + public List origin_country { get; set; } + public string overview { get; set; } + public string popularity { get; set; } + public string poster_path { get; set; } + public List seasons { get; set; } + public string status { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public Credits credits { get; set; } + public Images images { get; set; } + public Keywords keywords { get; set; } + public ExternalIds external_ids { get; set; } + public Videos videos { get; set; } + } + + public int Order + { + get + { + // After Omdb and Tvdb + return 2; + } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = MovieDbProvider.Current.MovieDbResourcePool + }); + } + } +} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs new file mode 100644 index 0000000000..50ecc6bbfd --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs @@ -0,0 +1,209 @@ +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.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; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + public class TvdbEpisodeImageProvider : IRemoteImageProvider, IHasChangeMonitor + { + private readonly IServerConfigurationManager _config; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + + public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + } + + public string Name + { + get { return "TheTVDB"; } + } + + public bool Supports(IHasImages item) + { + return item is Episode; + } + + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary + }; + } + + public Task> GetImages(IHasImages item, CancellationToken cancellationToken) + { + var episode = (Episode)item; + var series = episode.Series; + + if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) + { + // Process images + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.ProviderIds); + var indexOffset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds) ?? 0; + + var nodes = TvdbEpisodeProvider.Current.GetEpisodeXmlNodes(seriesDataPath, episode.GetLookupInfo()); + + var result = nodes.Select(i => GetImageInfo(i, cancellationToken)) + .Where(i => i != null) + .ToList(); + + return Task.FromResult>(result); + } + + return Task.FromResult>(new RemoteImageInfo[] { }); + } + + private RemoteImageInfo GetImageInfo(XmlReader reader, CancellationToken cancellationToken) + { + var height = 225; + var width = 400; + var url = string.Empty; + + // Use XmlReader for best performance + using (reader) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "thumb_width": + { + 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)) + { + width = rval; + } + } + break; + } + + case "thumb_height": + { + 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)) + { + height = rval; + } + } + break; + } + + case "filename": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + url = TVUtils.BannerUrl + val; + } + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + + if (string.IsNullOrEmpty(url)) + { + return null; + } + + return new RemoteImageInfo + { + Width = width, + Height = height, + ProviderName = Name, + Url = url, + Type = ImageType.Primary + }; + } + + public int Order + { + get { return 0; } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool + }); + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) + { + var episode = (Episode)item; + + if (!episode.IsVirtualUnaired) + { + // For non-unaired items, only enable if configured + if (!TvdbSeriesProvider.Current.GetTvDbOptions().EnableAutomaticUpdates) + { + return false; + } + } + + if (!item.HasImage(ImageType.Primary)) + { + var series = episode.Series; + + if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) + { + // Process images + var seriesXmlPath = TvdbSeriesProvider.Current.GetSeriesXmlPath(series.ProviderIds, series.GetPreferredMetadataLanguage()); + + return _fileSystem.GetLastWriteTimeUtc(seriesXmlPath) > date; + } + } + + return false; + } + } +} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs new file mode 100644 index 0000000000..3920330489 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs @@ -0,0 +1,978 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +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.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + + /// + /// Class RemoteEpisodeProvider + /// + class TvdbEpisodeProvider : IRemoteMetadataProvider, IItemIdentityProvider, IHasChangeMonitor + { + private static readonly string FullIdKey = MetadataProviders.Tvdb + "-Full"; + + internal static TvdbEpisodeProvider Current; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly ILogger _logger; + + public TvdbEpisodeProvider(IFileSystem fileSystem, IServerConfigurationManager config, IHttpClient httpClient, ILogger logger) + { + _fileSystem = fileSystem; + _config = config; + _httpClient = httpClient; + _logger = logger; + Current = this; + } + + public async Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + var list = new List(); + + // The search query must either provide an episode number or date + if (!searchInfo.IndexNumber.HasValue && !searchInfo.PremiereDate.HasValue) + { + return list; + } + + if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) + { + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, searchInfo.SeriesProviderIds); + + var searchNumbers = new EpisodeNumbers(); + + if (searchInfo.IndexNumber.HasValue) { + searchNumbers.EpisodeNumber = searchInfo.IndexNumber.Value; + } + + searchNumbers.SeasonNumber = searchInfo.ParentIndexNumber; + searchNumbers.EpisodeNumberEnd = searchInfo.IndexNumberEnd ?? searchNumbers.EpisodeNumber; + + try + { + var metadataResult = FetchEpisodeData(searchInfo, searchNumbers, seriesDataPath, cancellationToken); + + if (metadataResult.HasMetadata) + { + var item = metadataResult.Item; + + list.Add(new RemoteSearchResult + { + IndexNumber = item.IndexNumber, + Name = item.Name, + ParentIndexNumber = item.ParentIndexNumber, + PremiereDate = item.PremiereDate, + ProductionYear = item.ProductionYear, + ProviderIds = item.ProviderIds, + SearchProviderName = Name, + IndexNumberEnd = item.IndexNumberEnd + }); + } + } + catch (FileNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + catch (DirectoryNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + } + + return list; + } + + public string Name + { + get { return "TheTVDB"; } + } + + public async Task> GetMetadata(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + var result = new MetadataResult(); + + if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && + (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue)) + { + await TvdbSeriesProvider.Current.EnsureSeriesInfo(searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, searchInfo.SeriesProviderIds); + + var searchNumbers = new EpisodeNumbers(); + if (searchInfo.IndexNumber.HasValue) { + searchNumbers.EpisodeNumber = searchInfo.IndexNumber.Value; + } + searchNumbers.SeasonNumber = searchInfo.ParentIndexNumber; + searchNumbers.EpisodeNumberEnd = searchInfo.IndexNumberEnd ?? searchNumbers.EpisodeNumber; + + try + { + result = FetchEpisodeData(searchInfo, searchNumbers, seriesDataPath, cancellationToken); + } + catch (FileNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + catch (DirectoryNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. + } + } + else + { + _logger.Debug("No series identity found for {0}", searchInfo.Name); + } + + return result; + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) + { + // Only enable for virtual items + if (item.LocationType != LocationType.Virtual) + { + return false; + } + + var episode = (Episode)item; + var series = episode.Series; + + if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) + { + // Process images + var seriesXmlPath = TvdbSeriesProvider.Current.GetSeriesXmlPath(series.ProviderIds, series.GetPreferredMetadataLanguage()); + + return _fileSystem.GetLastWriteTimeUtc(seriesXmlPath) > date; + } + + return false; + } + + /// + /// Gets the episode XML files. + /// + /// The series data path. + /// The search information. + /// List{FileInfo}. + internal List GetEpisodeXmlNodes(string seriesDataPath, EpisodeInfo searchInfo) + { + var seriesXmlPath = TvdbSeriesProvider.Current.GetSeriesXmlPath (searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage); + + try + { + return GetXmlNodes(seriesXmlPath, searchInfo); + } + catch (DirectoryNotFoundException) + { + return new List (); + } + catch (FileNotFoundException) + { + return new List (); + } + } + + private class EpisodeNumbers + { + public int EpisodeNumber; + public int? SeasonNumber; + public int EpisodeNumberEnd; + } + + /// + /// Fetches the episode data. + /// + /// The identifier. + /// The search numbers. + /// The series data path. + /// The cancellation token. + /// Task{System.Boolean}. + private MetadataResult FetchEpisodeData(EpisodeInfo id, EpisodeNumbers searchNumbers, string seriesDataPath, CancellationToken cancellationToken) + { + var result = new MetadataResult() + { + Item = new Episode + { + IndexNumber = id.IndexNumber, + ParentIndexNumber = id.ParentIndexNumber, + IndexNumberEnd = id.IndexNumberEnd + } + }; + + var xmlNodes = GetEpisodeXmlNodes (seriesDataPath, id); + + if (xmlNodes.Count > 0) { + FetchMainEpisodeInfo(result, xmlNodes[0], cancellationToken); + + result.HasMetadata = true; + } + + foreach (var node in xmlNodes.Skip(1)) { + FetchAdditionalPartInfo(result, node, cancellationToken); + } + + return result; + } + + private List GetXmlNodes(string xmlFile, EpisodeInfo searchInfo) + { + var list = new List (); + + if (searchInfo.IndexNumber.HasValue) + { + var files = GetEpisodeXmlFiles (searchInfo.ParentIndexNumber, searchInfo.IndexNumber, searchInfo.IndexNumberEnd, Path.GetDirectoryName (xmlFile)); + + list = files.Select (GetXmlReader).ToList (); + } + + if (list.Count == 0 && searchInfo.PremiereDate.HasValue) { + list = GetXmlNodesByPremiereDate (xmlFile, searchInfo.PremiereDate.Value); + } + + return list; + } + + private List GetEpisodeXmlFiles(int? seasonNumber, int? episodeNumber, int? endingEpisodeNumber, string seriesDataPath) + { + var files = new List(); + + if (episodeNumber == null) + { + return files; + } + + if (seasonNumber == null) + { + return files; + } + + var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); + + var fileInfo = _fileSystem.GetFileInfo(file); + var usingAbsoluteData = false; + + if (fileInfo.Exists) + { + files.Add(fileInfo); + } + else + { + file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); + fileInfo = _fileSystem.GetFileInfo(file); + if (fileInfo.Exists) + { + files.Add(fileInfo); + usingAbsoluteData = true; + } + } + + var end = endingEpisodeNumber ?? 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 = _fileSystem.GetFileInfo(file); + if (fileInfo.Exists) + { + files.Add(fileInfo); + } + else + { + break; + } + + episodeNumber++; + } + + return files; + } + + private XmlReader GetXmlReader(FileSystemMetadata xmlFile) + { + return GetXmlReader (_fileSystem.ReadAllText(xmlFile.FullName, Encoding.UTF8)); + } + + private XmlReader GetXmlReader(String xml) + { + var streamReader = new StringReader (xml); + + return XmlReader.Create (streamReader, new XmlReaderSettings { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }); + } + + private List GetXmlNodesByPremiereDate(string xmlFile, DateTime premiereDate) + { + var list = new List (); + + 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()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Episode": + { + var outerXml = reader.ReadOuterXml(); + + var airDate = GetEpisodeAirDate (outerXml); + + if (airDate.HasValue && premiereDate.Date == airDate.Value.Date) + { + list.Add (GetXmlReader(outerXml)); + return list; + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + } + + return list; + } + + private DateTime? GetEpisodeAirDate(string xml) + { + using (var streamReader = new StringReader (xml)) + { + // 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 ()) { + + 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)) { + date = date.ToUniversalTime (); + + return date; + } + } + + break; + } + + default: + reader.Skip (); + break; + } + } + } + } + } + return null; + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private void FetchMainEpisodeInfo(MetadataResult result, XmlReader reader, CancellationToken cancellationToken) + { + var item = result.Item; + + // Use XmlReader for best performance + using (reader) + { + reader.MoveToContent(); + + result.ResetPeople(); + + // 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 "DVD_episodenumber": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + float num; + + if (float.TryParse(val, NumberStyles.Any, _usCulture, out num)) + { + item.DvdEpisodeNumber = num; + } + } + + break; + } + + case "DVD_season": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + float num; + + if (float.TryParse(val, NumberStyles.Any, _usCulture, out num)) + { + item.DvdSeasonNumber = Convert.ToInt32(num); + } + } + + break; + } + + case "EpisodeNumber": + { + if (!item.IndexNumber.HasValue) + { + 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.IndexNumber = rval; + } + } + } + + break; + } + + case "SeasonNumber": + { + if (!item.ParentIndexNumber.HasValue) + { + 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.ParentIndexNumber = rval; + } + } + } + + break; + } + + case "absolute_number": + { + 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.AbsoluteEpisodeNumber = rval; + } + } + + break; + } + + case "airsbefore_episode": + { + 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.AirsBeforeEpisodeNumber = rval; + } + } + + break; + } + + case "airsafter_season": + { + 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.AirsAfterSeasonNumber = rval; + } + } + + break; + } + + case "airsbefore_season": + { + 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.AirsBeforeSeasonNumber = rval; + } + } + + break; + } + + 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 = 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(result, val, PersonType.Director); + } + } + + break; + } + case "GuestStars": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddGuestStars(result, val); + } + } + + break; + } + case "Writer": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(result, val, PersonType.Writer); + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + } + + private void AddPeople(MetadataResult result, 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() })) + { + result.AddPerson(person); + } + } + + private void AddGuestStars(MetadataResult result, string val) + where T : BaseItem + { + // 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 }; + })) + { + if (!string.IsNullOrWhiteSpace(person.Name)) + { + result.AddPerson(person); + } + } + } + + private void FetchAdditionalPartInfo(MetadataResult result, XmlReader reader, CancellationToken cancellationToken) + { + var item = result.Item; + + // Use XmlReader for best performance + using (reader) + { + 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(result, val, PersonType.Director); + } + } + + break; + } + case "GuestStars": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddGuestStars(result, val); + } + } + + break; + } + case "Writer": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(result, val, PersonType.Writer); + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool + }); + } + + public Task Identify(EpisodeInfo info) + { + if (info.ProviderIds.ContainsKey(FullIdKey)) + { + return Task.FromResult(null); + } + + string seriesTvdbId; + info.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesTvdbId); + + if (string.IsNullOrEmpty(seriesTvdbId) || info.IndexNumber == null) + { + return Task.FromResult(null); + } + + var id = new Identity(seriesTvdbId, info.ParentIndexNumber, info.IndexNumber.Value, info.IndexNumberEnd); + info.SetProviderId(FullIdKey, id.ToString()); + + return Task.FromResult(id); + } + + public int Order { get { return 0; } } + + public struct Identity + { + public string SeriesId { get; private set; } + public int? SeasonIndex { get; private set; } + public int EpisodeNumber { get; private set; } + public int? EpisodeNumberEnd { get; private set; } + + public Identity(string id) + : this() + { + this = ParseIdentity(id).Value; + } + + public Identity(string seriesId, int? seasonIndex, int episodeNumber, int? episodeNumberEnd) + : this() + { + SeriesId = seriesId; + SeasonIndex = seasonIndex; + EpisodeNumber = episodeNumber; + EpisodeNumberEnd = episodeNumberEnd; + } + + public override string ToString() + { + return string.Format("{0}:{1}:{2}", + SeriesId, + SeasonIndex != null ? SeasonIndex.Value.ToString() : "A", + EpisodeNumber + (EpisodeNumberEnd != null ? "-" + EpisodeNumberEnd.Value.ToString() : "")); + } + + public static Identity? ParseIdentity(string id) + { + if (string.IsNullOrEmpty(id)) + return null; + + try { + var parts = id.Split(':'); + var series = parts[0]; + var season = parts[1] != "A" ? (int?)int.Parse(parts[1]) : null; + + int index; + int? indexEnd; + + if (parts[2].Contains("-")) { + var split = parts[2].IndexOf("-", StringComparison.OrdinalIgnoreCase); + index = int.Parse(parts[2].Substring(0, split)); + indexEnd = int.Parse(parts[2].Substring(split + 1)); + } else { + index = int.Parse(parts[2]); + indexEnd = null; + } + + return new Identity(series, season, index, indexEnd); + } catch { + return null; + } + } + } + } +} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs new file mode 100644 index 0000000000..d362ca722d --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs @@ -0,0 +1,366 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +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.Threading; +using System.Threading.Tasks; +using System.Xml; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + /// + /// Class TvdbPrescanTask + /// + public class TvdbPrescanTask : ILibraryPostScanTask + { + /// + /// The server time URL + /// + private const string ServerTimeUrl = "http://thetvdb.com/api/Updates.php?type=none"; + + /// + /// The updates URL + /// + private const string UpdatesUrl = "http://thetvdb.com/api/Updates.php?type=all&time={0}"; + + /// + /// The _HTTP client + /// + private readonly IHttpClient _httpClient; + /// + /// The _logger + /// + private readonly ILogger _logger; + /// + /// The _config + /// + private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private readonly ILibraryManager _libraryManager; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The HTTP client. + /// The config. + public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IFileSystem fileSystem, ILibraryManager libraryManager) + { + _logger = logger; + _httpClient = httpClient; + _config = config; + _fileSystem = fileSystem; + _libraryManager = libraryManager; + } + + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + if (!_config.Configuration.EnableInternetProviders) + { + progress.Report(100); + return; + } + + var seriesConfig = _config.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, typeof(Series).Name, StringComparison.OrdinalIgnoreCase)); + + if (seriesConfig != null && seriesConfig.DisabledMetadataFetchers.Contains(TvdbSeriesProvider.Current.Name, StringComparer.OrdinalIgnoreCase)) + { + progress.Report(100); + return; + } + + var path = TvdbSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); + + _fileSystem.CreateDirectory(path); + + var timestampFile = Path.Combine(path, "time.txt"); + + var timestampFileInfo = _fileSystem.GetFileInfo(timestampFile); + + // Don't check for tvdb updates anymore frequently than 24 hours + if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 1) + { + return; + } + + // Find out the last time we queried tvdb for updates + var lastUpdateTime = timestampFileInfo.Exists ? _fileSystem.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; + + string newUpdateTime; + + var existingDirectories = Directory.EnumerateDirectories(path) + .Select(Path.GetFileName) + .ToList(); + + var seriesIdsInLibrary = _libraryManager.RootFolder + .GetRecursiveChildren(i => i is Series && !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) + .Cast() + .Select(i => i.GetProviderId(MetadataProviders.Tvdb)) + .ToList(); + + var missingSeries = seriesIdsInLibrary.Except(existingDirectories, StringComparer.OrdinalIgnoreCase) + .ToList(); + + // If this is our first time, update all series + if (string.IsNullOrEmpty(lastUpdateTime)) + { + // First get tvdb server time + using (var stream = await _httpClient.Get(new HttpRequestOptions + { + Url = ServerTimeUrl, + CancellationToken = cancellationToken, + EnableHttpCompression = true, + ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool + + }).ConfigureAwait(false)) + { + newUpdateTime = GetUpdateTime(stream); + } + + existingDirectories.AddRange(missingSeries); + + await UpdateSeries(existingDirectories, path, null, progress, cancellationToken).ConfigureAwait(false); + } + else + { + var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false); + + newUpdateTime = seriesToUpdate.Item2; + + long lastUpdateValue; + + long.TryParse(lastUpdateTime, NumberStyles.Any, UsCulture, out lastUpdateValue); + + var nullableUpdateValue = lastUpdateValue == 0 ? (long?)null : lastUpdateValue; + + var listToUpdate = seriesToUpdate.Item1.ToList(); + listToUpdate.AddRange(missingSeries); + + await UpdateSeries(listToUpdate, path, nullableUpdateValue, progress, cancellationToken).ConfigureAwait(false); + } + + _fileSystem.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8); + progress.Report(100); + } + + /// + /// Gets the update time. + /// + /// The response. + /// System.String. + private string GetUpdateTime(Stream response) + { + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + using (var streamReader = new StreamReader(response, 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 "Time": + { + return (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + } + default: + reader.Skip(); + break; + } + } + } + } + } + + return null; + } + + /// + /// Gets the series ids to update. + /// + /// The existing series ids. + /// The last update time. + /// The cancellation token. + /// Task{IEnumerable{System.String}}. + private async Task, string>> GetSeriesIdsToUpdate(IEnumerable existingSeriesIds, string lastUpdateTime, CancellationToken cancellationToken) + { + // First get last time + using (var stream = await _httpClient.Get(new HttpRequestOptions + { + Url = string.Format(UpdatesUrl, lastUpdateTime), + CancellationToken = cancellationToken, + EnableHttpCompression = true, + ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool + + }).ConfigureAwait(false)) + { + var data = GetUpdatedSeriesIdList(stream); + + var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + + var seriesList = data.Item1 + .Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); + + return new Tuple, string>(seriesList, data.Item2); + } + } + + private Tuple, string> GetUpdatedSeriesIdList(Stream stream) + { + string updateTime = null; + var idList = new List(); + + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + using (var streamReader = new StreamReader(stream, 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 "Time": + { + updateTime = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + case "Series": + { + var id = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + idList.Add(id); + break; + } + default: + reader.Skip(); + break; + } + } + } + } + } + + return new Tuple, string>(idList, updateTime); + } + + /// + /// Updates the series. + /// + /// The series ids. + /// The series data path. + /// The last tv db update time. + /// The progress. + /// The cancellation token. + /// Task. + private async Task UpdateSeries(IEnumerable seriesIds, string seriesDataPath, long? lastTvDbUpdateTime, IProgress progress, CancellationToken cancellationToken) + { + var list = seriesIds.ToList(); + var numComplete = 0; + + // Gather all series into a lookup by tvdb id + var allSeries = _libraryManager.RootFolder + .GetRecursiveChildren(i => i is Series && !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) + .Cast() + .ToLookup(i => i.GetProviderId(MetadataProviders.Tvdb)); + + foreach (var seriesId in list) + { + // Find the preferred language(s) for the movie in the library + var languages = allSeries[seriesId] + .Select(i => i.GetPreferredMetadataLanguage()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var language in languages) + { + try + { + await UpdateSeries(seriesId, seriesDataPath, lastTvDbUpdateTime, language, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + _logger.ErrorException("Error updating tvdb series id {0}, language {1}", ex, seriesId, language); + + // Already logged at lower levels, but don't fail the whole operation, unless timed out + // We have to fail this to make it run again otherwise new episode data could potentially be missing + if (ex.IsTimedOut) + { + throw; + } + } + } + + numComplete++; + double percent = numComplete; + percent /= list.Count; + percent *= 100; + + progress.Report(percent); + } + } + + /// + /// Updates the series. + /// + /// The id. + /// The series data path. + /// The last tv db update time. + /// The preferred metadata language. + /// The cancellation token. + /// Task. + private Task UpdateSeries(string id, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + _logger.Info("Updating series from tvdb " + id + ", language " + preferredMetadataLanguage); + + seriesDataPath = Path.Combine(seriesDataPath, id); + + _fileSystem.CreateDirectory(seriesDataPath); + + return TvdbSeriesProvider.Current.DownloadSeriesZip(id, MetadataProviders.Tvdb.ToString(), seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonIdentityProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonIdentityProvider.cs new file mode 100644 index 0000000000..4198430c9f --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonIdentityProvider.cs @@ -0,0 +1,65 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.TV +{ + public class TvdbSeasonIdentityProvider : IItemIdentityProvider + { + public static readonly string FullIdKey = MetadataProviders.Tvdb + "-Full"; + + public Task Identify(SeasonInfo info) + { + string tvdbSeriesId; + if (!info.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out tvdbSeriesId) || string.IsNullOrEmpty(tvdbSeriesId) || info.IndexNumber == null) + { + return Task.FromResult(null); + } + + if (string.IsNullOrEmpty(info.GetProviderId(FullIdKey))) + { + var id = string.Format("{0}:{1}", tvdbSeriesId, info.IndexNumber.Value); + info.SetProviderId(FullIdKey, id); + } + + return Task.FromResult(null); + } + + public static TvdbSeasonIdentity? ParseIdentity(string id) + { + if (id == null) + { + return null; + } + + try + { + var parts = id.Split(':'); + return new TvdbSeasonIdentity(parts[0], int.Parse(parts[1])); + } + catch + { + return null; + } + } + } + + public struct TvdbSeasonIdentity + { + public string SeriesId { get; private set; } + public int Index { get; private set; } + + public TvdbSeasonIdentity(string id) + : this() + { + this = TvdbSeasonIdentityProvider.ParseIdentity(id).Value; + } + + public TvdbSeasonIdentity(string seriesId, int index) + : this() + { + SeriesId = seriesId; + Index = index; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs new file mode 100644 index 0000000000..7af85ecc93 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs @@ -0,0 +1,391 @@ +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.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; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor + { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + + public TvdbSeasonImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "TheTVDB"; } + } + + public bool Supports(IHasImages item) + { + return item is Season; + } + + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary, + ImageType.Banner, + ImageType.Backdrop + }; + } + + public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) + { + var season = (Season)item; + var series = season.Series; + + if (series != null && season.IndexNumber.HasValue && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) + { + var seriesProviderIds = series.ProviderIds; + var seasonNumber = season.IndexNumber.Value; + + var identity = TvdbSeasonIdentityProvider.ParseIdentity(season.GetProviderId(TvdbSeasonIdentityProvider.FullIdKey)); + if (identity == null) + { + identity = new TvdbSeasonIdentity(series.GetProviderId(MetadataProviders.Tvdb), seasonNumber); + } + + if (identity != null) + { + var id = identity.Value; + seasonNumber = AdjustForSeriesOffset(series, id.Index); + + seriesProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + seriesProviderIds[MetadataProviders.Tvdb.ToString()] = id.SeriesId; + } + + var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(seriesProviderIds, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); + + var path = Path.Combine(seriesDataPath, "banners.xml"); + + try + { + return GetImages(path, item.GetPreferredMetadataLanguage(), seasonNumber, cancellationToken); + } + catch (FileNotFoundException) + { + // No tvdb data yet. Don't blow up + } + catch (DirectoryNotFoundException) + { + // No tvdb data yet. Don't blow up + } + } + + return new RemoteImageInfo[] { }; + } + + private int AdjustForSeriesOffset(Series series, int seasonNumber) + { + var offset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds); + if (offset != null) + return (seasonNumber + offset.Value); + + return seasonNumber; + } + + internal static IEnumerable GetImages(string xmlPath, string preferredLanguage, int seasonNumber, 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, seasonNumber); + } + break; + } + default: + reader.Skip(); + break; + } + } + } + } + } + + var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); + + return list.OrderByDescending(i => + { + if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0) + .ToList(); + } + + private static void AddImage(XmlReader reader, List images, int seasonNumber) + { + reader.MoveToContent(); + + string bannerType = null; + string bannerType2 = 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 "BannerType2": + { + 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 "BannerPath": + { + url = reader.ReadElementContentAsString() ?? string.Empty; + 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 && bannerSeason.Value == seasonNumber) + { + var imageInfo = new RemoteImageInfo + { + RatingType = RatingType.Score, + CommunityRating = rating, + VoteCount = voteCount, + Url = TVUtils.BannerUrl + url, + ProviderName = ProviderName, + Language = language, + Width = width, + 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)) + { + imageInfo.Type = ImageType.Primary; + images.Add(imageInfo); + } + else if (string.Equals(bannerType2, "seasonwide", StringComparison.OrdinalIgnoreCase)) + { + imageInfo.Type = ImageType.Banner; + images.Add(imageInfo); + } + } + else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase)) + { + imageInfo.Type = ImageType.Backdrop; + images.Add(imageInfo); + } + } + + } + + public int Order + { + get { return 0; } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool + }); + } + + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) + { + if (item.LocationType != LocationType.Virtual) + { + // For non-virtual items, only enable if configured + if (!TvdbSeriesProvider.Current.GetTvDbOptions().EnableAutomaticUpdates) + { + return false; + } + } + + var season = (Season)item; + var series = season.Series; + + if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) + { + // Process images + var imagesXmlPath = Path.Combine(TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.ProviderIds), "banners.xml"); + + var fileInfo = _fileSystem.GetFileInfo(imagesXmlPath); + + return fileInfo.Exists && _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; + } + + return false; + } + } +} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs new file mode 100644 index 0000000000..eae389dfb0 --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs @@ -0,0 +1,356 @@ +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.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; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder, IHasItemChangeMonitor + { + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IFileSystem _fileSystem; + + public TvdbSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) + { + _config = config; + _httpClient = httpClient; + _fileSystem = fileSystem; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "TheTVDB"; } + } + + public bool Supports(IHasImages item) + { + return item is Series; + } + + public IEnumerable GetSupportedImages(IHasImages item) + { + return new List + { + ImageType.Primary, + ImageType.Banner, + ImageType.Backdrop + }; + } + + public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) + { + if (TvdbSeriesProvider.IsValidSeries(item.ProviderIds)) + { + var language = item.GetPreferredMetadataLanguage(); + + var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(item.ProviderIds, language, cancellationToken).ConfigureAwait(false); + + var path = Path.Combine(seriesDataPath, "banners.xml"); + + try + { + var seriesOffset = TvdbSeriesProvider.GetSeriesOffset(item.ProviderIds); + if (seriesOffset != null && seriesOffset.Value != 0) + return TvdbSeasonImageProvider.GetImages(path, language, seriesOffset.Value + 1, cancellationToken); + + return GetImages(path, language, cancellationToken); + } + catch (FileNotFoundException) + { + // No tvdb data yet. Don't blow up + } + catch (DirectoryNotFoundException) + { + // No tvdb data yet. Don't blow up + } + } + + return new RemoteImageInfo[] { }; + } + + private IEnumerable GetImages(string xmlPath, string preferredLanguage, CancellationToken cancellationToken) + { + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + var list = new List(); + + using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8)) + { + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, settings)) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Banner": + { + using (var subtree = reader.ReadSubtree()) + { + AddImage(subtree, list); + } + break; + } + default: + reader.Skip(); + break; + } + } + } + } + } + + var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); + + return list.OrderByDescending(i => + { + if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => i.VoteCount ?? 0) + .ToList(); + } + + private void AddImage(XmlReader reader, List images) + { + reader.MoveToContent(); + + string bannerType = null; + string url = null; + int? bannerSeason = null; + int? width = null; + int? height = null; + string language = null; + double? rating = null; + int? voteCount = null; + string thumbnailUrl = null; + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Rating": + { + var val = reader.ReadElementContentAsString() ?? string.Empty; + + double rval; + + if (double.TryParse(val, NumberStyles.Any, _usCulture, out rval)) + { + rating = rval; + } + + break; + } + + case "RatingCount": + { + var val = reader.ReadElementContentAsString() ?? string.Empty; + + int rval; + + if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval)) + { + voteCount = rval; + } + + break; + } + + case "Language": + { + language = reader.ReadElementContentAsString() ?? string.Empty; + break; + } + + case "ThumbnailPath": + { + thumbnailUrl = reader.ReadElementContentAsString() ?? string.Empty; + break; + } + + case "BannerType": + { + bannerType = reader.ReadElementContentAsString() ?? string.Empty; + + break; + } + + case "BannerPath": + { + url = reader.ReadElementContentAsString() ?? string.Empty; + break; + } + + case "BannerType2": + { + var bannerType2 = reader.ReadElementContentAsString() ?? string.Empty; + + // Sometimes the resolution is stuffed in here + var resolutionParts = bannerType2.Split('x'); + + if (resolutionParts.Length == 2) + { + int rval; + + if (int.TryParse(resolutionParts[0], NumberStyles.Integer, _usCulture, out rval)) + { + width = rval; + } + + if (int.TryParse(resolutionParts[1], NumberStyles.Integer, _usCulture, out rval)) + { + height = rval; + } + + } + + break; + } + + case "Season": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + bannerSeason = int.Parse(val); + } + break; + } + + + default: + reader.Skip(); + break; + } + } + } + + if (!string.IsNullOrEmpty(url) && !bannerSeason.HasValue) + { + var imageInfo = new RemoteImageInfo + { + RatingType = RatingType.Score, + CommunityRating = rating, + VoteCount = voteCount, + Url = TVUtils.BannerUrl + url, + ProviderName = Name, + Language = language, + Width = width, + Height = height + }; + + if (!string.IsNullOrEmpty(thumbnailUrl)) + { + imageInfo.ThumbnailUrl = TVUtils.BannerUrl + thumbnailUrl; + } + + if (string.Equals(bannerType, "poster", StringComparison.OrdinalIgnoreCase)) + { + imageInfo.Type = ImageType.Primary; + images.Add(imageInfo); + } + else if (string.Equals(bannerType, "series", StringComparison.OrdinalIgnoreCase)) + { + imageInfo.Type = ImageType.Banner; + images.Add(imageInfo); + } + else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase)) + { + imageInfo.Type = ImageType.Backdrop; + images.Add(imageInfo); + } + } + + } + + public int Order + { + get { return 0; } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool + }); + } + + public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) + { + if (!TvdbSeriesProvider.Current.GetTvDbOptions().EnableAutomaticUpdates) + { + return false; + } + + if (TvdbSeriesProvider.IsValidSeries(item.ProviderIds)) + { + // Process images + var imagesXmlPath = Path.Combine(TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, item.ProviderIds), "banners.xml"); + + var fileInfo = _fileSystem.GetFileInfo(imagesXmlPath); + + return fileInfo.Exists && _fileSystem.GetLastWriteTimeUtc(fileInfo) > (status.DateLastMetadataRefresh ?? DateTime.MinValue); + } + + return false; + } + } +} diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs new file mode 100644 index 0000000000..593507fb2a --- /dev/null +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs @@ -0,0 +1,1469 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Providers; +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; +using CommonIO; + +namespace MediaBrowser.Providers.TV +{ + public class TvdbSeriesProvider : IRemoteMetadataProvider, IItemIdentityProvider, IHasOrder + { + private const string TvdbSeriesOffset = "TvdbSeriesOffset"; + private const string TvdbSeriesOffsetFormat = "{0}-{1}"; + + internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2); + internal static TvdbSeriesProvider Current { get; private set; } + private readonly IZipClient _zipClient; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly ILogger _logger; + private readonly ISeriesOrderManager _seriesOrder; + private readonly ILibraryManager _libraryManager; + + public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ISeriesOrderManager seriesOrder, ILibraryManager libraryManager) + { + _zipClient = zipClient; + _httpClient = httpClient; + _fileSystem = fileSystem; + _config = config; + _logger = logger; + _seriesOrder = seriesOrder; + _libraryManager = libraryManager; + Current = this; + } + + private const string SeriesSearchUrl = "http://www.thetvdb.com/api/GetSeries.php?seriesname={0}&language={1}"; + private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip"; + private const string GetSeriesByImdbId = "http://www.thetvdb.com/api/GetSeriesByRemoteID.php?imdbid={0}&language={1}"; + + private string NormalizeLanguage(string language) + { + if (string.IsNullOrWhiteSpace(language)) + { + return language; + } + + // pt-br is just pt to tvdb + return language.Split('-')[0].ToLower(); + } + + public async Task> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) + { + if (IsValidSeries(searchInfo.ProviderIds)) + { + var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false); + + if (metadata.HasMetadata) + { + return new List + { + new RemoteSearchResult + { + Name = metadata.Item.Name, + PremiereDate = metadata.Item.PremiereDate, + ProductionYear = metadata.Item.ProductionYear, + ProviderIds = metadata.Item.ProviderIds, + SearchProviderName = Name + } + }; + } + } + + return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + } + + public async Task> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken) + { + var result = new MetadataResult(); + + if (!IsValidSeries(itemId.ProviderIds)) + { + await Identify(itemId).ConfigureAwait(false); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (IsValidSeries(itemId.ProviderIds)) + { + await EnsureSeriesInfo(itemId.ProviderIds, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false); + + result.Item = new Series(); + result.HasMetadata = true; + + FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken); + await FindAnimeSeriesIndex(result.Item, itemId).ConfigureAwait(false); + } + + return result; + } + + private async Task FindAnimeSeriesIndex(Series series, SeriesInfo info) + { + var index = await _seriesOrder.FindSeriesIndex(SeriesOrderTypes.Anime, series.Name); + if (index == null) + return; + + var offset = info.AnimeSeriesIndex - index; + var id = string.Format(TvdbSeriesOffsetFormat, series.GetProviderId(MetadataProviders.Tvdb), offset); + series.SetProviderId(TvdbSeriesOffset, id); + } + + internal static int? GetSeriesOffset(Dictionary seriesProviderIds) + { + string idString; + if (!seriesProviderIds.TryGetValue(TvdbSeriesOffset, out idString)) + return null; + + var parts = idString.Split('-'); + if (parts.Length < 2) + return null; + + int offset; + if (int.TryParse(parts[1], out offset)) + return offset; + + return null; + } + + /// + /// Fetches the series data. + /// + /// The result. + /// The metadata language. + /// The series provider ids. + /// The cancellation token. + /// Task{System.Boolean}. + private void FetchSeriesData(MetadataResult result, string metadataLanguage, Dictionary seriesProviderIds, CancellationToken cancellationToken) + { + var series = result.Item; + + string id; + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + series.SetProviderId(MetadataProviders.Tvdb, id); + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + series.SetProviderId(MetadataProviders.Imdb, id); + } + + var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); + + var seriesXmlPath = GetSeriesXmlPath(seriesProviderIds, metadataLanguage); + var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml"); + + FetchSeriesInfo(series, seriesXmlPath, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + result.ResetPeople(); + + FetchActors(result, actorsXmlPath); + } + + /// + /// Downloads the series zip. + /// + /// The series id. + /// Type of the identifier. + /// The series data path. + /// The last tv database update time. + /// The preferred metadata language. + /// The cancellation token. + /// Task. + /// seriesId + internal async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(seriesId)) + { + throw new ArgumentNullException("seriesId"); + } + + try + { + await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + return; + } + catch (HttpException ex) + { + if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) + { + throw; + } + } + + if (!string.Equals(preferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase)) + { + await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, "en", preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + } + } + + private async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, string saveAsMetadataLanguage, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(seriesId)) + { + throw new ArgumentNullException("seriesId"); + } + + if (!string.Equals(idType, "tvdb", StringComparison.OrdinalIgnoreCase)) + { + seriesId = await GetSeriesByRemoteId(seriesId, idType, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + } + + if (string.IsNullOrWhiteSpace(seriesId)) + { + throw new ArgumentNullException("seriesId"); + } + + var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, NormalizeLanguage(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.ExtractAllFromZip(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); + } + + var downloadLangaugeXmlFile = Path.Combine(seriesDataPath, NormalizeLanguage(preferredMetadataLanguage) + ".xml"); + var saveAsLanguageXmlFile = Path.Combine(seriesDataPath, saveAsMetadataLanguage + ".xml"); + + if (!string.Equals(downloadLangaugeXmlFile, saveAsLanguageXmlFile, StringComparison.OrdinalIgnoreCase)) + { + _fileSystem.CopyFile(downloadLangaugeXmlFile, saveAsLanguageXmlFile, true); + } + + await ExtractEpisodes(seriesDataPath, downloadLangaugeXmlFile, lastTvDbUpdateTime).ConfigureAwait(false); + } + + private async Task GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken) + { + var url = string.Format(GetSeriesByImdbId, id, NormalizeLanguage(language)); + + using (var result = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvDbResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + var doc = new XmlDocument(); + doc.Load(result); + + if (doc.HasChildNodes) + { + var node = doc.SelectSingleNode("//Series/seriesid"); + + if (node != null) + { + var idResult = node.InnerText; + + _logger.Info("Tvdb GetSeriesByRemoteId produced id of {0}", idResult ?? string.Empty); + + return idResult; + } + } + } + + return null; + } + + public TvdbOptions GetTvDbOptions() + { + return _config.GetConfiguration("tvdb"); + } + + internal static bool IsValidSeries(Dictionary seriesProviderIds) + { + string id; + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. + if (!string.IsNullOrWhiteSpace(id)) + { + return true; + } + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) + { + // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. + if (!string.IsNullOrWhiteSpace(id)) + { + return true; + } + } + return false; + } + + private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); + internal async Task EnsureSeriesInfo(Dictionary seriesProviderIds, string preferredMetadataLanguage, CancellationToken cancellationToken) + { + await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + string seriesId; + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); + + // Only download if not already there + // The post-scan task will take care of updates so we don't need to re-download here + if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) + { + await DownloadSeriesZip(seriesId, MetadataProviders.Tvdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + } + + return seriesDataPath; + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); + + // Only download if not already there + // The post-scan task will take care of updates so we don't need to re-download here + if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) + { + await DownloadSeriesZip(seriesId, MetadataProviders.Imdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + } + + return seriesDataPath; + } + + return null; + } + finally + { + _ensureSemaphore.Release(); + } + } + + private bool IsCacheValid(string seriesDataPath, string preferredMetadataLanguage) + { + try + { + var files = _fileSystem.GetFiles(seriesDataPath) + .ToList(); + + var seriesXmlFilename = preferredMetadataLanguage + ".xml"; + + var automaticUpdatesEnabled = GetTvDbOptions().EnableAutomaticUpdates; + + const int cacheDays = 1; + + var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase)); + // No need to check age if automatic updates are enabled + if (seriesFile == null || !seriesFile.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(seriesFile)).TotalDays > cacheDays)) + { + return false; + } + + var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase)); + // No need to check age if automatic updates are enabled + if (actorsXml == null || !actorsXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(actorsXml)).TotalDays > cacheDays)) + { + return false; + } + + var bannersXml = files.FirstOrDefault(i => string.Equals("banners.xml", i.Name, StringComparison.OrdinalIgnoreCase)); + // No need to check age if automatic updates are enabled + if (bannersXml == null || !bannersXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(bannersXml)).TotalDays > cacheDays)) + { + return false; + } + return true; + } + catch (DirectoryNotFoundException) + { + return false; + } + catch (FileNotFoundException) + { + return false; + } + } + + /// + /// Finds the series. + /// + /// The name. + /// The year. + /// The language. + /// The cancellation token. + /// Task{System.String}. + private async Task> FindSeries(string name, int? year, string language, CancellationToken cancellationToken) + { + var results = (await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false)).ToList(); + + if (results.Count == 0) + { + var parsedName = _libraryManager.ParseName(name); + var nameWithoutYear = parsedName.Name; + + if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase)) + { + results = (await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false)).ToList(); + } + } + + return results.Where(i => + { + if (year.HasValue && i.ProductionYear.HasValue) + { + // Allow one year tolerance + return Math.Abs(year.Value - i.ProductionYear.Value) <= 1; + } + + return true; + }); + } + + private async Task> FindSeriesInternal(string name, string language, CancellationToken cancellationToken) + { + var url = string.Format(SeriesSearchUrl, WebUtility.UrlEncode(name), NormalizeLanguage(language)); + var doc = new XmlDocument(); + + using (var results = await _httpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvDbResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + doc.Load(results); + } + + var searchResults = new List(); + + if (doc.HasChildNodes) + { + var nodes = doc.SelectNodes("//Series"); + var comparableName = GetComparableName(name); + if (nodes != null) + { + foreach (XmlNode node in nodes) + { + var searchResult = new RemoteSearchResult + { + SearchProviderName = Name + }; + + 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); + } + + var imdbIdNode = node.SelectSingleNode("./IMDB_ID"); + if (imdbIdNode != null) + { + var val = imdbIdNode.InnerText; + if (!string.IsNullOrWhiteSpace(val)) + { + searchResult.SetProviderId(MetadataProviders.Imdb, val); + } + } + + var bannerNode = node.SelectSingleNode("./banner"); + if (bannerNode != null) + { + var val = bannerNode.InnerText; + if (!string.IsNullOrWhiteSpace(val)) + { + searchResult.ImageUrl = TVUtils.BannerUrl + val; + } + } + + var airDateNode = node.SelectSingleNode("./FirstAired"); + if (airDateNode != null) + { + var val = airDateNode.InnerText; + if (!string.IsNullOrWhiteSpace(val)) + { + DateTime date; + if (DateTime.TryParse(val, out date)) + { + searchResult.ProductionYear = date.Year; + } + } + } + + foreach (var title in titles) + { + if (string.Equals(title, comparableName, StringComparison.OrdinalIgnoreCase)) + { + var id = node.SelectSingleNode("./seriesid") ?? + node.SelectSingleNode("./id"); + + if (id != null) + { + searchResult.Name = title; + searchResult.SetProviderId(MetadataProviders.Tvdb, id.InnerText); + searchResults.Add(searchResult); + } + break; + } + _logger.Info("TVDb Provider - " + title + " did not match " + comparableName); + } + } + } + } + + if (searchResults.Count == 0) + { + _logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org."); + } + + return searchResults; + } + + /// + /// 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(); + } + + 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 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 result. + /// The actors XML path. + private void FetchActors(MetadataResult result, string actorsXmlPath) + { + 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()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Actor": + { + using (var subtree = reader.ReadSubtree()) + { + FetchDataFromActorNode(result, subtree); + } + break; + } + default: + reader.Skip(); + break; + } + } + } + } + } + } + + /// + /// Fetches the data from actor node. + /// + /// The result. + /// The reader. + private void FetchDataFromActorNode(MetadataResult result, 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; + } + + case "id": + { + break; + } + + case "Image": + { + var url = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + if (!string.IsNullOrWhiteSpace(url)) + { + personInfo.ImageUrl = TVUtils.BannerUrl + url; + } + break; + } + + case "SortOrder": + { + 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)) + { + personInfo.SortOrder = rval; + } + } + break; + } + + default: + reader.Skip(); + break; + } + } + } + + personInfo.Type = PersonType.Actor; + + if (!string.IsNullOrWhiteSpace(personInfo.Name)) + { + result.AddPerson(personInfo); + } + } + + private void FetchDataFromSeriesNode(Series item, XmlReader reader, CancellationToken cancellationToken) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "SeriesName": + { + item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + + case "Overview": + { + item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + + case "Airs_DayOfWeek": + { + var val = reader.ReadElementContentAsString(); + + 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)) + { + item.OfficialRating = 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 "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)) + { + 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)) + { + 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)) + { + 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; + } + } + } + } + + /// + /// 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.IsNullOrWhiteSpace(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 || !_fileSystem.FileExists(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 || !_fileSystem.FileExists(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 provider ids. + /// System.String. + internal static string GetSeriesDataPath(IApplicationPaths appPaths, Dictionary seriesProviderIds) + { + string seriesId; + if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); + + return seriesDataPath; + } + + if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) + { + var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); + + return seriesDataPath; + } + + return null; + } + + public string GetSeriesXmlPath(Dictionary seriesProviderIds, string language) + { + var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); + + var seriesXmlFilename = language.ToLower() + ".xml"; + + return Path.Combine(seriesDataPath, seriesXmlFilename); + } + + /// + /// Gets the series data path. + /// + /// The app paths. + /// System.String. + internal static string GetSeriesDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.CachePath, "tvdb"); + + return dataPath; + } + + private void DeleteXmlFiles(string path) + { + try + { + foreach (var file in _fileSystem.GetFilePaths(path, true) + .ToList()) + { + _fileSystem.DeleteFile(file); + } + } + 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(); + } + + public string Name + { + get { return "TheTVDB"; } + } + + public async Task Identify(SeriesInfo info) + { + if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProviders.Tvdb))) + { + return; + } + + var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None).ConfigureAwait(false); + + var entry = srch.FirstOrDefault(); + + if (entry != null) + { + var id = entry.GetProviderId(MetadataProviders.Tvdb); + info.SetProviderId(MetadataProviders.Tvdb, id); + } + } + + public int Order + { + get + { + // After Omdb + return 1; + } + } + + public Task GetImageResponse(string url, CancellationToken cancellationToken) + { + return _httpClient.GetResponse(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + ResourcePool = TvDbResourcePool + }); + } + } + + public class TvdbConfigStore : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new List + { + new ConfigurationStore + { + Key = "tvdb", + ConfigurationType = typeof(TvdbOptions) + } + }; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs deleted file mode 100644 index 50ecc6bbfd..0000000000 --- a/MediaBrowser.Providers/TV/TvdbEpisodeImageProvider.cs +++ /dev/null @@ -1,209 +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.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; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - public class TvdbEpisodeImageProvider : IRemoteImageProvider, IHasChangeMonitor - { - private readonly IServerConfigurationManager _config; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - - public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - } - - public string Name - { - get { return "TheTVDB"; } - } - - public bool Supports(IHasImages item) - { - return item is Episode; - } - - public IEnumerable GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary - }; - } - - public Task> GetImages(IHasImages item, CancellationToken cancellationToken) - { - var episode = (Episode)item; - var series = episode.Series; - - if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) - { - // Process images - var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.ProviderIds); - var indexOffset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds) ?? 0; - - var nodes = TvdbEpisodeProvider.Current.GetEpisodeXmlNodes(seriesDataPath, episode.GetLookupInfo()); - - var result = nodes.Select(i => GetImageInfo(i, cancellationToken)) - .Where(i => i != null) - .ToList(); - - return Task.FromResult>(result); - } - - return Task.FromResult>(new RemoteImageInfo[] { }); - } - - private RemoteImageInfo GetImageInfo(XmlReader reader, CancellationToken cancellationToken) - { - var height = 225; - var width = 400; - var url = string.Empty; - - // Use XmlReader for best performance - using (reader) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "thumb_width": - { - 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)) - { - width = rval; - } - } - break; - } - - case "thumb_height": - { - 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)) - { - height = rval; - } - } - break; - } - - case "filename": - { - var val = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(val)) - { - url = TVUtils.BannerUrl + val; - } - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - - if (string.IsNullOrEmpty(url)) - { - return null; - } - - return new RemoteImageInfo - { - Width = width, - Height = height, - ProviderName = Name, - Url = url, - Type = ImageType.Primary - }; - } - - public int Order - { - get { return 0; } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool - }); - } - - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) - { - var episode = (Episode)item; - - if (!episode.IsVirtualUnaired) - { - // For non-unaired items, only enable if configured - if (!TvdbSeriesProvider.Current.GetTvDbOptions().EnableAutomaticUpdates) - { - return false; - } - } - - if (!item.HasImage(ImageType.Primary)) - { - var series = episode.Series; - - if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) - { - // Process images - var seriesXmlPath = TvdbSeriesProvider.Current.GetSeriesXmlPath(series.ProviderIds, series.GetPreferredMetadataLanguage()); - - return _fileSystem.GetLastWriteTimeUtc(seriesXmlPath) > date; - } - } - - return false; - } - } -} diff --git a/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs deleted file mode 100644 index 3920330489..0000000000 --- a/MediaBrowser.Providers/TV/TvdbEpisodeProvider.cs +++ /dev/null @@ -1,978 +0,0 @@ -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -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.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - - /// - /// Class RemoteEpisodeProvider - /// - class TvdbEpisodeProvider : IRemoteMetadataProvider, IItemIdentityProvider, IHasChangeMonitor - { - private static readonly string FullIdKey = MetadataProviders.Tvdb + "-Full"; - - internal static TvdbEpisodeProvider Current; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly ILogger _logger; - - public TvdbEpisodeProvider(IFileSystem fileSystem, IServerConfigurationManager config, IHttpClient httpClient, ILogger logger) - { - _fileSystem = fileSystem; - _config = config; - _httpClient = httpClient; - _logger = logger; - Current = this; - } - - public async Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) - { - var list = new List(); - - // The search query must either provide an episode number or date - if (!searchInfo.IndexNumber.HasValue && !searchInfo.PremiereDate.HasValue) - { - return list; - } - - if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) - { - var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, searchInfo.SeriesProviderIds); - - var searchNumbers = new EpisodeNumbers(); - - if (searchInfo.IndexNumber.HasValue) { - searchNumbers.EpisodeNumber = searchInfo.IndexNumber.Value; - } - - searchNumbers.SeasonNumber = searchInfo.ParentIndexNumber; - searchNumbers.EpisodeNumberEnd = searchInfo.IndexNumberEnd ?? searchNumbers.EpisodeNumber; - - try - { - var metadataResult = FetchEpisodeData(searchInfo, searchNumbers, seriesDataPath, cancellationToken); - - if (metadataResult.HasMetadata) - { - var item = metadataResult.Item; - - list.Add(new RemoteSearchResult - { - IndexNumber = item.IndexNumber, - Name = item.Name, - ParentIndexNumber = item.ParentIndexNumber, - PremiereDate = item.PremiereDate, - ProductionYear = item.ProductionYear, - ProviderIds = item.ProviderIds, - SearchProviderName = Name, - IndexNumberEnd = item.IndexNumberEnd - }); - } - } - catch (FileNotFoundException) - { - // Don't fail the provider because this will just keep on going and going. - } - catch (DirectoryNotFoundException) - { - // Don't fail the provider because this will just keep on going and going. - } - } - - return list; - } - - public string Name - { - get { return "TheTVDB"; } - } - - public async Task> GetMetadata(EpisodeInfo searchInfo, CancellationToken cancellationToken) - { - var result = new MetadataResult(); - - if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && - (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue)) - { - await TvdbSeriesProvider.Current.EnsureSeriesInfo(searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, searchInfo.SeriesProviderIds); - - var searchNumbers = new EpisodeNumbers(); - if (searchInfo.IndexNumber.HasValue) { - searchNumbers.EpisodeNumber = searchInfo.IndexNumber.Value; - } - searchNumbers.SeasonNumber = searchInfo.ParentIndexNumber; - searchNumbers.EpisodeNumberEnd = searchInfo.IndexNumberEnd ?? searchNumbers.EpisodeNumber; - - try - { - result = FetchEpisodeData(searchInfo, searchNumbers, seriesDataPath, cancellationToken); - } - catch (FileNotFoundException) - { - // Don't fail the provider because this will just keep on going and going. - } - catch (DirectoryNotFoundException) - { - // Don't fail the provider because this will just keep on going and going. - } - } - else - { - _logger.Debug("No series identity found for {0}", searchInfo.Name); - } - - return result; - } - - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) - { - // Only enable for virtual items - if (item.LocationType != LocationType.Virtual) - { - return false; - } - - var episode = (Episode)item; - var series = episode.Series; - - if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) - { - // Process images - var seriesXmlPath = TvdbSeriesProvider.Current.GetSeriesXmlPath(series.ProviderIds, series.GetPreferredMetadataLanguage()); - - return _fileSystem.GetLastWriteTimeUtc(seriesXmlPath) > date; - } - - return false; - } - - /// - /// Gets the episode XML files. - /// - /// The series data path. - /// The search information. - /// List{FileInfo}. - internal List GetEpisodeXmlNodes(string seriesDataPath, EpisodeInfo searchInfo) - { - var seriesXmlPath = TvdbSeriesProvider.Current.GetSeriesXmlPath (searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage); - - try - { - return GetXmlNodes(seriesXmlPath, searchInfo); - } - catch (DirectoryNotFoundException) - { - return new List (); - } - catch (FileNotFoundException) - { - return new List (); - } - } - - private class EpisodeNumbers - { - public int EpisodeNumber; - public int? SeasonNumber; - public int EpisodeNumberEnd; - } - - /// - /// Fetches the episode data. - /// - /// The identifier. - /// The search numbers. - /// The series data path. - /// The cancellation token. - /// Task{System.Boolean}. - private MetadataResult FetchEpisodeData(EpisodeInfo id, EpisodeNumbers searchNumbers, string seriesDataPath, CancellationToken cancellationToken) - { - var result = new MetadataResult() - { - Item = new Episode - { - IndexNumber = id.IndexNumber, - ParentIndexNumber = id.ParentIndexNumber, - IndexNumberEnd = id.IndexNumberEnd - } - }; - - var xmlNodes = GetEpisodeXmlNodes (seriesDataPath, id); - - if (xmlNodes.Count > 0) { - FetchMainEpisodeInfo(result, xmlNodes[0], cancellationToken); - - result.HasMetadata = true; - } - - foreach (var node in xmlNodes.Skip(1)) { - FetchAdditionalPartInfo(result, node, cancellationToken); - } - - return result; - } - - private List GetXmlNodes(string xmlFile, EpisodeInfo searchInfo) - { - var list = new List (); - - if (searchInfo.IndexNumber.HasValue) - { - var files = GetEpisodeXmlFiles (searchInfo.ParentIndexNumber, searchInfo.IndexNumber, searchInfo.IndexNumberEnd, Path.GetDirectoryName (xmlFile)); - - list = files.Select (GetXmlReader).ToList (); - } - - if (list.Count == 0 && searchInfo.PremiereDate.HasValue) { - list = GetXmlNodesByPremiereDate (xmlFile, searchInfo.PremiereDate.Value); - } - - return list; - } - - private List GetEpisodeXmlFiles(int? seasonNumber, int? episodeNumber, int? endingEpisodeNumber, string seriesDataPath) - { - var files = new List(); - - if (episodeNumber == null) - { - return files; - } - - if (seasonNumber == null) - { - return files; - } - - var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); - - var fileInfo = _fileSystem.GetFileInfo(file); - var usingAbsoluteData = false; - - if (fileInfo.Exists) - { - files.Add(fileInfo); - } - else - { - file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); - fileInfo = _fileSystem.GetFileInfo(file); - if (fileInfo.Exists) - { - files.Add(fileInfo); - usingAbsoluteData = true; - } - } - - var end = endingEpisodeNumber ?? 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 = _fileSystem.GetFileInfo(file); - if (fileInfo.Exists) - { - files.Add(fileInfo); - } - else - { - break; - } - - episodeNumber++; - } - - return files; - } - - private XmlReader GetXmlReader(FileSystemMetadata xmlFile) - { - return GetXmlReader (_fileSystem.ReadAllText(xmlFile.FullName, Encoding.UTF8)); - } - - private XmlReader GetXmlReader(String xml) - { - var streamReader = new StringReader (xml); - - return XmlReader.Create (streamReader, new XmlReaderSettings { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }); - } - - private List GetXmlNodesByPremiereDate(string xmlFile, DateTime premiereDate) - { - var list = new List (); - - 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()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Episode": - { - var outerXml = reader.ReadOuterXml(); - - var airDate = GetEpisodeAirDate (outerXml); - - if (airDate.HasValue && premiereDate.Date == airDate.Value.Date) - { - list.Add (GetXmlReader(outerXml)); - return list; - } - - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - } - - return list; - } - - private DateTime? GetEpisodeAirDate(string xml) - { - using (var streamReader = new StringReader (xml)) - { - // 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 ()) { - - 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)) { - date = date.ToUniversalTime (); - - return date; - } - } - - break; - } - - default: - reader.Skip (); - break; - } - } - } - } - } - return null; - } - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private void FetchMainEpisodeInfo(MetadataResult result, XmlReader reader, CancellationToken cancellationToken) - { - var item = result.Item; - - // Use XmlReader for best performance - using (reader) - { - reader.MoveToContent(); - - result.ResetPeople(); - - // 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 "DVD_episodenumber": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - float num; - - if (float.TryParse(val, NumberStyles.Any, _usCulture, out num)) - { - item.DvdEpisodeNumber = num; - } - } - - break; - } - - case "DVD_season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - float num; - - if (float.TryParse(val, NumberStyles.Any, _usCulture, out num)) - { - item.DvdSeasonNumber = Convert.ToInt32(num); - } - } - - break; - } - - case "EpisodeNumber": - { - if (!item.IndexNumber.HasValue) - { - 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.IndexNumber = rval; - } - } - } - - break; - } - - case "SeasonNumber": - { - if (!item.ParentIndexNumber.HasValue) - { - 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.ParentIndexNumber = rval; - } - } - } - - break; - } - - case "absolute_number": - { - 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.AbsoluteEpisodeNumber = rval; - } - } - - break; - } - - case "airsbefore_episode": - { - 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.AirsBeforeEpisodeNumber = rval; - } - } - - break; - } - - case "airsafter_season": - { - 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.AirsAfterSeasonNumber = rval; - } - } - - break; - } - - case "airsbefore_season": - { - 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.AirsBeforeSeasonNumber = rval; - } - } - - break; - } - - 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 = 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(result, val, PersonType.Director); - } - } - - break; - } - case "GuestStars": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddGuestStars(result, val); - } - } - - break; - } - case "Writer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddPeople(result, val, PersonType.Writer); - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - } - - private void AddPeople(MetadataResult result, 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() })) - { - result.AddPerson(person); - } - } - - private void AddGuestStars(MetadataResult result, string val) - where T : BaseItem - { - // 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 }; - })) - { - if (!string.IsNullOrWhiteSpace(person.Name)) - { - result.AddPerson(person); - } - } - } - - private void FetchAdditionalPartInfo(MetadataResult result, XmlReader reader, CancellationToken cancellationToken) - { - var item = result.Item; - - // Use XmlReader for best performance - using (reader) - { - 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(result, val, PersonType.Director); - } - } - - break; - } - case "GuestStars": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddGuestStars(result, val); - } - } - - break; - } - case "Writer": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - if (!item.LockedFields.Contains(MetadataFields.Cast)) - { - AddPeople(result, val, PersonType.Writer); - } - } - - break; - } - - default: - reader.Skip(); - break; - } - } - } - } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool - }); - } - - public Task Identify(EpisodeInfo info) - { - if (info.ProviderIds.ContainsKey(FullIdKey)) - { - return Task.FromResult(null); - } - - string seriesTvdbId; - info.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesTvdbId); - - if (string.IsNullOrEmpty(seriesTvdbId) || info.IndexNumber == null) - { - return Task.FromResult(null); - } - - var id = new Identity(seriesTvdbId, info.ParentIndexNumber, info.IndexNumber.Value, info.IndexNumberEnd); - info.SetProviderId(FullIdKey, id.ToString()); - - return Task.FromResult(id); - } - - public int Order { get { return 0; } } - - public struct Identity - { - public string SeriesId { get; private set; } - public int? SeasonIndex { get; private set; } - public int EpisodeNumber { get; private set; } - public int? EpisodeNumberEnd { get; private set; } - - public Identity(string id) - : this() - { - this = ParseIdentity(id).Value; - } - - public Identity(string seriesId, int? seasonIndex, int episodeNumber, int? episodeNumberEnd) - : this() - { - SeriesId = seriesId; - SeasonIndex = seasonIndex; - EpisodeNumber = episodeNumber; - EpisodeNumberEnd = episodeNumberEnd; - } - - public override string ToString() - { - return string.Format("{0}:{1}:{2}", - SeriesId, - SeasonIndex != null ? SeasonIndex.Value.ToString() : "A", - EpisodeNumber + (EpisodeNumberEnd != null ? "-" + EpisodeNumberEnd.Value.ToString() : "")); - } - - public static Identity? ParseIdentity(string id) - { - if (string.IsNullOrEmpty(id)) - return null; - - try { - var parts = id.Split(':'); - var series = parts[0]; - var season = parts[1] != "A" ? (int?)int.Parse(parts[1]) : null; - - int index; - int? indexEnd; - - if (parts[2].Contains("-")) { - var split = parts[2].IndexOf("-", StringComparison.OrdinalIgnoreCase); - index = int.Parse(parts[2].Substring(0, split)); - indexEnd = int.Parse(parts[2].Substring(split + 1)); - } else { - index = int.Parse(parts[2]); - indexEnd = null; - } - - return new Identity(series, season, index, indexEnd); - } catch { - return null; - } - } - } - } -} diff --git a/MediaBrowser.Providers/TV/TvdbPrescanTask.cs b/MediaBrowser.Providers/TV/TvdbPrescanTask.cs deleted file mode 100644 index d362ca722d..0000000000 --- a/MediaBrowser.Providers/TV/TvdbPrescanTask.cs +++ /dev/null @@ -1,366 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -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.Threading; -using System.Threading.Tasks; -using System.Xml; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - /// - /// Class TvdbPrescanTask - /// - public class TvdbPrescanTask : ILibraryPostScanTask - { - /// - /// The server time URL - /// - private const string ServerTimeUrl = "http://thetvdb.com/api/Updates.php?type=none"; - - /// - /// The updates URL - /// - private const string UpdatesUrl = "http://thetvdb.com/api/Updates.php?type=all&time={0}"; - - /// - /// The _HTTP client - /// - private readonly IHttpClient _httpClient; - /// - /// The _logger - /// - private readonly ILogger _logger; - /// - /// The _config - /// - private readonly IServerConfigurationManager _config; - private readonly IFileSystem _fileSystem; - private readonly ILibraryManager _libraryManager; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The HTTP client. - /// The config. - public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IFileSystem fileSystem, ILibraryManager libraryManager) - { - _logger = logger; - _httpClient = httpClient; - _config = config; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - } - - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - if (!_config.Configuration.EnableInternetProviders) - { - progress.Report(100); - return; - } - - var seriesConfig = _config.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, typeof(Series).Name, StringComparison.OrdinalIgnoreCase)); - - if (seriesConfig != null && seriesConfig.DisabledMetadataFetchers.Contains(TvdbSeriesProvider.Current.Name, StringComparer.OrdinalIgnoreCase)) - { - progress.Report(100); - return; - } - - var path = TvdbSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); - - _fileSystem.CreateDirectory(path); - - var timestampFile = Path.Combine(path, "time.txt"); - - var timestampFileInfo = _fileSystem.GetFileInfo(timestampFile); - - // Don't check for tvdb updates anymore frequently than 24 hours - if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 1) - { - return; - } - - // Find out the last time we queried tvdb for updates - var lastUpdateTime = timestampFileInfo.Exists ? _fileSystem.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; - - string newUpdateTime; - - var existingDirectories = Directory.EnumerateDirectories(path) - .Select(Path.GetFileName) - .ToList(); - - var seriesIdsInLibrary = _libraryManager.RootFolder - .GetRecursiveChildren(i => i is Series && !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) - .Cast() - .Select(i => i.GetProviderId(MetadataProviders.Tvdb)) - .ToList(); - - var missingSeries = seriesIdsInLibrary.Except(existingDirectories, StringComparer.OrdinalIgnoreCase) - .ToList(); - - // If this is our first time, update all series - if (string.IsNullOrEmpty(lastUpdateTime)) - { - // First get tvdb server time - using (var stream = await _httpClient.Get(new HttpRequestOptions - { - Url = ServerTimeUrl, - CancellationToken = cancellationToken, - EnableHttpCompression = true, - ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool - - }).ConfigureAwait(false)) - { - newUpdateTime = GetUpdateTime(stream); - } - - existingDirectories.AddRange(missingSeries); - - await UpdateSeries(existingDirectories, path, null, progress, cancellationToken).ConfigureAwait(false); - } - else - { - var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false); - - newUpdateTime = seriesToUpdate.Item2; - - long lastUpdateValue; - - long.TryParse(lastUpdateTime, NumberStyles.Any, UsCulture, out lastUpdateValue); - - var nullableUpdateValue = lastUpdateValue == 0 ? (long?)null : lastUpdateValue; - - var listToUpdate = seriesToUpdate.Item1.ToList(); - listToUpdate.AddRange(missingSeries); - - await UpdateSeries(listToUpdate, path, nullableUpdateValue, progress, cancellationToken).ConfigureAwait(false); - } - - _fileSystem.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8); - progress.Report(100); - } - - /// - /// Gets the update time. - /// - /// The response. - /// System.String. - private string GetUpdateTime(Stream response) - { - var settings = new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }; - - using (var streamReader = new StreamReader(response, 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 "Time": - { - return (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - } - default: - reader.Skip(); - break; - } - } - } - } - } - - return null; - } - - /// - /// Gets the series ids to update. - /// - /// The existing series ids. - /// The last update time. - /// The cancellation token. - /// Task{IEnumerable{System.String}}. - private async Task, string>> GetSeriesIdsToUpdate(IEnumerable existingSeriesIds, string lastUpdateTime, CancellationToken cancellationToken) - { - // First get last time - using (var stream = await _httpClient.Get(new HttpRequestOptions - { - Url = string.Format(UpdatesUrl, lastUpdateTime), - CancellationToken = cancellationToken, - EnableHttpCompression = true, - ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool - - }).ConfigureAwait(false)) - { - var data = GetUpdatedSeriesIdList(stream); - - var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - var seriesList = data.Item1 - .Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); - - return new Tuple, string>(seriesList, data.Item2); - } - } - - private Tuple, string> GetUpdatedSeriesIdList(Stream stream) - { - string updateTime = null; - var idList = new List(); - - var settings = new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }; - - using (var streamReader = new StreamReader(stream, 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 "Time": - { - updateTime = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - case "Series": - { - var id = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - idList.Add(id); - break; - } - default: - reader.Skip(); - break; - } - } - } - } - } - - return new Tuple, string>(idList, updateTime); - } - - /// - /// Updates the series. - /// - /// The series ids. - /// The series data path. - /// The last tv db update time. - /// The progress. - /// The cancellation token. - /// Task. - private async Task UpdateSeries(IEnumerable seriesIds, string seriesDataPath, long? lastTvDbUpdateTime, IProgress progress, CancellationToken cancellationToken) - { - var list = seriesIds.ToList(); - var numComplete = 0; - - // Gather all series into a lookup by tvdb id - var allSeries = _libraryManager.RootFolder - .GetRecursiveChildren(i => i is Series && !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tvdb))) - .Cast() - .ToLookup(i => i.GetProviderId(MetadataProviders.Tvdb)); - - foreach (var seriesId in list) - { - // Find the preferred language(s) for the movie in the library - var languages = allSeries[seriesId] - .Select(i => i.GetPreferredMetadataLanguage()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - - foreach (var language in languages) - { - try - { - await UpdateSeries(seriesId, seriesDataPath, lastTvDbUpdateTime, language, cancellationToken).ConfigureAwait(false); - } - catch (HttpException ex) - { - _logger.ErrorException("Error updating tvdb series id {0}, language {1}", ex, seriesId, language); - - // Already logged at lower levels, but don't fail the whole operation, unless timed out - // We have to fail this to make it run again otherwise new episode data could potentially be missing - if (ex.IsTimedOut) - { - throw; - } - } - } - - numComplete++; - double percent = numComplete; - percent /= list.Count; - percent *= 100; - - progress.Report(percent); - } - } - - /// - /// Updates the series. - /// - /// The id. - /// The series data path. - /// The last tv db update time. - /// The preferred metadata language. - /// The cancellation token. - /// Task. - private Task UpdateSeries(string id, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - _logger.Info("Updating series from tvdb " + id + ", language " + preferredMetadataLanguage); - - seriesDataPath = Path.Combine(seriesDataPath, id); - - _fileSystem.CreateDirectory(seriesDataPath); - - return TvdbSeriesProvider.Current.DownloadSeriesZip(id, MetadataProviders.Tvdb.ToString(), seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, cancellationToken); - } - } -} diff --git a/MediaBrowser.Providers/TV/TvdbSeasonIdentityProvider.cs b/MediaBrowser.Providers/TV/TvdbSeasonIdentityProvider.cs deleted file mode 100644 index 4198430c9f..0000000000 --- a/MediaBrowser.Providers/TV/TvdbSeasonIdentityProvider.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Providers.TV -{ - public class TvdbSeasonIdentityProvider : IItemIdentityProvider - { - public static readonly string FullIdKey = MetadataProviders.Tvdb + "-Full"; - - public Task Identify(SeasonInfo info) - { - string tvdbSeriesId; - if (!info.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out tvdbSeriesId) || string.IsNullOrEmpty(tvdbSeriesId) || info.IndexNumber == null) - { - return Task.FromResult(null); - } - - if (string.IsNullOrEmpty(info.GetProviderId(FullIdKey))) - { - var id = string.Format("{0}:{1}", tvdbSeriesId, info.IndexNumber.Value); - info.SetProviderId(FullIdKey, id); - } - - return Task.FromResult(null); - } - - public static TvdbSeasonIdentity? ParseIdentity(string id) - { - if (id == null) - { - return null; - } - - try - { - var parts = id.Split(':'); - return new TvdbSeasonIdentity(parts[0], int.Parse(parts[1])); - } - catch - { - return null; - } - } - } - - public struct TvdbSeasonIdentity - { - public string SeriesId { get; private set; } - public int Index { get; private set; } - - public TvdbSeasonIdentity(string id) - : this() - { - this = TvdbSeasonIdentityProvider.ParseIdentity(id).Value; - } - - public TvdbSeasonIdentity(string seriesId, int index) - : this() - { - SeriesId = seriesId; - Index = index; - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Providers/TV/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/TV/TvdbSeasonImageProvider.cs deleted file mode 100644 index 7af85ecc93..0000000000 --- a/MediaBrowser.Providers/TV/TvdbSeasonImageProvider.cs +++ /dev/null @@ -1,391 +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.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; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor - { - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - - public TvdbSeasonImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - } - - public string Name - { - get { return ProviderName; } - } - - public static string ProviderName - { - get { return "TheTVDB"; } - } - - public bool Supports(IHasImages item) - { - return item is Season; - } - - public IEnumerable GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary, - ImageType.Banner, - ImageType.Backdrop - }; - } - - public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) - { - var season = (Season)item; - var series = season.Series; - - if (series != null && season.IndexNumber.HasValue && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) - { - var seriesProviderIds = series.ProviderIds; - var seasonNumber = season.IndexNumber.Value; - - var identity = TvdbSeasonIdentityProvider.ParseIdentity(season.GetProviderId(TvdbSeasonIdentityProvider.FullIdKey)); - if (identity == null) - { - identity = new TvdbSeasonIdentity(series.GetProviderId(MetadataProviders.Tvdb), seasonNumber); - } - - if (identity != null) - { - var id = identity.Value; - seasonNumber = AdjustForSeriesOffset(series, id.Index); - - seriesProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - seriesProviderIds[MetadataProviders.Tvdb.ToString()] = id.SeriesId; - } - - var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(seriesProviderIds, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); - - var path = Path.Combine(seriesDataPath, "banners.xml"); - - try - { - return GetImages(path, item.GetPreferredMetadataLanguage(), seasonNumber, cancellationToken); - } - catch (FileNotFoundException) - { - // No tvdb data yet. Don't blow up - } - catch (DirectoryNotFoundException) - { - // No tvdb data yet. Don't blow up - } - } - - return new RemoteImageInfo[] { }; - } - - private int AdjustForSeriesOffset(Series series, int seasonNumber) - { - var offset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds); - if (offset != null) - return (seasonNumber + offset.Value); - - return seasonNumber; - } - - internal static IEnumerable GetImages(string xmlPath, string preferredLanguage, int seasonNumber, 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, seasonNumber); - } - break; - } - default: - reader.Skip(); - break; - } - } - } - } - } - - var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => - { - if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0) - .ToList(); - } - - private static void AddImage(XmlReader reader, List images, int seasonNumber) - { - reader.MoveToContent(); - - string bannerType = null; - string bannerType2 = 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 "BannerType2": - { - 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 "BannerPath": - { - url = reader.ReadElementContentAsString() ?? string.Empty; - 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 && bannerSeason.Value == seasonNumber) - { - var imageInfo = new RemoteImageInfo - { - RatingType = RatingType.Score, - CommunityRating = rating, - VoteCount = voteCount, - Url = TVUtils.BannerUrl + url, - ProviderName = ProviderName, - Language = language, - Width = width, - 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)) - { - imageInfo.Type = ImageType.Primary; - images.Add(imageInfo); - } - else if (string.Equals(bannerType2, "seasonwide", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Banner; - images.Add(imageInfo); - } - } - else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Backdrop; - images.Add(imageInfo); - } - } - - } - - public int Order - { - get { return 0; } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool - }); - } - - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) - { - if (item.LocationType != LocationType.Virtual) - { - // For non-virtual items, only enable if configured - if (!TvdbSeriesProvider.Current.GetTvDbOptions().EnableAutomaticUpdates) - { - return false; - } - } - - var season = (Season)item; - var series = season.Series; - - if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) - { - // Process images - var imagesXmlPath = Path.Combine(TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, series.ProviderIds), "banners.xml"); - - var fileInfo = _fileSystem.GetFileInfo(imagesXmlPath); - - return fileInfo.Exists && _fileSystem.GetLastWriteTimeUtc(fileInfo) > date; - } - - return false; - } - } -} diff --git a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs deleted file mode 100644 index eae389dfb0..0000000000 --- a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs +++ /dev/null @@ -1,356 +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.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; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder, IHasItemChangeMonitor - { - private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly IFileSystem _fileSystem; - - public TvdbSeriesImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem) - { - _config = config; - _httpClient = httpClient; - _fileSystem = fileSystem; - } - - public string Name - { - get { return ProviderName; } - } - - public static string ProviderName - { - get { return "TheTVDB"; } - } - - public bool Supports(IHasImages item) - { - return item is Series; - } - - public IEnumerable GetSupportedImages(IHasImages item) - { - return new List - { - ImageType.Primary, - ImageType.Banner, - ImageType.Backdrop - }; - } - - public async Task> GetImages(IHasImages item, CancellationToken cancellationToken) - { - if (TvdbSeriesProvider.IsValidSeries(item.ProviderIds)) - { - var language = item.GetPreferredMetadataLanguage(); - - var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(item.ProviderIds, language, cancellationToken).ConfigureAwait(false); - - var path = Path.Combine(seriesDataPath, "banners.xml"); - - try - { - var seriesOffset = TvdbSeriesProvider.GetSeriesOffset(item.ProviderIds); - if (seriesOffset != null && seriesOffset.Value != 0) - return TvdbSeasonImageProvider.GetImages(path, language, seriesOffset.Value + 1, cancellationToken); - - return GetImages(path, language, cancellationToken); - } - catch (FileNotFoundException) - { - // No tvdb data yet. Don't blow up - } - catch (DirectoryNotFoundException) - { - // No tvdb data yet. Don't blow up - } - } - - return new RemoteImageInfo[] { }; - } - - private IEnumerable GetImages(string xmlPath, string preferredLanguage, CancellationToken cancellationToken) - { - var settings = new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }; - - var list = new List(); - - using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Banner": - { - using (var subtree = reader.ReadSubtree()) - { - AddImage(subtree, list); - } - break; - } - default: - reader.Skip(); - break; - } - } - } - } - } - - var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase); - - return list.OrderByDescending(i => - { - if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (!isLanguageEn) - { - if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - } - if (string.IsNullOrEmpty(i.Language)) - { - return isLanguageEn ? 3 : 2; - } - return 0; - }) - .ThenByDescending(i => i.CommunityRating ?? 0) - .ThenByDescending(i => i.VoteCount ?? 0) - .ToList(); - } - - private void AddImage(XmlReader reader, List images) - { - reader.MoveToContent(); - - string bannerType = null; - string url = null; - int? bannerSeason = null; - int? width = null; - int? height = null; - string language = null; - double? rating = null; - int? voteCount = null; - string thumbnailUrl = null; - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Rating": - { - var val = reader.ReadElementContentAsString() ?? string.Empty; - - double rval; - - if (double.TryParse(val, NumberStyles.Any, _usCulture, out rval)) - { - rating = rval; - } - - break; - } - - case "RatingCount": - { - var val = reader.ReadElementContentAsString() ?? string.Empty; - - int rval; - - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval)) - { - voteCount = rval; - } - - break; - } - - case "Language": - { - language = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "ThumbnailPath": - { - thumbnailUrl = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType": - { - bannerType = reader.ReadElementContentAsString() ?? string.Empty; - - break; - } - - case "BannerPath": - { - url = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType2": - { - var bannerType2 = reader.ReadElementContentAsString() ?? string.Empty; - - // Sometimes the resolution is stuffed in here - var resolutionParts = bannerType2.Split('x'); - - if (resolutionParts.Length == 2) - { - int rval; - - if (int.TryParse(resolutionParts[0], NumberStyles.Integer, _usCulture, out rval)) - { - width = rval; - } - - if (int.TryParse(resolutionParts[1], NumberStyles.Integer, _usCulture, out rval)) - { - height = rval; - } - - } - - break; - } - - case "Season": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) - { - bannerSeason = int.Parse(val); - } - break; - } - - - default: - reader.Skip(); - break; - } - } - } - - if (!string.IsNullOrEmpty(url) && !bannerSeason.HasValue) - { - var imageInfo = new RemoteImageInfo - { - RatingType = RatingType.Score, - CommunityRating = rating, - VoteCount = voteCount, - Url = TVUtils.BannerUrl + url, - ProviderName = Name, - Language = language, - Width = width, - Height = height - }; - - if (!string.IsNullOrEmpty(thumbnailUrl)) - { - imageInfo.ThumbnailUrl = TVUtils.BannerUrl + thumbnailUrl; - } - - if (string.Equals(bannerType, "poster", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Primary; - images.Add(imageInfo); - } - else if (string.Equals(bannerType, "series", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Banner; - images.Add(imageInfo); - } - else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase)) - { - imageInfo.Type = ImageType.Backdrop; - images.Add(imageInfo); - } - } - - } - - public int Order - { - get { return 0; } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool - }); - } - - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) - { - if (!TvdbSeriesProvider.Current.GetTvDbOptions().EnableAutomaticUpdates) - { - return false; - } - - if (TvdbSeriesProvider.IsValidSeries(item.ProviderIds)) - { - // Process images - var imagesXmlPath = Path.Combine(TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, item.ProviderIds), "banners.xml"); - - var fileInfo = _fileSystem.GetFileInfo(imagesXmlPath); - - return fileInfo.Exists && _fileSystem.GetLastWriteTimeUtc(fileInfo) > (status.DateLastMetadataRefresh ?? DateTime.MinValue); - } - - return false; - } - } -} diff --git a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs deleted file mode 100644 index 593507fb2a..0000000000 --- a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs +++ /dev/null @@ -1,1469 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Providers; -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; -using CommonIO; - -namespace MediaBrowser.Providers.TV -{ - public class TvdbSeriesProvider : IRemoteMetadataProvider, IItemIdentityProvider, IHasOrder - { - private const string TvdbSeriesOffset = "TvdbSeriesOffset"; - private const string TvdbSeriesOffsetFormat = "{0}-{1}"; - - internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2); - internal static TvdbSeriesProvider Current { get; private set; } - private readonly IZipClient _zipClient; - private readonly IHttpClient _httpClient; - private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _config; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private readonly ILogger _logger; - private readonly ISeriesOrderManager _seriesOrder; - private readonly ILibraryManager _libraryManager; - - public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ISeriesOrderManager seriesOrder, ILibraryManager libraryManager) - { - _zipClient = zipClient; - _httpClient = httpClient; - _fileSystem = fileSystem; - _config = config; - _logger = logger; - _seriesOrder = seriesOrder; - _libraryManager = libraryManager; - Current = this; - } - - private const string SeriesSearchUrl = "http://www.thetvdb.com/api/GetSeries.php?seriesname={0}&language={1}"; - private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip"; - private const string GetSeriesByImdbId = "http://www.thetvdb.com/api/GetSeriesByRemoteID.php?imdbid={0}&language={1}"; - - private string NormalizeLanguage(string language) - { - if (string.IsNullOrWhiteSpace(language)) - { - return language; - } - - // pt-br is just pt to tvdb - return language.Split('-')[0].ToLower(); - } - - public async Task> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) - { - if (IsValidSeries(searchInfo.ProviderIds)) - { - var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false); - - if (metadata.HasMetadata) - { - return new List - { - new RemoteSearchResult - { - Name = metadata.Item.Name, - PremiereDate = metadata.Item.PremiereDate, - ProductionYear = metadata.Item.ProductionYear, - ProviderIds = metadata.Item.ProviderIds, - SearchProviderName = Name - } - }; - } - } - - return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken) - { - var result = new MetadataResult(); - - if (!IsValidSeries(itemId.ProviderIds)) - { - await Identify(itemId).ConfigureAwait(false); - } - - cancellationToken.ThrowIfCancellationRequested(); - - if (IsValidSeries(itemId.ProviderIds)) - { - await EnsureSeriesInfo(itemId.ProviderIds, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false); - - result.Item = new Series(); - result.HasMetadata = true; - - FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken); - await FindAnimeSeriesIndex(result.Item, itemId).ConfigureAwait(false); - } - - return result; - } - - private async Task FindAnimeSeriesIndex(Series series, SeriesInfo info) - { - var index = await _seriesOrder.FindSeriesIndex(SeriesOrderTypes.Anime, series.Name); - if (index == null) - return; - - var offset = info.AnimeSeriesIndex - index; - var id = string.Format(TvdbSeriesOffsetFormat, series.GetProviderId(MetadataProviders.Tvdb), offset); - series.SetProviderId(TvdbSeriesOffset, id); - } - - internal static int? GetSeriesOffset(Dictionary seriesProviderIds) - { - string idString; - if (!seriesProviderIds.TryGetValue(TvdbSeriesOffset, out idString)) - return null; - - var parts = idString.Split('-'); - if (parts.Length < 2) - return null; - - int offset; - if (int.TryParse(parts[1], out offset)) - return offset; - - return null; - } - - /// - /// Fetches the series data. - /// - /// The result. - /// The metadata language. - /// The series provider ids. - /// The cancellation token. - /// Task{System.Boolean}. - private void FetchSeriesData(MetadataResult result, string metadataLanguage, Dictionary seriesProviderIds, CancellationToken cancellationToken) - { - var series = result.Item; - - string id; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id)) - { - series.SetProviderId(MetadataProviders.Tvdb, id); - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) - { - series.SetProviderId(MetadataProviders.Imdb, id); - } - - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - var seriesXmlPath = GetSeriesXmlPath(seriesProviderIds, metadataLanguage); - var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml"); - - FetchSeriesInfo(series, seriesXmlPath, cancellationToken); - - cancellationToken.ThrowIfCancellationRequested(); - - result.ResetPeople(); - - FetchActors(result, actorsXmlPath); - } - - /// - /// Downloads the series zip. - /// - /// The series id. - /// Type of the identifier. - /// The series data path. - /// The last tv database update time. - /// The preferred metadata language. - /// The cancellation token. - /// Task. - /// seriesId - internal async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(seriesId)) - { - throw new ArgumentNullException("seriesId"); - } - - try - { - await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - return; - } - catch (HttpException ex) - { - if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound) - { - throw; - } - } - - if (!string.Equals(preferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase)) - { - await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, "en", preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - } - } - - private async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, string saveAsMetadataLanguage, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(seriesId)) - { - throw new ArgumentNullException("seriesId"); - } - - if (!string.Equals(idType, "tvdb", StringComparison.OrdinalIgnoreCase)) - { - seriesId = await GetSeriesByRemoteId(seriesId, idType, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - } - - if (string.IsNullOrWhiteSpace(seriesId)) - { - throw new ArgumentNullException("seriesId"); - } - - var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, NormalizeLanguage(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.ExtractAllFromZip(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); - } - - var downloadLangaugeXmlFile = Path.Combine(seriesDataPath, NormalizeLanguage(preferredMetadataLanguage) + ".xml"); - var saveAsLanguageXmlFile = Path.Combine(seriesDataPath, saveAsMetadataLanguage + ".xml"); - - if (!string.Equals(downloadLangaugeXmlFile, saveAsLanguageXmlFile, StringComparison.OrdinalIgnoreCase)) - { - _fileSystem.CopyFile(downloadLangaugeXmlFile, saveAsLanguageXmlFile, true); - } - - await ExtractEpisodes(seriesDataPath, downloadLangaugeXmlFile, lastTvDbUpdateTime).ConfigureAwait(false); - } - - private async Task GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken) - { - var url = string.Format(GetSeriesByImdbId, id, NormalizeLanguage(language)); - - using (var result = await _httpClient.Get(new HttpRequestOptions - { - Url = url, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken - - }).ConfigureAwait(false)) - { - var doc = new XmlDocument(); - doc.Load(result); - - if (doc.HasChildNodes) - { - var node = doc.SelectSingleNode("//Series/seriesid"); - - if (node != null) - { - var idResult = node.InnerText; - - _logger.Info("Tvdb GetSeriesByRemoteId produced id of {0}", idResult ?? string.Empty); - - return idResult; - } - } - } - - return null; - } - - public TvdbOptions GetTvDbOptions() - { - return _config.GetConfiguration("tvdb"); - } - - internal static bool IsValidSeries(Dictionary seriesProviderIds) - { - string id; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id) && !string.IsNullOrEmpty(id)) - { - // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. - if (!string.IsNullOrWhiteSpace(id)) - { - return true; - } - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id)) - { - // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. - if (!string.IsNullOrWhiteSpace(id)) - { - return true; - } - } - return false; - } - - private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); - internal async Task EnsureSeriesInfo(Dictionary seriesProviderIds, string preferredMetadataLanguage, CancellationToken cancellationToken) - { - await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - string seriesId; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - // Only download if not already there - // The post-scan task will take care of updates so we don't need to re-download here - if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) - { - await DownloadSeriesZip(seriesId, MetadataProviders.Tvdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - } - - return seriesDataPath; - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - // Only download if not already there - // The post-scan task will take care of updates so we don't need to re-download here - if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) - { - await DownloadSeriesZip(seriesId, MetadataProviders.Imdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); - } - - return seriesDataPath; - } - - return null; - } - finally - { - _ensureSemaphore.Release(); - } - } - - private bool IsCacheValid(string seriesDataPath, string preferredMetadataLanguage) - { - try - { - var files = _fileSystem.GetFiles(seriesDataPath) - .ToList(); - - var seriesXmlFilename = preferredMetadataLanguage + ".xml"; - - var automaticUpdatesEnabled = GetTvDbOptions().EnableAutomaticUpdates; - - const int cacheDays = 1; - - var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase)); - // No need to check age if automatic updates are enabled - if (seriesFile == null || !seriesFile.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(seriesFile)).TotalDays > cacheDays)) - { - return false; - } - - var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase)); - // No need to check age if automatic updates are enabled - if (actorsXml == null || !actorsXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(actorsXml)).TotalDays > cacheDays)) - { - return false; - } - - var bannersXml = files.FirstOrDefault(i => string.Equals("banners.xml", i.Name, StringComparison.OrdinalIgnoreCase)); - // No need to check age if automatic updates are enabled - if (bannersXml == null || !bannersXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(bannersXml)).TotalDays > cacheDays)) - { - return false; - } - return true; - } - catch (DirectoryNotFoundException) - { - return false; - } - catch (FileNotFoundException) - { - return false; - } - } - - /// - /// Finds the series. - /// - /// The name. - /// The year. - /// The language. - /// The cancellation token. - /// Task{System.String}. - private async Task> FindSeries(string name, int? year, string language, CancellationToken cancellationToken) - { - var results = (await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false)).ToList(); - - if (results.Count == 0) - { - var parsedName = _libraryManager.ParseName(name); - var nameWithoutYear = parsedName.Name; - - if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase)) - { - results = (await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false)).ToList(); - } - } - - return results.Where(i => - { - if (year.HasValue && i.ProductionYear.HasValue) - { - // Allow one year tolerance - return Math.Abs(year.Value - i.ProductionYear.Value) <= 1; - } - - return true; - }); - } - - private async Task> FindSeriesInternal(string name, string language, CancellationToken cancellationToken) - { - var url = string.Format(SeriesSearchUrl, WebUtility.UrlEncode(name), NormalizeLanguage(language)); - var doc = new XmlDocument(); - - using (var results = await _httpClient.Get(new HttpRequestOptions - { - Url = url, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken - - }).ConfigureAwait(false)) - { - doc.Load(results); - } - - var searchResults = new List(); - - if (doc.HasChildNodes) - { - var nodes = doc.SelectNodes("//Series"); - var comparableName = GetComparableName(name); - if (nodes != null) - { - foreach (XmlNode node in nodes) - { - var searchResult = new RemoteSearchResult - { - SearchProviderName = Name - }; - - 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); - } - - var imdbIdNode = node.SelectSingleNode("./IMDB_ID"); - if (imdbIdNode != null) - { - var val = imdbIdNode.InnerText; - if (!string.IsNullOrWhiteSpace(val)) - { - searchResult.SetProviderId(MetadataProviders.Imdb, val); - } - } - - var bannerNode = node.SelectSingleNode("./banner"); - if (bannerNode != null) - { - var val = bannerNode.InnerText; - if (!string.IsNullOrWhiteSpace(val)) - { - searchResult.ImageUrl = TVUtils.BannerUrl + val; - } - } - - var airDateNode = node.SelectSingleNode("./FirstAired"); - if (airDateNode != null) - { - var val = airDateNode.InnerText; - if (!string.IsNullOrWhiteSpace(val)) - { - DateTime date; - if (DateTime.TryParse(val, out date)) - { - searchResult.ProductionYear = date.Year; - } - } - } - - foreach (var title in titles) - { - if (string.Equals(title, comparableName, StringComparison.OrdinalIgnoreCase)) - { - var id = node.SelectSingleNode("./seriesid") ?? - node.SelectSingleNode("./id"); - - if (id != null) - { - searchResult.Name = title; - searchResult.SetProviderId(MetadataProviders.Tvdb, id.InnerText); - searchResults.Add(searchResult); - } - break; - } - _logger.Info("TVDb Provider - " + title + " did not match " + comparableName); - } - } - } - } - - if (searchResults.Count == 0) - { - _logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org."); - } - - return searchResults; - } - - /// - /// 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(); - } - - 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 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 result. - /// The actors XML path. - private void FetchActors(MetadataResult result, string actorsXmlPath) - { - 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()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Actor": - { - using (var subtree = reader.ReadSubtree()) - { - FetchDataFromActorNode(result, subtree); - } - break; - } - default: - reader.Skip(); - break; - } - } - } - } - } - } - - /// - /// Fetches the data from actor node. - /// - /// The result. - /// The reader. - private void FetchDataFromActorNode(MetadataResult result, 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; - } - - case "id": - { - break; - } - - case "Image": - { - var url = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - if (!string.IsNullOrWhiteSpace(url)) - { - personInfo.ImageUrl = TVUtils.BannerUrl + url; - } - break; - } - - case "SortOrder": - { - 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)) - { - personInfo.SortOrder = rval; - } - } - break; - } - - default: - reader.Skip(); - break; - } - } - } - - personInfo.Type = PersonType.Actor; - - if (!string.IsNullOrWhiteSpace(personInfo.Name)) - { - result.AddPerson(personInfo); - } - } - - private void FetchDataFromSeriesNode(Series item, XmlReader reader, CancellationToken cancellationToken) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "SeriesName": - { - item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "Overview": - { - item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); - break; - } - - case "Airs_DayOfWeek": - { - var val = reader.ReadElementContentAsString(); - - 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)) - { - item.OfficialRating = 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 "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)) - { - 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)) - { - 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)) - { - 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; - } - } - } - } - - /// - /// 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.IsNullOrWhiteSpace(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 || !_fileSystem.FileExists(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 || !_fileSystem.FileExists(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 provider ids. - /// System.String. - internal static string GetSeriesDataPath(IApplicationPaths appPaths, Dictionary seriesProviderIds) - { - string seriesId; - if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); - - return seriesDataPath; - } - - if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out seriesId) && !string.IsNullOrEmpty(seriesId)) - { - var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); - - return seriesDataPath; - } - - return null; - } - - public string GetSeriesXmlPath(Dictionary seriesProviderIds, string language) - { - var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesProviderIds); - - var seriesXmlFilename = language.ToLower() + ".xml"; - - return Path.Combine(seriesDataPath, seriesXmlFilename); - } - - /// - /// Gets the series data path. - /// - /// The app paths. - /// System.String. - internal static string GetSeriesDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "tvdb"); - - return dataPath; - } - - private void DeleteXmlFiles(string path) - { - try - { - foreach (var file in _fileSystem.GetFilePaths(path, true) - .ToList()) - { - _fileSystem.DeleteFile(file); - } - } - 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(); - } - - public string Name - { - get { return "TheTVDB"; } - } - - public async Task Identify(SeriesInfo info) - { - if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProviders.Tvdb))) - { - return; - } - - var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None).ConfigureAwait(false); - - var entry = srch.FirstOrDefault(); - - if (entry != null) - { - var id = entry.GetProviderId(MetadataProviders.Tvdb); - info.SetProviderId(MetadataProviders.Tvdb, id); - } - } - - public int Order - { - get - { - // After Omdb - return 1; - } - } - - public Task GetImageResponse(string url, CancellationToken cancellationToken) - { - return _httpClient.GetResponse(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = url, - ResourcePool = TvDbResourcePool - }); - } - } - - public class TvdbConfigStore : IConfigurationFactory - { - public IEnumerable GetConfigurations() - { - return new List - { - new ConfigurationStore - { - Key = "tvdb", - ConfigurationType = typeof(TvdbOptions) - } - }; - } - } -} \ No newline at end of file -- cgit v1.2.3