diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 7b335b719..dbed0b489 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -718,7 +718,7 @@ namespace MediaBrowser.Controller.Entities { var newChildren = validChildren.Select(c => c.Item1).ToList(); - //that's all the new and changed ones - now see if there are any that are missing + // That's all the new and changed ones - now see if there are any that are missing var itemsRemoved = currentChildren.Values.Except(newChildren).ToList(); var actualRemovals = new List(); @@ -936,13 +936,14 @@ namespace MediaBrowser.Controller.Entities /// IEnumerable{BaseItem}. protected virtual IEnumerable GetNonCachedChildren() { + var resolveArgs = ResolveArgs; - if (ResolveArgs == null || ResolveArgs.FileSystemDictionary == null) + if (resolveArgs == null || resolveArgs.FileSystemDictionary == null) { - Logger.Error("Null for {0}", Path); + Logger.Error("ResolveArgs null for {0}", Path); } - return LibraryManager.ResolvePaths(ResolveArgs.FileSystemChildren, this); + return LibraryManager.ResolvePaths(resolveArgs.FileSystemChildren, this); } /// diff --git a/MediaBrowser.Model/Providers/RemoteImageInfo.cs b/MediaBrowser.Model/Providers/RemoteImageInfo.cs index 9a16d89d3..6db7f77bd 100644 --- a/MediaBrowser.Model/Providers/RemoteImageInfo.cs +++ b/MediaBrowser.Model/Providers/RemoteImageInfo.cs @@ -20,6 +20,11 @@ namespace MediaBrowser.Model.Providers /// The URL. public string Url { get; set; } + /// + /// Gets a url used for previewing a smaller version + /// + public string ThumbnailUrl { get; set; } + /// /// Gets or sets the height. /// diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9906ad14c..4f0f2a7d9 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -113,6 +113,7 @@ + diff --git a/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs index 3d56b3c71..003b40f6d 100644 --- a/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/TV/ManualTvdbEpisodeImageProvider.cs @@ -36,9 +36,11 @@ namespace MediaBrowser.Providers.TV return item is Episode; } - public Task> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + public async Task> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) { - return GetAllImages(item, cancellationToken); + var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); + + return images.Where(i => i.Type == imageType); } public Task> GetAllImages(BaseItem item, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs b/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs new file mode 100644 index 000000000..211ec5a84 --- /dev/null +++ b/MediaBrowser.Providers/TV/ManualTvdbSeasonImageProvider.cs @@ -0,0 +1,313 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +namespace MediaBrowser.Providers.TV +{ + public class ManualTvdbSeasonImageProvider : IImageProvider + { + private readonly IServerConfigurationManager _config; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public ManualTvdbSeasonImageProvider(IServerConfigurationManager config) + { + _config = config; + } + + public string Name + { + get { return ProviderName; } + } + + public static string ProviderName + { + get { return "TvDb"; } + } + + public bool Supports(BaseItem item) + { + return item is Season; + } + + public async Task> GetImages(BaseItem item, ImageType imageType, CancellationToken cancellationToken) + { + var images = await GetAllImages(item, cancellationToken).ConfigureAwait(false); + + return images.Where(i => i.Type == imageType); + } + + public Task> GetAllImages(BaseItem item, CancellationToken cancellationToken) + { + var season = (Season)item; + + var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId) && season.IndexNumber.HasValue) + { + // Process images + var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); + + var path = Path.Combine(seriesDataPath, "banners.xml"); + + try + { + var result = GetImages(path, season.IndexNumber.Value, cancellationToken); + + return Task.FromResult(result); + } + catch (FileNotFoundException) + { + // No tvdb data yet. Don't blow up + } + } + + return Task.FromResult>(new RemoteImageInfo[] { }); + } + + private IEnumerable GetImages(string xmlPath, 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 language = _config.Configuration.PreferredMetadataLanguage; + + var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase); + + return list.OrderByDescending(i => + { + if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 3; + } + if (!isLanguageEn) + { + if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase)) + { + return 2; + } + } + if (string.IsNullOrEmpty(i.Language)) + { + return isLanguageEn ? 3 : 2; + } + return 0; + }) + .ThenByDescending(i => i.CommunityRating ?? 0) + .ToList(); + } + + private void AddImage(XmlReader reader, List images, 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 = Name, + Language = language, + Width = width, + Height = height, + ThumbnailUrl = 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 Priority + { + get { return 0; } + } + } +} diff --git a/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs b/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs index ca6f699c3..744469847 100644 --- a/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs +++ b/MediaBrowser.Providers/TV/RemoteSeasonProvider.cs @@ -6,12 +6,13 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; using System; +using System.Collections.Generic; using System.IO; -using System.Text; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Xml; namespace MediaBrowser.Providers.TV { @@ -144,56 +145,36 @@ namespace MediaBrowser.Providers.TV { cancellationToken.ThrowIfCancellationRequested(); - var season = (Season)item; + var images = await _providerManager.GetAvailableRemoteImages(item, cancellationToken, ManualFanartSeriesImageProvider.ProviderName).ConfigureAwait(false); - var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; + const int backdropLimit = 1; - var seasonNumber = season.IndexNumber; + await DownloadImages(item, images.ToList(), backdropLimit, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(seriesId) && seasonNumber.HasValue) - { - // Process images - var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); - - try - { - 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); - return true; - } - - return false; + SetLastRefreshed(item, DateTime.UtcNow); + return true; } - private async Task DownloadImages(BaseItem item, FanartXmlData data, int backdropLimit, CancellationToken cancellationToken) + private async Task DownloadImages(BaseItem item, List images, int backdropLimit, CancellationToken cancellationToken) { if (!item.HasImage(ImageType.Primary)) { - var url = data.LanguagePoster ?? data.Poster; - if (!string.IsNullOrEmpty(url)) - { - url = TVUtils.BannerUrl + url; + var image = images.FirstOrDefault(i => i.Type == ImageType.Primary); - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) + if (image != null) + { + await _providerManager.SaveImage(item, image.Url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) .ConfigureAwait(false); } } if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && !item.HasImage(ImageType.Banner)) { - var url = data.LanguageBanner ?? data.Banner; - if (!string.IsNullOrEmpty(url)) - { - url = TVUtils.BannerUrl + url; + var image = images.FirstOrDefault(i => i.Type == ImageType.Banner); - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) + if (image != null) + { + await _providerManager.SaveImage(item, image.Url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Banner, null, cancellationToken) .ConfigureAwait(false); } } @@ -202,17 +183,16 @@ namespace MediaBrowser.Providers.TV { var bdNo = item.BackdropImagePaths.Count; - foreach (var backdrop in data.Backdrops) + foreach (var backdrop in images.Where(i => i.Type == ImageType.Backdrop)) { - var url = TVUtils.BannerUrl + backdrop.Url; + var url = backdrop.Url; if (item.ContainsImageWithSourceUrl(url)) { continue; } - await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken) - .ConfigureAwait(false); + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Backdrop, bdNo, cancellationToken).ConfigureAwait(false); bdNo++; @@ -220,157 +200,5 @@ namespace MediaBrowser.Providers.TV } } } - - private FanartXmlData FetchFanartXmlData(string bannersXmlPath, int seasonNumber, CancellationToken cancellationToken) - { - var settings = new XmlReaderSettings - { - CheckCharacters = false, - IgnoreProcessingInstructions = true, - IgnoreComments = true, - ValidationType = ValidationType.None - }; - - var data = new FanartXmlData(); - - using (var streamReader = new StreamReader(bannersXmlPath, Encoding.UTF8)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader, settings)) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Banner": - { - using (var subtree = reader.ReadSubtree()) - { - FetchInfoFromBannerNode(data, subtree, seasonNumber); - } - break; - } - default: - reader.Skip(); - break; - } - } - } - } - } - - 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; - string resolution = null; - string language = null; - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "Language": - { - language = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType": - { - bannerType = reader.ReadElementContentAsString() ?? string.Empty; - break; - } - - case "BannerType2": - { - bannerType2 = reader.ReadElementContentAsString() ?? string.Empty; - 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) - { - if (string.Equals(bannerType, "season", StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(bannerType2, "season", StringComparison.OrdinalIgnoreCase)) - { - // Just grab the first - if (string.IsNullOrWhiteSpace(data.Poster)) - { - data.Poster = url; - } - } - else if (string.Equals(bannerType2, "seasonwide", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrWhiteSpace(language) || string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) - { - // Just grab the first - if (string.IsNullOrWhiteSpace(data.Banner)) - { - data.Banner = url; - } - } - else if (string.Equals(language, ConfigurationManager.Configuration.PreferredMetadataLanguage, StringComparison.OrdinalIgnoreCase)) - { - // Just grab the first - if (string.IsNullOrWhiteSpace(data.LanguageBanner)) - { - data.LanguageBanner = url; - } - } - } - } - else if (string.Equals(bannerType, "fanart", StringComparison.OrdinalIgnoreCase)) - { - data.Backdrops.Add(new ImageInfo - { - Url = url, - Resolution = resolution - }); - } - } - } - } } diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs index ace633be7..a439251db 100644 --- a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs +++ b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs @@ -48,10 +48,7 @@ namespace MediaBrowser.Server.Implementations.Drawing /// The _logger /// private readonly ILogger _logger; - /// - /// The _app paths - /// - private readonly IServerApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; private readonly string _imageSizeCachePath; @@ -62,13 +59,12 @@ namespace MediaBrowser.Server.Implementations.Drawing public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem) { _logger = logger; - _appPaths = appPaths; _fileSystem = fileSystem; - _imageSizeCachePath = Path.Combine(_appPaths.ImageCachePath, "image-sizes"); - _croppedWhitespaceImageCachePath = Path.Combine(_appPaths.ImageCachePath, "cropped-images"); - _enhancedImageCachePath = Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); - _resizedImageCachePath = Path.Combine(_appPaths.ImageCachePath, "resized-images"); + _imageSizeCachePath = Path.Combine(appPaths.ImageCachePath, "image-sizes"); + _croppedWhitespaceImageCachePath = Path.Combine(appPaths.ImageCachePath, "cropped-images"); + _enhancedImageCachePath = Path.Combine(appPaths.ImageCachePath, "enhanced-images"); + _resizedImageCachePath = Path.Combine(appPaths.ImageCachePath, "resized-images"); } public void AddParts(IEnumerable enhancers) diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index 9197328bd..88832722b 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -60,25 +60,25 @@ namespace MediaBrowser.Server.Implementations.Library /// Gets the list of entity resolution ignore rules /// /// The entity resolution ignore rules. - private IEnumerable EntityResolutionIgnoreRules { get; set; } + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } /// /// Gets the list of BasePluginFolders added by plugins /// /// The plugin folders. - private IEnumerable PluginFolderCreators { get; set; } + private IVirtualFolderCreator[] PluginFolderCreators { get; set; } /// /// Gets the list of currently registered entity resolvers /// /// The entity resolvers enumerable. - private IEnumerable EntityResolvers { get; set; } + private IItemResolver[] EntityResolvers { get; set; } /// /// Gets or sets the comparers. /// /// The comparers. - private IEnumerable Comparers { get; set; } + private IBaseItemComparer[] Comparers { get; set; } /// /// Gets the active item repository @@ -218,11 +218,11 @@ namespace MediaBrowser.Server.Implementations.Library IEnumerable peoplePrescanTasks, IEnumerable savers) { - EntityResolutionIgnoreRules = rules; - PluginFolderCreators = pluginFolders; + EntityResolutionIgnoreRules = rules.ToArray(); + PluginFolderCreators = pluginFolders.ToArray(); EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); IntroProviders = introProviders; - Comparers = itemComparers; + Comparers = itemComparers.ToArray(); PrescanTasks = prescanTasks; PostscanTasks = postscanTasks; PeoplePrescanTasks = peoplePrescanTasks; diff --git a/MediaBrowser.Tests/MediaBrowser.Tests.csproj b/MediaBrowser.Tests/MediaBrowser.Tests.csproj index 915b90c8e..6ae7544b8 100644 --- a/MediaBrowser.Tests/MediaBrowser.Tests.csproj +++ b/MediaBrowser.Tests/MediaBrowser.Tests.csproj @@ -74,6 +74,9 @@ MediaBrowser.Server.Implementations + + + diff --git a/MediaBrowser.Tests/app.config b/MediaBrowser.Tests/app.config new file mode 100644 index 000000000..cbc4501c5 --- /dev/null +++ b/MediaBrowser.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file