diff --git a/MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs b/MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs index 287980fc71..a2f7b153f1 100644 --- a/MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs +++ b/MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs @@ -1,6 +1,7 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -21,6 +22,12 @@ namespace MediaBrowser.Api.DefaultTheme { [ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] public Guid UserId { get; set; } + + [ApiMember(Name = "ComedyGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ComedyGenre { get; set; } + + [ApiMember(Name = "RomanceGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string RomanceGenre { get; set; } } [Route("/MBT/DefaultTheme/Movies", "GET")] @@ -31,6 +38,19 @@ namespace MediaBrowser.Api.DefaultTheme [ApiMember(Name = "FamilyRating", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string FamilyRating { get; set; } + + [ApiMember(Name = "ComedyGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string ComedyGenre { get; set; } + + [ApiMember(Name = "RomanceGenre", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string RomanceGenre { get; set; } + } + + [Route("/MBT/DefaultTheme/Home", "GET")] + public class GetHomeView : IReturn + { + [ApiMember(Name = "UserId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public Guid UserId { get; set; } } public class DefaultThemeService : BaseApiService @@ -51,6 +71,39 @@ namespace MediaBrowser.Api.DefaultTheme _localization = localization; } + public object Get(GetHomeView request) + { + var result = GetHomeView(request).Result; + + return ToOptimizedResult(result); + } + + private async Task GetHomeView(GetHomeView request) + { + var user = _userManager.GetUserById(request.UserId); + + var allItems = user.RootFolder.GetRecursiveChildren(user) + .ToList(); + + var itemsWithBackdrops = allItems.Where(i => i.BackdropImagePaths.Count > 0).ToList(); + + var view = new HomeView(); + + var fields = new List(); + + var eligibleSpotlightItems = itemsWithBackdrops + .Where(i => i is Game || i is Movie || i is Series || i is MusicArtist); + + var spotlightItemTasks = FilterItemsForBackdropDisplay(eligibleSpotlightItems) + .OrderBy(i => Guid.NewGuid()) + .Take(50) + .Select(i => _dtoService.GetBaseItemDto(i, fields, user)); + + view.SpotlightItems = await Task.WhenAll(spotlightItemTasks).ConfigureAwait(false); + + return view; + } + public object Get(GetTvView request) { var result = GetTvView(request).Result; @@ -72,10 +125,9 @@ namespace MediaBrowser.Api.DefaultTheme var fields = new List(); - var spotlightItemTasks = seriesWithBackdrops - .OrderByDescending(i => GetResolution(i, i.BackdropImagePaths[0])) - .Take(60) + var spotlightItemTasks = FilterItemsForBackdropDisplay(seriesWithBackdrops) .OrderBy(i => Guid.NewGuid()) + .Take(50) .Select(i => _dtoService.GetBaseItemDto(i, fields, user)); view.SpotlightItems = await Task.WhenAll(spotlightItemTasks).ConfigureAwait(false); @@ -88,6 +140,25 @@ namespace MediaBrowser.Api.DefaultTheme .Take(3) .ToArray(); + var romanceGenres = request.RomanceGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + var comedyGenres = request.ComedyGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + + view.RomanceItems = seriesWithBackdrops + .Where(i => i.Genres.Any(romanceGenres.ContainsKey)) + .OrderBy(i => Guid.NewGuid()) + .Select(i => GetItemStub(i, ImageType.Backdrop)) + .Where(i => i != null) + .Take(3) + .ToArray(); + + view.ComedyItems = seriesWithBackdrops + .Where(i => i.Genres.Any(comedyGenres.ContainsKey)) + .OrderBy(i => Guid.NewGuid()) + .Select(i => GetItemStub(i, ImageType.Backdrop)) + .Where(i => i != null) + .Take(3) + .ToArray(); + view.ActorItems = await GetActors(series).ConfigureAwait(false); return view; @@ -137,10 +208,9 @@ namespace MediaBrowser.Api.DefaultTheme var fields = new List(); - var spotlightItemTasks = itemsWithBackdrops - .OrderByDescending(i => GetResolution(i, i.BackdropImagePaths[0])) - .Take(60) + var spotlightItemTasks = FilterItemsForBackdropDisplay(itemsWithBackdrops) .OrderBy(i => Guid.NewGuid()) + .Take(50) .Select(i => _dtoService.GetBaseItemDto(i, fields, user)); view.SpotlightItems = await Task.WhenAll(spotlightItemTasks).ConfigureAwait(false); @@ -178,9 +248,10 @@ namespace MediaBrowser.Api.DefaultTheme .Take(3) .ToArray(); - var romanceGenres = new[] { "romance" }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + var romanceGenres = request.RomanceGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + var comedyGenres = request.ComedyGenre.Split(',').ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - view.RomanticItems = moviesWithBackdrops + view.RomanceItems = moviesWithBackdrops .Where(i => i.Genres.Any(romanceGenres.ContainsKey)) .OrderBy(i => Guid.NewGuid()) .Select(i => GetItemStub(i, ImageType.Backdrop)) @@ -188,6 +259,14 @@ namespace MediaBrowser.Api.DefaultTheme .Take(3) .ToArray(); + view.ComedyItems = moviesWithBackdrops + .Where(i => i.Genres.Any(comedyGenres.ContainsKey)) + .OrderBy(i => Guid.NewGuid()) + .Select(i => GetItemStub(i, ImageType.Backdrop)) + .Where(i => i != null) + .Take(3) + .ToArray(); + view.HDItems = hdMovies .Where(i => i.BackdropImagePaths.Count > 0) .OrderBy(i => Guid.NewGuid()) @@ -209,6 +288,25 @@ namespace MediaBrowser.Api.DefaultTheme return view; } + private IEnumerable FilterItemsForBackdropDisplay(IEnumerable items) + { + var tuples = items + .Select(i => new Tuple(i, GetResolution(i, i.BackdropImagePaths[0]))) + .Where(i => i.Item2 > 0) + .ToList(); + + var topItems = tuples + .Where(i => i.Item2 >= 1920) + .ToList(); + + if (topItems.Count >= 10) + { + return topItems.Select(i => i.Item1); + } + + return tuples.Select(i => i.Item1); + } + private double GetResolution(BaseItem item, string path) { try diff --git a/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs b/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs index 3e01ad5be2..f6c0c9208c 100644 --- a/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs +++ b/MediaBrowser.Providers/Movies/OpenMovieDatabaseProvider.cs @@ -1,5 +1,4 @@ -using System.Linq; -using MediaBrowser.Common.Net; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -10,6 +9,7 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -46,7 +46,7 @@ namespace MediaBrowser.Providers.Movies { get { - return "8"; + return "9"; } } diff --git a/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs b/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs index 1ea2c1db5f..de9e5c0185 100644 --- a/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs +++ b/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs @@ -5,10 +5,9 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Net; using System; using System.IO; -using System.Net; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -77,7 +76,7 @@ namespace MediaBrowser.Providers.TV return ItemUpdateType.ImageUpdate; } } - + /// /// Gets a value indicating whether [refresh on version change]. /// @@ -98,7 +97,7 @@ namespace MediaBrowser.Providers.TV { get { - return "1"; + return "2"; } } @@ -119,7 +118,7 @@ namespace MediaBrowser.Providers.TV return imagesFileInfo.LastWriteTimeUtc; } } - + return base.CompareDate(item); } @@ -138,22 +137,21 @@ namespace MediaBrowser.Providers.TV var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; - if (!string.IsNullOrEmpty(seriesId)) + var seasonNumber = season.IndexNumber; + + if (!string.IsNullOrEmpty(seriesId) && seasonNumber.HasValue) { // Process images var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); - var imagesFileInfo = new FileInfo(imagesXmlPath); - - if (imagesFileInfo.Exists) + try { - if (!season.HasImage(ImageType.Primary) || !season.HasImage(ImageType.Banner) || season.BackdropImagePaths.Count == 0) - { - var xmlDoc = new XmlDocument(); - xmlDoc.Load(imagesXmlPath); - - await FetchImages(season, xmlDoc, cancellationToken).ConfigureAwait(false); - } + var fanartData = FetchFanartXmlData(imagesXmlPath, seasonNumber.Value, cancellationToken); + await DownloadImages(item, fanartData, ConfigurationManager.Configuration.MaxBackdrops, cancellationToken).ConfigureAwait(false); + } + catch (FileNotFoundException) + { + // No biggie. Not all series have images } SetLastRefreshed(item, DateTime.UtcNow); @@ -163,86 +161,169 @@ namespace MediaBrowser.Providers.TV return false; } - /// - /// Fetches the images. - /// - /// The season. - /// The images. - /// The cancellation token. - /// Task. - private async Task FetchImages(Season season, XmlDocument images, CancellationToken cancellationToken) + private async Task DownloadImages(BaseItem item, FanartXmlData data, int backdropLimit, CancellationToken cancellationToken) { - var seasonNumber = season.IndexNumber ?? -1; + if (!item.HasImage(ImageType.Primary)) + { + var url = data.LanguagePoster ?? data.Poster; + if (!string.IsNullOrEmpty(url)) + { + url = TVUtils.BannerUrl + url; - if (seasonNumber == -1) + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) + .ConfigureAwait(false); + } + } + + if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !item.HasImage(ImageType.Banner)) { - return; + var url = data.LanguageBanner ?? data.Banner; + if (!string.IsNullOrEmpty(url)) + { + url = TVUtils.BannerUrl + url; + + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) + .ConfigureAwait(false); + } } - if (!season.HasImage(ImageType.Primary)) + if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && item.BackdropImagePaths.Count == 0) { - var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='season'][Season='" + seasonNumber + "'][Language='" + ConfigurationManager.Configuration.PreferredMetadataLanguage + "']") ?? - images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='season'][Season='" + seasonNumber + "'][Language='en']"); - if (n != null) + var bdNo = item.BackdropImagePaths.Count; + + foreach (var backdrop in data.Backdrops) { - n = n.SelectSingleNode("./BannerPath"); + var url = TVUtils.BannerUrl + backdrop; - if (n != null) - { - var url = TVUtils.BannerUrl + n.InnerText; + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken) + .ConfigureAwait(false); - await _providerManager.SaveImage(season, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) - .ConfigureAwait(false); - } + bdNo++; + + if (item.BackdropImagePaths.Count >= backdropLimit) break; } } + } + + private FanartXmlData FetchFanartXmlData(string bannersXmlPath, int seasonNumber, CancellationToken cancellationToken) + { + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; - if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !season.HasImage(ImageType.Banner)) + var data = new FanartXmlData(); + + using (var streamReader = new StreamReader(bannersXmlPath, Encoding.UTF8)) { - var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='seasonwide'][Season='" + seasonNumber + "'][Language='" + ConfigurationManager.Configuration.PreferredMetadataLanguage + "']") ?? - images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='seasonwide'][Season='" + seasonNumber + "'][Language='en']"); - if (n != null) + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, settings)) { - n = n.SelectSingleNode("./BannerPath"); - if (n != null) + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) { - try - { - var url = TVUtils.BannerUrl + n.InnerText; + cancellationToken.ThrowIfCancellationRequested(); - await _providerManager.SaveImage(season, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) - .ConfigureAwait(false); - } - catch (HttpException ex) + if (reader.NodeType == XmlNodeType.Element) { - Logger.ErrorException("Error downloading season banner for {0}", ex, season.Path); - - // Sometimes banners will come up not found even though they're reported in tvdb xml - if (ex.StatusCode.HasValue && ex.StatusCode.Value != HttpStatusCode.NotFound) + switch (reader.Name) { - throw; + case "Banner": + { + using (var subtree = reader.ReadSubtree()) + { + FetchInfoFromBannerNode(data, subtree, seasonNumber); + } + break; + } + default: + reader.Skip(); + break; } } } } } - if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && season.BackdropImagePaths.Count == 0) + return data; + } + + private void FetchInfoFromBannerNode(FanartXmlData data, XmlReader reader, int seasonNumber) + { + reader.MoveToContent(); + + string bannerType = null; + string bannerType2 = null; + string url = null; + int? bannerSeason = null; + + while (reader.Read()) { - var n = images.SelectSingleNode("//Banner[BannerType='fanart'][Season='" + seasonNumber + "']"); - if (n != null) + if (reader.NodeType == XmlNodeType.Element) { - n = n.SelectSingleNode("./BannerPath"); - if (n != null) + switch (reader.Name) { - var url = TVUtils.BannerUrl + n.InnerText; + case "BannerType": + { + bannerType = reader.ReadElementContentAsString() ?? string.Empty; + break; + } + + case "BannerType2": + { + bannerType2 = reader.ReadElementContentAsString() ?? string.Empty; + break; + } + + case "BannerPath": + { + url = reader.ReadElementContentAsString() ?? string.Empty; + break; + } - await _providerManager.SaveImage(season, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, 0, cancellationToken) - .ConfigureAwait(false); + 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) + { + if (string.Equals(bannerType, "season", StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(bannerType2, "season", StringComparison.OrdinalIgnoreCase)) + { + data.Poster = url; + } + else if (string.Equals(bannerType2, "seasonwide", StringComparison.OrdinalIgnoreCase)) + { + data.Banner = url; } } + else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase)) + { + data.Backdrops.Add(url); + } } } + } } diff --git a/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs b/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs index 34be77a9ea..b79e50e205 100644 --- a/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs @@ -247,10 +247,7 @@ namespace MediaBrowser.Providers.TV if (!series.LockedFields.Contains(MetadataFields.Cast)) { - var actorsDoc = new XmlDocument(); - actorsDoc.Load(actorsXmlPath); - - FetchActors(series, actorsDoc, seriesDoc); + FetchActors(series, actorsXmlPath, cancellationToken); } } } @@ -384,8 +381,11 @@ namespace MediaBrowser.Providers.TV } } } + series.OfficialRating = doc.SafeGetString("//ContentRating"); - if (!series.LockedFields.Contains(MetadataFields.Genres)) + + // Only fill this in if there's no existing genres, because Imdb data from Omdb is preferred + if (!series.LockedFields.Contains(MetadataFields.Genres) && (series.Genres.Count == 0 || !string.Equals(ConfigurationManager.Configuration.PreferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase))) { string g = doc.SafeGetString("//Genre"); @@ -429,31 +429,94 @@ namespace MediaBrowser.Providers.TV /// Fetches the actors. /// /// The series. - /// The actors doc. - /// The seriesDoc. - /// Task. - private void FetchActors(Series series, XmlDocument actorsDoc, XmlDocument seriesDoc) + /// The actors XML path. + /// The cancellation token. + private void FetchActors(Series series, string actorsXmlPath, CancellationToken cancellationToken) { - var xmlNodeList = actorsDoc.SelectNodes("Actors/Actor"); - - if (xmlNodeList != null) + var settings = new XmlReaderSettings { - series.People.Clear(); + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; - foreach (XmlNode p in xmlNodeList) + using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8)) + { + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, settings)) { - string actorName = p.SafeGetString("Name"); - string actorRole = p.SafeGetString("Role"); + reader.MoveToContent(); - if (!string.IsNullOrWhiteSpace(actorName)) + // Loop through each element + while (reader.Read()) { - // Sometimes tvdb actors have leading spaces - series.AddPerson(new PersonInfo { Type = PersonType.Actor, Name = actorName.Trim(), Role = actorRole }); + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Actor": + { + using (var subtree = reader.ReadSubtree()) + { + FetchDataFromActorNode(series, subtree); + } + break; + } + default: + reader.Skip(); + break; + } + } } } } } + /// + /// Fetches the data from actor node. + /// + /// The series. + /// The reader. + private void FetchDataFromActorNode(Series series, XmlReader reader) + { + reader.MoveToContent(); + + var personInfo = new PersonInfo(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Name": + { + personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + + case "Role": + { + personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + + default: + reader.Skip(); + break; + } + } + } + + if (!string.IsNullOrEmpty(personInfo.Name)) + { + series.AddPerson(personInfo); + } + } + /// /// The us culture /// diff --git a/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs index bcdd03fc10..2f1ae8e987 100644 --- a/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbPersonImageProvider.cs @@ -5,10 +5,10 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; -using MediaBrowser.Providers.Extensions; using System; using System.IO; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -87,38 +87,114 @@ namespace MediaBrowser.Providers.TV var actorXmlPath = Path.Combine(tvdbPath, "actors.xml"); - var xmlDoc = new XmlDocument(); + var url = FetchImageUrl(item, actorXmlPath, cancellationToken); - xmlDoc.Load(actorXmlPath); - - var actorNodes = xmlDoc.SelectNodes("//Actor"); - - if (actorNodes == null) + if (!string.IsNullOrEmpty(url)) { - return; - } + url = TVUtils.BannerUrl + url; - foreach (var actorNode in actorNodes.OfType()) + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, + ImageType.Primary, null, cancellationToken).ConfigureAwait(false); + } + } + private string FetchImageUrl(BaseItem item, string actorsXmlPath, CancellationToken cancellationToken) + { + var settings = new XmlReaderSettings { - var name = actorNode.SafeGetString("Name"); + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; - if (string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase)) + using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8)) + { + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, settings)) { - var image = actorNode.SafeGetString("Image"); + reader.MoveToContent(); - if (!string.IsNullOrEmpty(image)) + // Loop through each element + while (reader.Read()) { - var url = TVUtils.BannerUrl + image; - - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, - ImageType.Primary, null, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Actor": + { + using (var subtree = reader.ReadSubtree()) + { + var url = FetchImageUrlFromActorNode(item, subtree); + + if (!string.IsNullOrEmpty(url)) + { + return url; + } + } + break; + } + default: + reader.Skip(); + break; + } + } } + } + } - break; + return null; + } + + /// + /// Fetches the data from actor node. + /// + /// The item. + /// The reader. + private string FetchImageUrlFromActorNode(BaseItem item, XmlReader reader) + { + reader.MoveToContent(); + + string name = null; + string image = null; + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Name": + { + name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + + case "Image": + { + image = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + break; + } + + default: + reader.Skip(); + break; + } } } + + if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(image) && + string.Equals(name, item.Name, StringComparison.OrdinalIgnoreCase)) + { + return image; + } + + return null; } + public override MetadataProviderPriority Priority { get { return MetadataProviderPriority.Third; } diff --git a/MediaBrowser.Providers/TV/TvdbPrescanTask.cs b/MediaBrowser.Providers/TV/TvdbPrescanTask.cs index 2e6038b765..e6aa07be0a 100644 --- a/MediaBrowser.Providers/TV/TvdbPrescanTask.cs +++ b/MediaBrowser.Providers/TV/TvdbPrescanTask.cs @@ -11,7 +11,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; -using MediaBrowser.Providers.Extensions; namespace MediaBrowser.Providers.TV { @@ -102,11 +101,7 @@ namespace MediaBrowser.Providers.TV }).ConfigureAwait(false)) { - var doc = new XmlDocument(); - - doc.Load(stream); - - newUpdateTime = doc.SafeGetString("//Time"); + newUpdateTime = GetUpdateTime(stream); } await UpdateSeries(existingDirectories, path, progress, cancellationToken).ConfigureAwait(false); @@ -124,6 +119,51 @@ namespace MediaBrowser.Providers.TV 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. /// @@ -143,23 +183,65 @@ namespace MediaBrowser.Providers.TV }).ConfigureAwait(false)) { - var doc = new XmlDocument(); + var data = GetUpdatedSeriesIdList(stream); - doc.Load(stream); + var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - var newUpdateTime = doc.SafeGetString("//Time"); + var seriesList = data.Item1 + .Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); - var seriesNodes = doc.SelectNodes("//Series"); + return new Tuple, string>(seriesList, data.Item2); + } + } - var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + private Tuple, string> GetUpdatedSeriesIdList(Stream stream) + { + string updateTime = null; + var idList = new List(); - var seriesList = seriesNodes == null ? new string[] { } : - seriesNodes.Cast() - .Select(i => i.InnerText) - .Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; - return new Tuple, string>(seriesList, newUpdateTime); + 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); } /// @@ -217,7 +299,7 @@ namespace MediaBrowser.Providers.TV { Directory.CreateDirectory(seriesDataPath); } - + return RemoteSeriesProvider.Current.DownloadSeriesZip(id, seriesDataPath, cancellationToken); } } diff --git a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs index 2ac201a56b..9e3799ab83 100644 --- a/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/TV/TvdbSeriesImageProvider.cs @@ -7,8 +7,10 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; +using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; @@ -86,7 +88,7 @@ namespace MediaBrowser.Providers.TV return ItemUpdateType.ImageUpdate; } } - + /// /// Gets a value indicating whether [refresh on version change]. /// @@ -127,7 +129,7 @@ namespace MediaBrowser.Providers.TV return imagesFileInfo.LastWriteTimeUtc; } } - + return base.CompareDate(item); } @@ -150,16 +152,18 @@ namespace MediaBrowser.Providers.TV // Process images var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); - var imagesFileInfo = new FileInfo(imagesXmlPath); - - if (imagesFileInfo.Exists) + if (!series.HasImage(ImageType.Primary) || !series.HasImage(ImageType.Banner) || series.BackdropImagePaths.Count == 0) { - if (!series.HasImage(ImageType.Primary) || !series.HasImage(ImageType.Banner) || series.BackdropImagePaths.Count == 0) - { - var xmlDoc = new XmlDocument(); - xmlDoc.Load(imagesXmlPath); + var backdropLimit = ConfigurationManager.Configuration.MaxBackdrops; - await FetchImages(series, xmlDoc, cancellationToken).ConfigureAwait(false); + try + { + var fanartData = FetchFanartXmlData(imagesXmlPath, backdropLimit, cancellationToken); + await DownloadImages(item, fanartData, backdropLimit, cancellationToken).ConfigureAwait(false); + } + catch (FileNotFoundException) + { + // No biggie. Not all series have images } } @@ -179,72 +183,183 @@ namespace MediaBrowser.Providers.TV protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - /// - /// Fetches the images. - /// - /// The series. - /// The images. - /// The cancellation token. - /// Task. - private async Task FetchImages(Series series, XmlDocument images, CancellationToken cancellationToken) + private async Task DownloadImages(BaseItem item, FanartXmlData data, int backdropLimit, CancellationToken cancellationToken) { - if (!series.HasImage(ImageType.Primary)) + if (!item.HasImage(ImageType.Primary)) { - var n = images.SelectSingleNode("//Banner[BannerType='poster']"); - if (n != null) + var url = data.LanguagePoster ?? data.Poster; + if (!string.IsNullOrEmpty(url)) { - n = n.SelectSingleNode("./BannerPath"); - if (n != null) - { - var url = TVUtils.BannerUrl + n.InnerText; + url = TVUtils.BannerUrl + url; - await _providerManager.SaveImage(series, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) - .ConfigureAwait(false); - } + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) + .ConfigureAwait(false); } } - if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !series.HasImage(ImageType.Banner)) + if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && !item.HasImage(ImageType.Banner)) { - var n = images.SelectSingleNode("//Banner[BannerType='series']"); - if (n != null) + var url = data.LanguageBanner ?? data.Banner; + if (!string.IsNullOrEmpty(url)) { - n = n.SelectSingleNode("./BannerPath"); - if (n != null) - { - var url = TVUtils.BannerUrl + n.InnerText; + url = TVUtils.BannerUrl + url; - await _providerManager.SaveImage(series, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) - .ConfigureAwait(false); - } + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) + .ConfigureAwait(false); + } + } + + if (ConfigurationManager.Configuration.DownloadSeriesImages.Backdrops && item.BackdropImagePaths.Count == 0) + { + var bdNo = item.BackdropImagePaths.Count; + + foreach (var backdrop in data.Backdrops) + { + var url = TVUtils.BannerUrl + backdrop; + + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken) + .ConfigureAwait(false); + + bdNo++; + + if (item.BackdropImagePaths.Count >= backdropLimit) break; } } + } - if (series.BackdropImagePaths.Count == 0) + private FanartXmlData FetchFanartXmlData(string bannersXmlPath, int backdropLimit, CancellationToken cancellationToken) + { + var settings = new XmlReaderSettings { - var bdNo = series.BackdropImagePaths.Count; + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + var data = new FanartXmlData(); - var xmlNodeList = images.SelectNodes("//Banner[BannerType='fanart']"); - if (xmlNodeList != null) + using (var streamReader = new StreamReader(bannersXmlPath, Encoding.UTF8)) + { + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, settings)) { - foreach (XmlNode b in xmlNodeList) + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) { - var p = b.SelectSingleNode("./BannerPath"); + cancellationToken.ThrowIfCancellationRequested(); - if (p != null) + if (reader.NodeType == XmlNodeType.Element) { - var url = TVUtils.BannerUrl + p.InnerText; - - await _providerManager.SaveImage(series, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken) - .ConfigureAwait(false); - - bdNo++; + switch (reader.Name) + { + case "Banner": + { + using (var subtree = reader.ReadSubtree()) + { + FetchInfoFromBannerNode(data, subtree, backdropLimit); + } + break; + } + default: + reader.Skip(); + break; + } } + } + } + } + + return data; + } + + private void FetchInfoFromBannerNode(FanartXmlData data, XmlReader reader, int backdropLimit) + { + reader.MoveToContent(); + + string type = null; + string url = null; + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "BannerType": + { + type = reader.ReadElementContentAsString() ?? string.Empty; + + if (string.Equals(type, "poster", StringComparison.OrdinalIgnoreCase)) + { + // Already got it + if (!string.IsNullOrEmpty(data.Poster)) + { + return; + } + } + else if (string.Equals(type, "series", StringComparison.OrdinalIgnoreCase)) + { + // Already got it + if (!string.IsNullOrEmpty(data.Banner)) + { + return; + } + } + else if (string.Equals(type, "fanart", StringComparison.OrdinalIgnoreCase)) + { + if (data.Backdrops.Count >= backdropLimit) + { + return; + } + } + else + { + return; + } - if (series.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops) break; + break; + } + + case "BannerPath": + { + url = reader.ReadElementContentAsString() ?? string.Empty; + break; + } + + default: + reader.Skip(); + break; } } } + + if (!string.IsNullOrEmpty(url)) + { + if (string.Equals(type, "poster", StringComparison.OrdinalIgnoreCase)) + { + data.Poster = url; + } + else if (string.Equals(type, "series", StringComparison.OrdinalIgnoreCase)) + { + data.Banner = url; + } + else if (string.Equals(type, "fanart", StringComparison.OrdinalIgnoreCase)) + { + data.Backdrops.Add(url); + } + } } } + + internal class FanartXmlData + { + public string LanguagePoster { get; set; } + public string LanguageBanner { get; set; } + public string Poster { get; set; } + public string Banner { get; set; } + public List Backdrops = new List(); + } }