diff --git a/MediaBrowser.Controller/Extensions/XmlExtensions.cs b/MediaBrowser.Controller/Extensions/XmlExtensions.cs index 8698730d4..941d9fca7 100644 --- a/MediaBrowser.Controller/Extensions/XmlExtensions.cs +++ b/MediaBrowser.Controller/Extensions/XmlExtensions.cs @@ -96,11 +96,15 @@ namespace MediaBrowser.Controller.Extensions /// System.String. public static string SafeGetString(this XmlDocument doc, string path, string defaultString) { - XmlNode rvalNode = doc.SelectSingleNode(path); - if (rvalNode != null && rvalNode.InnerText.Trim().Length > 0) + var rvalNode = doc.SelectSingleNode(path); + + if (rvalNode != null) { - return rvalNode.InnerText; + var text = rvalNode.InnerText; + + return !string.IsNullOrWhiteSpace(text) ? text : defaultString; } + return defaultString; } @@ -124,10 +128,12 @@ namespace MediaBrowser.Controller.Extensions /// System.String. public static string SafeGetString(this XmlNode doc, string path, string defaultValue) { - XmlNode rvalNode = doc.SelectSingleNode(path); - if (rvalNode != null && rvalNode.InnerText.Length > 0) + var rvalNode = doc.SelectSingleNode(path); + if (rvalNode != null) { - return rvalNode.InnerText; + var text = rvalNode.InnerText; + + return !string.IsNullOrWhiteSpace(text) ? text : defaultValue; } return defaultValue; } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 89d17758e..0917fa276 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -11,6 +11,9 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Library { + /// + /// Interface ILibraryManager + /// public interface ILibraryManager { /// @@ -140,11 +143,13 @@ namespace MediaBrowser.Controller.Library /// The resolvers. /// The intro providers. /// The item comparers. + /// The prescan tasks. void AddParts(IEnumerable rules, IEnumerable pluginFolders, IEnumerable resolvers, IEnumerable introProviders, - IEnumerable itemComparers); + IEnumerable itemComparers, + IEnumerable prescanTasks); /// /// Sorts the specified items. @@ -160,7 +165,7 @@ namespace MediaBrowser.Controller.Library /// /// Ensure supplied item has only one instance throughout /// - /// + /// The item. /// The proper instance to the item BaseItem GetOrAddByReferenceItem(BaseItem item); @@ -186,7 +191,7 @@ namespace MediaBrowser.Controller.Library /// The cancellation token. /// Task. Task UpdateItem(BaseItem item, CancellationToken cancellationToken); - + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Controller/Library/ILibraryPrescanTask.cs b/MediaBrowser.Controller/Library/ILibraryPrescanTask.cs new file mode 100644 index 000000000..6a48ba777 --- /dev/null +++ b/MediaBrowser.Controller/Library/ILibraryPrescanTask.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Library +{ + /// + /// An interface for tasks that run prior to the media library scan + /// + public interface ILibraryPrescanTask + { + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + Task Run(IProgress progress, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index 8bd1c270d..6a220c6d7 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -1,7 +1,7 @@ -using System.Globalization; -using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Controller.Resolvers; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -243,7 +243,7 @@ namespace MediaBrowser.Controller.Library /// /// The full path. /// System.String. - public static string SeasonNumberFromEpisodeFile(string fullPath) + public static int? GetSeasonNumberFromEpisodeFile(string fullPath) { string fl = fullPath.ToLower(); foreach (var r in EpisodeExpressions) @@ -253,7 +253,19 @@ namespace MediaBrowser.Controller.Library { Group g = m.Groups["seasonnumber"]; if (g != null) - return g.Value; + { + var val = g.Value; + + if (!string.IsNullOrWhiteSpace(val)) + { + int num; + + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out num)) + { + return num; + } + } + } return null; } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 62e92d7f2..017f3dead 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -72,7 +72,9 @@ + + diff --git a/MediaBrowser.Controller/Providers/Movies/MovieDbImagesProvider.cs b/MediaBrowser.Controller/Providers/Movies/MovieDbImagesProvider.cs index bd290fbea..f62ea2483 100644 --- a/MediaBrowser.Controller/Providers/Movies/MovieDbImagesProvider.cs +++ b/MediaBrowser.Controller/Providers/Movies/MovieDbImagesProvider.cs @@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Providers.Movies var status = ProviderRefreshStatus.Success; - var hasLocalPoster = item.LocationType == LocationType.FileSystem ? item.HasLocalImage("folder") : item.HasImage(ImageType.Primary); + var hasLocalPoster = item.HasImage(ImageType.Primary); // poster if (images.posters != null && images.posters.Count > 0 && (ConfigurationManager.Configuration.RefreshItemImages || !hasLocalPoster)) @@ -290,7 +290,7 @@ namespace MediaBrowser.Controller.Providers.Movies { var bdName = "backdrop" + (i == 0 ? "" : i.ToString(CultureInfo.InvariantCulture)); - var hasLocalBackdrop = item.LocationType == LocationType.FileSystem ? item.HasLocalImage(bdName) : item.BackdropImagePaths.Count > i; + var hasLocalBackdrop = item.BackdropImagePaths.Count > i; if (ConfigurationManager.Configuration.RefreshItemImages || !hasLocalBackdrop) { diff --git a/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs index f5dae305f..71249c581 100644 --- a/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs +++ b/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Net; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -22,8 +23,11 @@ namespace MediaBrowser.Controller.Providers.TV /// class RemoteEpisodeProvider : BaseMetadataProvider { + /// + /// The _provider manager + /// private readonly IProviderManager _providerManager; - + /// /// Gets the HTTP client. /// @@ -36,6 +40,7 @@ namespace MediaBrowser.Controller.Providers.TV /// The HTTP client. /// The log manager. /// The configuration manager. + /// The provider manager. public RemoteEpisodeProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) : base(logManager, configurationManager) { @@ -80,6 +85,10 @@ namespace MediaBrowser.Controller.Providers.TV get { return true; } } + /// + /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes + /// + /// true if [refresh on file system stamp change]; otherwise, false. protected override bool RefreshOnFileSystemStampChange { get @@ -88,6 +97,30 @@ namespace MediaBrowser.Controller.Providers.TV } } + /// + /// Gets a value indicating whether [refresh on version change]. + /// + /// true if [refresh on version change]; otherwise, false. + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + /// + /// Gets the provider version. + /// + /// The provider version. + protected override string ProviderVersion + { + get + { + return "1"; + } + } + /// /// Needses the refresh internal. /// @@ -101,34 +134,102 @@ namespace MediaBrowser.Controller.Providers.TV return false; } + if (GetComparisonData(item) != providerInfo.Data) + { + return true; + } + return base.NeedsRefreshInternal(item, providerInfo); } + /// + /// Gets the comparison data. + /// + /// The item. + /// Guid. + private Guid GetComparisonData(BaseItem item) + { + var episode = (Episode)item; + + var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId)) + { + // Process images + var seriesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"); + + var seriesXmlFileInfo = new FileInfo(seriesXmlPath); + + return GetComparisonData(seriesXmlFileInfo); + } + + return Guid.Empty; + } + + /// + /// Gets the comparison data. + /// + /// The series XML file info. + /// Guid. + private Guid GetComparisonData(FileInfo seriesXmlFileInfo) + { + var date = seriesXmlFileInfo.Exists ? seriesXmlFileInfo.LastWriteTimeUtc : DateTime.MinValue; + + var key = date.Ticks + seriesXmlFileInfo.FullName; + + return key.GetMD5(); + } + /// /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// /// The item. /// if set to true [force]. + /// The cancellation token. /// Task{System.Boolean}. public override async Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - - var episode = (Episode)item; - if (!HasLocalMeta(episode)) + if (HasLocalMeta(item)) { - var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; - - if (seriesId != null) - { - var status = await FetchEpisodeData(episode, seriesId, cancellationToken).ConfigureAwait(false); - SetLastRefreshed(item, DateTime.UtcNow, status); - return true; - } - Logger.Info("Episode provider not fetching because series does not have a tvdb id: " + item.Path); return false; } - Logger.Info("Episode provider not fetching because local meta exists or requested to ignore: " + item.Name); + + cancellationToken.ThrowIfCancellationRequested(); + + var episode = (Episode)item; + + var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId)) + { + var seriesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"); + + var seriesXmlFileInfo = new FileInfo(seriesXmlPath); + + var status = ProviderRefreshStatus.Success; + + if (seriesXmlFileInfo.Exists) + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(seriesXmlPath); + + status = await FetchEpisodeData(xmlDoc, episode, seriesId, cancellationToken).ConfigureAwait(false); + } + + BaseProviderInfo data; + if (!item.ProviderData.TryGetValue(Id, out data)) + { + data = new BaseProviderInfo(); + item.ProviderData[Id] = data; + } + + data.Data = GetComparisonData(seriesXmlFileInfo); + + SetLastRefreshed(item, DateTime.UtcNow, status); + return true; + } + + Logger.Info("Episode provider not fetching because series does not have a tvdb id: " + item.Path); return false; } @@ -136,162 +237,121 @@ namespace MediaBrowser.Controller.Providers.TV /// /// Fetches the episode data. /// + /// The series XML. /// The episode. /// The series id. /// The cancellation token. /// Task{System.Boolean}. - private async Task FetchEpisodeData(Episode episode, string seriesId, CancellationToken cancellationToken) + private async Task FetchEpisodeData(XmlDocument seriesXml, Episode episode, string seriesId, CancellationToken cancellationToken) { - string location = episode.Path; - - var episodeNumber = episode.IndexNumber ?? TVUtils.GetEpisodeNumberFromFile(location, episode.Season != null); - var status = ProviderRefreshStatus.Success; - if (episodeNumber == null) + if (episode.IndexNumber == null) { - Logger.Warn("TvDbProvider: Could not determine episode number for: " + episode.Path); return status; } - episode.IndexNumber = episodeNumber; - var usingAbsoluteData = false; + var seasonNumber = episode.ParentIndexNumber ?? TVUtils.GetSeasonNumberFromEpisodeFile(episode.Path); - if (string.IsNullOrEmpty(seriesId)) return status; - - var seasonNumber = ""; - if (episode.Parent is Season) + if (seasonNumber == null) { - seasonNumber = episode.Parent.IndexNumber.ToString(); + return status; } - if (string.IsNullOrEmpty(seasonNumber)) - seasonNumber = TVUtils.SeasonNumberFromEpisodeFile(location); // try and extract the season number from the file name for S1E1, 1x04 etc. + var usingAbsoluteData = false; - if (!string.IsNullOrEmpty(seasonNumber)) + var episodeNode = seriesXml.SelectSingleNode("//Episode[EpisodeNumber='" + episode.IndexNumber.Value + "'][SeasonNumber='" + seasonNumber.Value + "']"); + + if (episodeNode == null) { - seasonNumber = seasonNumber.TrimStart('0'); - - if (string.IsNullOrEmpty(seasonNumber)) + if (seasonNumber.Value == 1) { - seasonNumber = "0"; // Specials + episodeNode = seriesXml.SelectSingleNode("//Episode[absolute_number='" + episode.IndexNumber.Value + "']"); + usingAbsoluteData = true; } + } - var url = string.Format(EpisodeQuery, TVUtils.TvdbApiKey, seriesId, seasonNumber, episodeNumber, ConfigurationManager.Configuration.PreferredMetadataLanguage); - var doc = new XmlDocument(); + // If still null, nothing we can do + if (episodeNode == null) + { + return status; + } - using (var result = await HttpClient.Get(new HttpRequestOptions + var doc = new XmlDocument(); + doc.LoadXml(episodeNode.OuterXml); + + if (!episode.HasImage(ImageType.Primary)) + { + var p = doc.SafeGetString("//filename"); + if (p != null) { - Url = url, - ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true + if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation); - }).ConfigureAwait(false)) - { - doc.Load(result); - } - - //episode does not exist under this season, try absolute numbering. - //still assuming it's numbered as 1x01 - //this is basicly just for anime. - if (!doc.HasChildNodes && Int32.Parse(seasonNumber) == 1) - { - url = string.Format(AbsEpisodeQuery, TVUtils.TvdbApiKey, seriesId, episodeNumber, ConfigurationManager.Configuration.PreferredMetadataLanguage); - - using (var result = await HttpClient.Get(new HttpRequestOptions + try { - Url = url, - ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true - - }).ConfigureAwait(false)) + episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken); + } + catch (HttpException) { - if (result != null) doc.Load(result); - usingAbsoluteData = true; + status = ProviderRefreshStatus.CompletedWithErrors; } } + } - if (doc.HasChildNodes) + episode.Overview = doc.SafeGetString("//Overview"); + if (usingAbsoluteData) + episode.IndexNumber = doc.SafeGetInt32("//absolute_number", -1); + if (episode.IndexNumber < 0) + episode.IndexNumber = doc.SafeGetInt32("//EpisodeNumber"); + + episode.Name = doc.SafeGetString("//EpisodeName"); + episode.CommunityRating = doc.SafeGetSingle("//Rating", -1, 10); + var firstAired = doc.SafeGetString("//FirstAired"); + DateTime airDate; + if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850) + { + episode.PremiereDate = airDate.ToUniversalTime(); + episode.ProductionYear = airDate.Year; + } + + episode.People.Clear(); + + var actors = doc.SafeGetString("//GuestStars"); + if (actors != null) + { + foreach (var person in actors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.GuestStar, Name = str })) { - if (!episode.HasImage(ImageType.Primary)) - { - var p = doc.SafeGetString("//filename"); - if (p != null) - { - if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation); - - try - { - episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken); - } - catch (HttpException) - { - status = ProviderRefreshStatus.CompletedWithErrors; - } - } - } - - episode.Overview = doc.SafeGetString("//Overview"); - if (usingAbsoluteData) - episode.IndexNumber = doc.SafeGetInt32("//absolute_number", -1); - if (episode.IndexNumber < 0) - episode.IndexNumber = doc.SafeGetInt32("//EpisodeNumber"); - - episode.Name = doc.SafeGetString("//EpisodeName"); - episode.CommunityRating = doc.SafeGetSingle("//Rating", -1, 10); - var firstAired = doc.SafeGetString("//FirstAired"); - DateTime airDate; - if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850) - { - episode.PremiereDate = airDate.ToUniversalTime(); - episode.ProductionYear = airDate.Year; - } - - episode.People.Clear(); - - var actors = doc.SafeGetString("//GuestStars"); - if (actors != null) - { - foreach (var person in actors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.GuestStar, Name = str })) - { - episode.AddPerson(person); - } - } - - - var directors = doc.SafeGetString("//Director"); - if (directors != null) - { - foreach (var person in directors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.Director, Name = str })) - { - episode.AddPerson(person); - } - } - - - var writers = doc.SafeGetString("//Writer"); - if (writers != null) - { - foreach (var person in writers.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.Writer, Name = str })) - { - episode.AddPerson(person); - } - } - - if (ConfigurationManager.Configuration.SaveLocalMeta) - { - if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation); - var ms = new MemoryStream(); - doc.Save(ms); - - await _providerManager.SaveToLibraryFilesystem(episode, Path.Combine(episode.MetaLocation, Path.GetFileNameWithoutExtension(episode.Path) + ".xml"), ms, cancellationToken).ConfigureAwait(false); - } - - return status; + episode.AddPerson(person); } + } + + var directors = doc.SafeGetString("//Director"); + if (directors != null) + { + foreach (var person in directors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.Director, Name = str })) + { + episode.AddPerson(person); + } + } + + + var writers = doc.SafeGetString("//Writer"); + if (writers != null) + { + foreach (var person in writers.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.Writer, Name = str })) + { + episode.AddPerson(person); + } + } + + if (ConfigurationManager.Configuration.SaveLocalMeta) + { + if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation); + var ms = new MemoryStream(); + doc.Save(ms); + + await _providerManager.SaveToLibraryFilesystem(episode, Path.Combine(episode.MetaLocation, Path.GetFileNameWithoutExtension(episode.Path) + ".xml"), ms, cancellationToken).ConfigureAwait(false); } return status; diff --git a/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs index 8b7c5e6e4..e9953d135 100644 --- a/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs +++ b/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Net; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -25,8 +26,19 @@ namespace MediaBrowser.Controller.Providers.TV /// The HTTP client. protected IHttpClient HttpClient { get; private set; } + /// + /// The _provider manager + /// private readonly IProviderManager _providerManager; - + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client. + /// The log manager. + /// The configuration manager. + /// The provider manager. + /// httpClient public RemoteSeasonProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) : base(logManager, configurationManager) { @@ -70,6 +82,10 @@ namespace MediaBrowser.Controller.Providers.TV } } + /// + /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes + /// + /// true if [refresh on file system stamp change]; otherwise, false. protected override bool RefreshOnFileSystemStampChange { get @@ -78,6 +94,30 @@ namespace MediaBrowser.Controller.Providers.TV } } + /// + /// Gets a value indicating whether [refresh on version change]. + /// + /// true if [refresh on version change]; otherwise, false. + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + /// + /// Gets the provider version. + /// + /// The provider version. + protected override string ProviderVersion + { + get + { + return "1"; + } + } + /// /// Needses the refresh internal. /// @@ -86,14 +126,51 @@ namespace MediaBrowser.Controller.Providers.TV /// true if XXXX, false otherwise protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) { - if (HasLocalMeta(item)) + if (GetComparisonData(item) != providerInfo.Data) { - return false; + return true; } return base.NeedsRefreshInternal(item, providerInfo); } + /// + /// Gets the comparison data. + /// + /// The item. + /// Guid. + private Guid GetComparisonData(BaseItem item) + { + var season = (Season)item; + var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId)) + { + // Process images + var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); + + var imagesFileInfo = new FileInfo(imagesXmlPath); + + return GetComparisonData(imagesFileInfo); + } + + return Guid.Empty; + } + + /// + /// Gets the comparison data. + /// + /// The images file info. + /// Guid. + private Guid GetComparisonData(FileInfo imagesFileInfo) + { + var date = imagesFileInfo.Exists ? imagesFileInfo.LastWriteTimeUtc : DateTime.MinValue; + + var key = date.Ticks + imagesFileInfo.FullName; + + return key.GetMD5(); + } + /// /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// @@ -107,162 +184,106 @@ namespace MediaBrowser.Controller.Providers.TV var season = (Season)item; - if (!HasLocalMeta(item)) - { - var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; + var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; - if (seriesId != null) + if (!string.IsNullOrEmpty(seriesId)) + { + // Process images + var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); + + var imagesFileInfo = new FileInfo(imagesXmlPath); + + if (imagesFileInfo.Exists) { - var status = await FetchSeasonData(season, seriesId, cancellationToken).ConfigureAwait(false); + if (!season.HasImage(ImageType.Primary) || !season.HasImage(ImageType.Banner) || season.BackdropImagePaths.Count == 0) + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(imagesXmlPath); - SetLastRefreshed(item, DateTime.UtcNow, status); - - return true; + await FetchImages(season, xmlDoc, cancellationToken).ConfigureAwait(false); + } } - Logger.Info("Season provider not fetching because series does not have a tvdb id: " + season.Path); - } - else - { - Logger.Info("Season provider not fetching because local meta exists: " + season.Name); + + BaseProviderInfo data; + if (!item.ProviderData.TryGetValue(Id, out data)) + { + data = new BaseProviderInfo(); + item.ProviderData[Id] = data; + } + + data.Data = GetComparisonData(imagesFileInfo); + + SetLastRefreshed(item, DateTime.UtcNow); + return true; } + return false; } - /// - /// Fetches the season data. + /// Fetches the images. /// /// The season. - /// The series id. + /// The images. /// The cancellation token. - /// Task{System.Boolean}. - private async Task FetchSeasonData(Season season, string seriesId, CancellationToken cancellationToken) + /// Task. + private async Task FetchImages(Season season, XmlDocument images, CancellationToken cancellationToken) { - var seasonNumber = TVUtils.GetSeasonNumberFromPath(season.Path) ?? -1; + var seasonNumber = season.IndexNumber ?? -1; - season.IndexNumber = seasonNumber; - - if (seasonNumber == 0) + if (seasonNumber == -1) { - season.Name = "Specials"; + return; } - var status = ProviderRefreshStatus.Success; - - if (string.IsNullOrEmpty(seriesId)) + if (ConfigurationManager.Configuration.RefreshItemImages || !season.HasImage(ImageType.Primary)) { - return status; - } - - if ((season.PrimaryImagePath == null) || (!season.HasImage(ImageType.Banner)) || (season.BackdropImagePaths == null)) - { - var images = new XmlDocument(); - var url = string.Format("http://www.thetvdb.com/api/" + TVUtils.TvdbApiKey + "/series/{0}/banners.xml", seriesId); - - using (var imgs = await HttpClient.Get(new HttpRequestOptions + 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) { - Url = url, - ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true + n = n.SelectSingleNode("./BannerPath"); - }).ConfigureAwait(false)) - { - images.Load(imgs); + if (n != null) + season.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false); } + } - if (images.HasChildNodes) + if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !season.HasImage(ImageType.Banner))) + { + 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) { - if (ConfigurationManager.Configuration.RefreshItemImages || !season.HasLocalImage("folder")) + n = n.SelectSingleNode("./BannerPath"); + if (n != null) { - 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) - { - n = n.SelectSingleNode("./BannerPath"); + var bannerImagePath = + await _providerManager.DownloadAndSaveImage(season, + TVUtils.BannerUrl + n.InnerText, + "banner" + + Path.GetExtension(n.InnerText), + ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken). + ConfigureAwait(false); - if (n != null) - season.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false); - } - } - - if (ConfigurationManager.Configuration.DownloadSeasonImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !season.HasLocalImage("banner"))) - { - 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) - { - n = n.SelectSingleNode("./BannerPath"); - if (n != null) - { - var bannerImagePath = - await _providerManager.DownloadAndSaveImage(season, - TVUtils.BannerUrl + n.InnerText, - "banner" + - Path.GetExtension(n.InnerText), - ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken). - ConfigureAwait(false); - - season.SetImage(ImageType.Banner, bannerImagePath); - } - } - } - - if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && (ConfigurationManager.Configuration.RefreshItemImages || !season.HasLocalImage("backdrop"))) - { - var n = images.SelectSingleNode("//Banner[BannerType='fanart'][Season='" + seasonNumber + "']"); - if (n != null) - { - n = n.SelectSingleNode("./BannerPath"); - if (n != null) - { - if (season.BackdropImagePaths == null) season.BackdropImagePaths = new List(); - season.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "backdrop" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false)); - } - } - else if (!ConfigurationManager.Configuration.SaveLocalMeta) //if saving local - season will inherit from series - { - // not necessarily accurate but will give a different bit of art to each season - var lst = images.SelectNodes("//Banner[BannerType='fanart']"); - if (lst != null && lst.Count > 0) - { - var num = seasonNumber % lst.Count; - n = lst[num]; - n = n.SelectSingleNode("./BannerPath"); - if (n != null) - { - if (season.BackdropImagePaths == null) - season.BackdropImagePaths = new List(); - - season.BackdropImagePaths.Add( - await _providerManager.DownloadAndSaveImage(season, - TVUtils.BannerUrl + - n.InnerText, - "backdrop" + - Path.GetExtension( - n.InnerText), - ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken) - .ConfigureAwait(false)); - } - } - } + season.SetImage(ImageType.Banner, bannerImagePath); } } } - return status; - } - /// - /// Determines whether [has local meta] [the specified item]. - /// - /// The item. - /// true if [has local meta] [the specified item]; otherwise, false. - private bool HasLocalMeta(BaseItem item) - { - //just folder.jpg/png - return (item.ResolveArgs.ContainsMetaFileByName("folder.jpg") || - item.ResolveArgs.ContainsMetaFileByName("folder.png")); + if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && (ConfigurationManager.Configuration.RefreshItemImages || season.BackdropImagePaths.Count == 0)) + { + var n = images.SelectSingleNode("//Banner[BannerType='fanart'][Season='" + seasonNumber + "']"); + if (n != null) + { + n = n.SelectSingleNode("./BannerPath"); + if (n != null) + { + if (season.BackdropImagePaths == null) season.BackdropImagePaths = new List(); + season.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "backdrop" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken).ConfigureAwait(false)); + } + } + } } - } } diff --git a/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs index a30cf69da..82ff0b98e 100644 --- a/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs +++ b/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -6,12 +7,13 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; 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.Net; using System.Text; using System.Threading; @@ -25,15 +27,27 @@ namespace MediaBrowser.Controller.Providers.TV /// class RemoteSeriesProvider : BaseMetadataProvider, IDisposable { + /// + /// The _provider manager + /// private readonly IProviderManager _providerManager; - + /// /// The tv db /// - internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(3, 3); + internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(1, 1); + /// + /// Gets the current. + /// + /// The current. internal static RemoteSeriesProvider Current { get; private set; } + /// + /// The _zip client + /// + private readonly IZipClient _zipClient; + /// /// Gets the HTTP client. /// @@ -47,8 +61,9 @@ namespace MediaBrowser.Controller.Providers.TV /// The log manager. /// The configuration manager. /// The provider manager. + /// The zip client. /// httpClient - public RemoteSeriesProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) + public RemoteSeriesProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IZipClient zipClient) : base(logManager, configurationManager) { if (httpClient == null) @@ -57,6 +72,7 @@ namespace MediaBrowser.Controller.Providers.TV } HttpClient = httpClient; _providerManager = providerManager; + _zipClient = zipClient; Current = this; } @@ -81,13 +97,9 @@ namespace MediaBrowser.Controller.Providers.TV /// private const string SeriesQuery = "GetSeries.php?seriesname={0}"; /// - /// The series get + /// The series get zip /// - private const string SeriesGet = "http://www.thetvdb.com/api/{0}/series/{1}/{2}.xml"; - /// - /// The get actors - /// - private const string GetActors = "http://www.thetvdb.com/api/{0}/series/{1}/actors.xml"; + private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip"; /// /// The LOCA l_ MET a_ FIL e_ NAME @@ -125,6 +137,30 @@ namespace MediaBrowser.Controller.Providers.TV } } + /// + /// Gets a value indicating whether [refresh on version change]. + /// + /// true if [refresh on version change]; otherwise, false. + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + /// + /// Gets the provider version. + /// + /// The provider version. + protected override string ProviderVersion + { + get + { + return "1"; + } + } + /// /// Needses the refresh internal. /// @@ -133,9 +169,43 @@ namespace MediaBrowser.Controller.Providers.TV /// true if XXXX, false otherwise protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) { - return !HasLocalMeta(item) && base.NeedsRefreshInternal(item, providerInfo); + // Refresh even if local metadata exists because we need episode infos + if (GetComparisonData(item) != providerInfo.Data) + { + return true; + } + + return base.NeedsRefreshInternal(item, providerInfo); } + /// + /// Gets the comparison data. + /// + /// The item. + /// Guid. + private Guid GetComparisonData(BaseItem item) + { + var series = (Series)item; + var seriesId = series.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(seriesId)) + { + // Process images + var path = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); + + var files = new DirectoryInfo(path) + .EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly) + .Select(i => i.FullName + i.LastWriteTimeUtc.Ticks) + .ToArray(); + + if (files.Length > 0) + { + return string.Join(string.Empty, files).GetMD5(); + } + } + + return Guid.Empty; + } /// /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// @@ -146,30 +216,40 @@ namespace MediaBrowser.Controller.Providers.TV public override async Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - + var series = (Series)item; - if (!HasLocalMeta(series)) + + var seriesId = series.GetProviderId(MetadataProviders.Tvdb); + + if (string.IsNullOrEmpty(seriesId)) { - var path = item.Path ?? ""; - var seriesId = Path.GetFileName(path).GetAttributeValue("tvdbid") ?? await GetSeriesId(series, cancellationToken); - - cancellationToken.ThrowIfCancellationRequested(); - - var status = ProviderRefreshStatus.Success; - - if (!string.IsNullOrEmpty(seriesId)) - { - series.SetProviderId(MetadataProviders.Tvdb, seriesId); - - status = await FetchSeriesData(series, seriesId, cancellationToken).ConfigureAwait(false); - } - - SetLastRefreshed(item, DateTime.UtcNow, status); - return true; + seriesId = await GetSeriesId(series, cancellationToken); } - Logger.Info("Series provider not fetching because local meta exists or requested to ignore: " + item.Name); - return false; + cancellationToken.ThrowIfCancellationRequested(); + + var status = ProviderRefreshStatus.Success; + + if (!string.IsNullOrEmpty(seriesId)) + { + series.SetProviderId(MetadataProviders.Tvdb, seriesId); + + var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); + + status = await FetchSeriesData(series, seriesId, seriesDataPath, cancellationToken).ConfigureAwait(false); + } + + BaseProviderInfo data; + if (!item.ProviderData.TryGetValue(Id, out data)) + { + data = new BaseProviderInfo(); + item.ProviderData[Id] = data; + } + + data.Data = GetComparisonData(item); + + SetLastRefreshed(item, DateTime.UtcNow, status); + return true; } /// @@ -177,263 +257,291 @@ namespace MediaBrowser.Controller.Providers.TV /// /// The series. /// The series id. + /// The series data path. /// The cancellation token. /// Task{System.Boolean}. - private async Task FetchSeriesData(Series series, string seriesId, CancellationToken cancellationToken) + private async Task FetchSeriesData(Series series, string seriesId, string seriesDataPath, CancellationToken cancellationToken) { var status = ProviderRefreshStatus.Success; - if (!string.IsNullOrEmpty(seriesId)) + var files = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).ToArray(); + + var seriesXmlFilename = ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"; + + // Only download if not already there + // The prescan task will take care of updates so we don't need to re-download here + if (!files.Contains("banners.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains("actors.xml", StringComparer.OrdinalIgnoreCase) || !files.Contains(seriesXmlFilename, StringComparer.OrdinalIgnoreCase)) { + await DownloadSeriesZip(seriesId, seriesDataPath, cancellationToken).ConfigureAwait(false); + } - string url = string.Format(SeriesGet, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage); - var doc = new XmlDocument(); + // Only examine the main info if there's no local metadata + if (!HasLocalMeta(series)) + { + var seriesXmlPath = Path.Combine(seriesDataPath, seriesXmlFilename); + var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml"); - using (var xml = await HttpClient.Get(new HttpRequestOptions + var seriesDoc = new XmlDocument(); + seriesDoc.Load(seriesXmlPath); + + FetchMainInfo(series, seriesDoc); + + var actorsDoc = new XmlDocument(); + actorsDoc.Load(actorsXmlPath); + + FetchActors(series, actorsDoc, seriesDoc); + + if (ConfigurationManager.Configuration.SaveLocalMeta) { - Url = url, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true + var ms = new MemoryStream(); + seriesDoc.Save(ms); - }).ConfigureAwait(false)) - { - doc.Load(xml); + await _providerManager.SaveToLibraryFilesystem(series, Path.Combine(series.MetaLocation, LocalMetaFileName), ms, cancellationToken).ConfigureAwait(false); } + } - if (doc.HasChildNodes) - { - //kick off the actor and image fetch simultaneously - var actorTask = FetchActors(series, seriesId, doc, cancellationToken); - var imageTask = FetchImages(series, seriesId, cancellationToken); + // Process images + var imagesXmlPath = Path.Combine(seriesDataPath, "banners.xml"); - series.Name = doc.SafeGetString("//SeriesName"); - series.Overview = doc.SafeGetString("//Overview"); - series.CommunityRating = doc.SafeGetSingle("//Rating", 0, 10); - series.AirDays = TVUtils.GetAirDays(doc.SafeGetString("//Airs_DayOfWeek")); - series.AirTime = doc.SafeGetString("//Airs_Time"); + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(imagesXmlPath); - string n = doc.SafeGetString("//banner"); - if (!string.IsNullOrWhiteSpace(n) && !series.HasImage(ImageType.Banner)) - { - series.SetImage(ImageType.Banner, await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n, "banner" + Path.GetExtension(n), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false)); - } - - string s = doc.SafeGetString("//Network"); - - if (!string.IsNullOrWhiteSpace(s)) - { - series.Studios.Clear(); - - foreach (var studio in s.Trim().Split('|')) - { - series.AddStudio(studio); - } - } - - series.OfficialRating = doc.SafeGetString("//ContentRating"); - - string g = doc.SafeGetString("//Genre"); - - if (g != null) - { - string[] genres = g.Trim('|').Split('|'); - if (g.Length > 0) - { - series.Genres.Clear(); - - foreach (var genre in genres) - { - series.AddGenre(genre); - } - } - } - - try - { - //wait for other tasks - await Task.WhenAll(actorTask, imageTask).ConfigureAwait(false); - } - catch (HttpException) - { - status = ProviderRefreshStatus.CompletedWithErrors; - } - - if (ConfigurationManager.Configuration.SaveLocalMeta) - { - var ms = new MemoryStream(); - doc.Save(ms); - - await _providerManager.SaveToLibraryFilesystem(series, Path.Combine(series.MetaLocation, LocalMetaFileName), ms, cancellationToken).ConfigureAwait(false); - } - } + await FetchImages(series, xmlDoc, cancellationToken).ConfigureAwait(false); + } + catch (HttpException) + { + // Have the provider try again next time, but don't let it fail here + status = ProviderRefreshStatus.CompletedWithErrors; } return status; } /// - /// Fetches the actors. + /// Downloads the series zip. /// - /// The series. /// The series id. - /// The doc. + /// The series data path. /// The cancellation token. /// Task. - private async Task FetchActors(Series series, string seriesId, XmlDocument doc, CancellationToken cancellationToken) + internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, CancellationToken cancellationToken) { - string urlActors = string.Format(GetActors, TVUtils.TvdbApiKey, seriesId); - var docActors = new XmlDocument(); - - using (var actors = await HttpClient.Get(new HttpRequestOptions + var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage); + + using (var zipStream = await HttpClient.Get(new HttpRequestOptions { - Url = urlActors, + Url = url, ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true + CancellationToken = cancellationToken }).ConfigureAwait(false)) { - docActors.Load(actors); + // Copy to memory stream because we need a seekable stream + using (var ms = new MemoryStream()) + { + await zipStream.CopyToAsync(ms).ConfigureAwait(false); + + ms.Position = 0; + _zipClient.ExtractAll(ms, seriesDataPath, true); + } + } + } + + /// + /// Gets the series data path. + /// + /// The app paths. + /// The series id. + /// System.String. + internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId) + { + var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); + + if (!Directory.Exists(seriesDataPath)) + { + Directory.CreateDirectory(seriesDataPath); } - if (docActors.HasChildNodes) + return seriesDataPath; + } + + /// + /// Gets the series data path. + /// + /// The app paths. + /// System.String. + internal static string GetSeriesDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.DataPath, "tvdb"); + + if (!Directory.Exists(dataPath)) { - XmlNode actorsNode = null; - if (ConfigurationManager.Configuration.SaveLocalMeta) + Directory.CreateDirectory(dataPath); + } + + return dataPath; + } + + /// + /// Fetches the main info. + /// + /// The series. + /// The doc. + private void FetchMainInfo(Series series, XmlDocument doc) + { + series.Name = doc.SafeGetString("//SeriesName"); + series.Overview = doc.SafeGetString("//Overview"); + series.CommunityRating = doc.SafeGetSingle("//Rating", 0, 10); + series.AirDays = TVUtils.GetAirDays(doc.SafeGetString("//Airs_DayOfWeek")); + series.AirTime = doc.SafeGetString("//Airs_Time"); + + string s = doc.SafeGetString("//Network"); + + if (!string.IsNullOrWhiteSpace(s)) + { + series.Studios.Clear(); + + foreach (var studio in s.Trim().Split('|')) { - //add to the main doc for saving - var seriesNode = doc.SelectSingleNode("//Series"); - if (seriesNode != null) - { - actorsNode = doc.CreateNode(XmlNodeType.Element, "Persons", null); - seriesNode.AppendChild(actorsNode); - } + series.AddStudio(studio); } + } - var xmlNodeList = docActors.SelectNodes("Actors/Actor"); + series.OfficialRating = doc.SafeGetString("//ContentRating"); - if (xmlNodeList != null) + string g = doc.SafeGetString("//Genre"); + + if (g != null) + { + string[] genres = g.Trim('|').Split('|'); + if (g.Length > 0) { - series.People.Clear(); + series.Genres.Clear(); - foreach (XmlNode p in xmlNodeList) + foreach (var genre in genres) { - string actorName = p.SafeGetString("Name"); - string actorRole = p.SafeGetString("Role"); - if (!string.IsNullOrWhiteSpace(actorName)) - { - series.AddPerson(new PersonInfo { Type = PersonType.Actor, Name = actorName, Role = actorRole }); - - if (ConfigurationManager.Configuration.SaveLocalMeta && actorsNode != null) - { - //create in main doc - var personNode = doc.CreateNode(XmlNodeType.Element, "Person", null); - foreach (XmlNode subNode in p.ChildNodes) - personNode.AppendChild(doc.ImportNode(subNode, true)); - //need to add the type - var typeNode = doc.CreateNode(XmlNodeType.Element, "Type", null); - typeNode.InnerText = PersonType.Actor; - personNode.AppendChild(typeNode); - actorsNode.AppendChild(personNode); - } - - } + series.AddGenre(genre); } } } } + /// + /// Fetches the actors. + /// + /// The series. + /// The actors doc. + /// The seriesDoc. + /// Task. + private void FetchActors(Series series, XmlDocument actorsDoc, XmlDocument seriesDoc) + { + XmlNode actorsNode = null; + if (ConfigurationManager.Configuration.SaveLocalMeta) + { + //add to the main seriesDoc for saving + var seriesNode = seriesDoc.SelectSingleNode("//Series"); + if (seriesNode != null) + { + actorsNode = seriesDoc.CreateNode(XmlNodeType.Element, "Persons", null); + seriesNode.AppendChild(actorsNode); + } + } + + var xmlNodeList = actorsDoc.SelectNodes("Actors/Actor"); + + if (xmlNodeList != null) + { + series.People.Clear(); + + foreach (XmlNode p in xmlNodeList) + { + string actorName = p.SafeGetString("Name"); + string actorRole = p.SafeGetString("Role"); + if (!string.IsNullOrWhiteSpace(actorName)) + { + series.AddPerson(new PersonInfo { Type = PersonType.Actor, Name = actorName, Role = actorRole }); + + if (ConfigurationManager.Configuration.SaveLocalMeta && actorsNode != null) + { + //create in main seriesDoc + var personNode = seriesDoc.CreateNode(XmlNodeType.Element, "Person", null); + foreach (XmlNode subNode in p.ChildNodes) + personNode.AppendChild(seriesDoc.ImportNode(subNode, true)); + //need to add the type + var typeNode = seriesDoc.CreateNode(XmlNodeType.Element, "Type", null); + typeNode.InnerText = PersonType.Actor; + personNode.AppendChild(typeNode); + actorsNode.AppendChild(personNode); + } + + } + } + } + } + + /// + /// The us culture + /// protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - + /// /// Fetches the images. /// /// The series. - /// The series id. + /// The images. /// The cancellation token. /// Task. - private async Task FetchImages(Series series, string seriesId, CancellationToken cancellationToken) + private async Task FetchImages(Series series, XmlDocument images, CancellationToken cancellationToken) { - if ((!string.IsNullOrEmpty(seriesId)) && ((series.PrimaryImagePath == null) || (series.BackdropImagePaths == null))) + if (ConfigurationManager.Configuration.RefreshItemImages || !series.HasImage(ImageType.Primary)) { - string url = string.Format("http://www.thetvdb.com/api/" + TVUtils.TvdbApiKey + "/series/{0}/banners.xml", seriesId); - var images = new XmlDocument(); - - try + var n = images.SelectSingleNode("//Banner[BannerType='poster']"); + if (n != null) { - using (var imgs = await HttpClient.Get(new HttpRequestOptions + n = n.SelectSingleNode("./BannerPath"); + if (n != null) { - Url = url, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true - - }).ConfigureAwait(false)) - { - images.Load(imgs); + series.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false); } } - catch (HttpException ex) - { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - // If a series has no images this will produce a 404. - // Return gracefully so we don't keep retrying on subsequent scans - return; - } + } - throw; + if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !series.HasImage(ImageType.Banner))) + { + var n = images.SelectSingleNode("//Banner[BannerType='series']"); + if (n != null) + { + n = n.SelectSingleNode("./BannerPath"); + if (n != null) + { + var bannerImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken); + + series.SetImage(ImageType.Banner, bannerImagePath); + } } + } - if (images.HasChildNodes) + if (series.BackdropImagePaths.Count < ConfigurationManager.Configuration.MaxBackdrops) + { + var bdNo = 0; + var xmlNodeList = images.SelectNodes("//Banner[BannerType='fanart']"); + if (xmlNodeList != null) { - if (ConfigurationManager.Configuration.RefreshItemImages || !series.HasLocalImage("folder")) + foreach (XmlNode b in xmlNodeList) { - var n = images.SelectSingleNode("//Banner[BannerType='poster']"); - if (n != null) + var p = b.SelectSingleNode("./BannerPath"); + + if (p != null) { - n = n.SelectSingleNode("./BannerPath"); - if (n != null) - { - series.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false); - } + var bdName = "backdrop" + (bdNo > 0 ? bdNo.ToString(UsCulture) : ""); + series.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + p.InnerText, bdName + Path.GetExtension(p.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false)); + bdNo++; } + + if (series.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops) break; } - - if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !series.HasLocalImage("banner"))) - { - var n = images.SelectSingleNode("//Banner[BannerType='series']"); - if (n != null) - { - n = n.SelectSingleNode("./BannerPath"); - if (n != null) - { - var bannerImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken); - - series.SetImage(ImageType.Banner, bannerImagePath); - } - } - } - - var bdNo = 0; - var xmlNodeList = images.SelectNodes("//Banner[BannerType='fanart']"); - if (xmlNodeList != null) - foreach (XmlNode b in xmlNodeList) - { - series.BackdropImagePaths = new List(); - var p = b.SelectSingleNode("./BannerPath"); - if (p != null) - { - var bdName = "backdrop" + (bdNo > 0 ? bdNo.ToString(UsCulture) : ""); - if (ConfigurationManager.Configuration.RefreshItemImages || !series.HasLocalImage(bdName)) - { - series.BackdropImagePaths.Add(await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + p.InnerText, bdName + Path.GetExtension(p.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken).ConfigureAwait(false)); - } - bdNo++; - if (bdNo >= ConfigurationManager.Configuration.MaxBackdrops) break; - } - } } } } @@ -573,6 +681,9 @@ namespace MediaBrowser.Controller.Providers.TV return name.Trim(); } + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// public void Dispose() { Dispose(true); diff --git a/MediaBrowser.Controller/Providers/TV/TvdbPrescanTask.cs b/MediaBrowser.Controller/Providers/TV/TvdbPrescanTask.cs new file mode 100644 index 000000000..3a2f47a5e --- /dev/null +++ b/MediaBrowser.Controller/Providers/TV/TvdbPrescanTask.cs @@ -0,0 +1,204 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; + +namespace MediaBrowser.Controller.Providers.TV +{ + /// + /// Class TvdbPrescanTask + /// + public class TvdbPrescanTask : ILibraryPrescanTask + { + /// + /// 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 IConfigurationManager _config; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The HTTP client. + /// The config. + public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IConfigurationManager config) + { + _logger = logger; + _httpClient = httpClient; + _config = config; + } + + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var path = RemoteSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); + + var timestampFile = Path.Combine(path, "time.txt"); + + var timestampFileInfo = new FileInfo(timestampFile); + + // Don't check for tvdb updates anymore frequently than 24 hours + if (timestampFileInfo.Exists && (DateTime.UtcNow - timestampFileInfo.LastWriteTimeUtc).TotalDays < 1) + { + return; + } + + // Find out the last time we queried tvdb for updates + var lastUpdateTime = timestampFileInfo.Exists ? File.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; + + string newUpdateTime; + + var existingDirectories = Directory.EnumerateDirectories(path).Select(Path.GetFileName).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 = RemoteSeriesProvider.Current.TvDbResourcePool + + }).ConfigureAwait(false)) + { + var doc = new XmlDocument(); + + doc.Load(stream); + + newUpdateTime = doc.SafeGetString("//Time"); + } + + await UpdateSeries(existingDirectories, path, cancellationToken).ConfigureAwait(false); + } + else + { + var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false); + + newUpdateTime = seriesToUpdate.Item2; + + await UpdateSeries(seriesToUpdate.Item1, path, cancellationToken).ConfigureAwait(false); + } + + File.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8); + } + + /// + /// 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 = RemoteSeriesProvider.Current.TvDbResourcePool + + }).ConfigureAwait(false)) + { + var doc = new XmlDocument(); + + doc.Load(stream); + + var newUpdateTime = doc.SafeGetString("//Time"); + + var seriesNodes = doc.SelectNodes("//Series"); + + var seriesList = seriesNodes == null ? new string[] { } : + seriesNodes.Cast() + .Select(i => i.InnerText) + .Where(i => !string.IsNullOrWhiteSpace(i) && existingSeriesIds.Contains(i, StringComparer.OrdinalIgnoreCase)); + + return new Tuple, string>(seriesList, newUpdateTime); + } + } + + /// + /// Updates the series. + /// + /// The series ids. + /// The series data path. + /// The cancellation token. + /// Task. + private async Task UpdateSeries(IEnumerable seriesIds, string seriesDataPath, CancellationToken cancellationToken) + { + foreach (var seriesId in seriesIds) + { + try + { + await UpdateSeries(seriesId, seriesDataPath, cancellationToken).ConfigureAwait(false); + } + catch (HttpException ex) + { + // Already logged at lower levels, but don't fail the whole operation, unless timed out + + if (ex.IsTimedOut) + { + throw; + } + } + } + } + + /// + /// Updates the series. + /// + /// The id. + /// The series data path. + /// The cancellation token. + /// Task. + private Task UpdateSeries(string id, string seriesDataPath, CancellationToken cancellationToken) + { + _logger.Info("Updating series " + id); + + seriesDataPath = Path.Combine(seriesDataPath, id); + + if (!Directory.Exists(seriesDataPath)) + { + Directory.CreateDirectory(seriesDataPath); + } + + return RemoteSeriesProvider.Current.DownloadSeriesZip(id, seriesDataPath, cancellationToken); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index b692e97f3..2068ac0da 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -31,6 +31,8 @@ namespace MediaBrowser.Server.Implementations.Library /// public class LibraryManager : ILibraryManager { + private IEnumerable PrescanTasks { get; set; } + /// /// Gets the intro providers. /// @@ -161,13 +163,15 @@ namespace MediaBrowser.Server.Implementations.Library IEnumerable pluginFolders, IEnumerable resolvers, IEnumerable introProviders, - IEnumerable itemComparers) + IEnumerable itemComparers, + IEnumerable prescanTasks) { EntityResolutionIgnoreRules = rules; PluginFolderCreators = pluginFolders; EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); IntroProviders = introProviders; Comparers = itemComparers; + PrescanTasks = prescanTasks; } /// @@ -841,6 +845,19 @@ namespace MediaBrowser.Server.Implementations.Library await ValidateCollectionFolders(folder, cancellationToken).ConfigureAwait(false); } + // Run prescan tasks + foreach (var task in PrescanTasks) + { + try + { + await task.Run(new Progress(), cancellationToken); + } + catch (Exception ex) + { + _logger.ErrorException("Error running prescan task", ex); + } + } + var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(pct * .8)); diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 84f7a8522..d6fe5d456 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -18,41 +18,54 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV /// Episode. protected override Episode Resolve(ItemResolveArgs args) { - var isInSeason = args.Parent is Season; + var season = args.Parent as Season; // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something - if (isInSeason || args.Parent is Series) + if (season != null || args.Parent is Series) { + Episode episode = null; + if (args.IsDirectory) { if (args.ContainsFileSystemEntryByName("video_ts")) { - return new Episode + episode = new Episode { - IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, isInSeason), Path = args.Path, VideoType = VideoType.Dvd }; } if (args.ContainsFileSystemEntryByName("bdmv")) { - return new Episode + episode = new Episode { - IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, isInSeason), Path = args.Path, VideoType = VideoType.BluRay }; } } - var episide = base.Resolve(args); - - if (episide != null) + if (episode == null) { - episide.IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, isInSeason); + episode = base.Resolve(args); } - return episide; + if (episode != null) + { + episode.IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, season != null); + + if (season != null) + { + episode.ParentIndexNumber = season.IndexNumber; + } + + if (episode.ParentIndexNumber == null) + { + episode.ParentIndexNumber = TVUtils.GetSeasonNumberFromEpisodeFile(args.Path); + } + } + + return episode; } return null; diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 35a9698c3..f1350b8a5 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -367,7 +367,12 @@ namespace MediaBrowser.ServerApplication Parallel.Invoke( - () => LibraryManager.AddParts(GetExports(), GetExports(), GetExports(), GetExports(), GetExports()), + () => LibraryManager.AddParts(GetExports(), + GetExports(), + GetExports(), + GetExports(), + GetExports(), + GetExports()), () => ProviderManager.AddMetadataProviders(GetExports().ToArray())