diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2013-05-20 23:16:43 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2013-05-20 23:16:43 -0400 |
| commit | f3a7307ebb9a1a484a82563c4cfab6bf461c7631 (patch) | |
| tree | 9cfc2177081b9713215e4453b45e4213dc67c200 | |
| parent | 96e8f053b56a385dc0f8c8e2c81fd0ac23794692 (diff) | |
reduce requests against tvdb by getting entire series metadata at once
13 files changed, 969 insertions, 493 deletions
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 /// <returns>System.String.</returns> 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 /// <returns>System.String.</returns> 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 { + /// <summary> + /// Interface ILibraryManager + /// </summary> public interface ILibraryManager { /// <summary> @@ -140,11 +143,13 @@ namespace MediaBrowser.Controller.Library /// <param name="resolvers">The resolvers.</param> /// <param name="introProviders">The intro providers.</param> /// <param name="itemComparers">The item comparers.</param> + /// <param name="prescanTasks">The prescan tasks.</param> void AddParts(IEnumerable<IResolverIgnoreRule> rules, IEnumerable<IVirtualFolderCreator> pluginFolders, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, - IEnumerable<IBaseItemComparer> itemComparers); + IEnumerable<IBaseItemComparer> itemComparers, + IEnumerable<ILibraryPrescanTask> prescanTasks); /// <summary> /// Sorts the specified items. @@ -160,7 +165,7 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Ensure supplied item has only one instance throughout /// </summary> - /// <param name="item"></param> + /// <param name="item">The item.</param> /// <returns>The proper instance to the item</returns> BaseItem GetOrAddByReferenceItem(BaseItem item); @@ -186,7 +191,7 @@ namespace MediaBrowser.Controller.Library /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> Task UpdateItem(BaseItem item, CancellationToken cancellationToken); - + /// <summary> /// Retrieves the item. /// </summary> 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 +{ + /// <summary> + /// An interface for tasks that run prior to the media library scan + /// </summary> + public interface ILibraryPrescanTask + { + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task Run(IProgress<double> 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 /// </summary> /// <param name="fullPath">The full path.</param> /// <returns>System.String.</returns> - 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 @@ <Compile Include="Configuration\IServerConfigurationManager.cs" /> <Compile Include="Dto\SessionInfoDtoBuilder.cs" /> <Compile Include="Entities\Audio\MusicAlbumDisc.cs" /> + <Compile Include="Library\ILibraryPrescanTask.cs" /> <Compile Include="Providers\Movies\MovieDbImagesProvider.cs" /> + <Compile Include="Providers\TV\TvdbPrescanTask.cs" /> <Compile Include="Session\ISessionManager.cs" /> <Compile Include="Drawing\ImageExtensions.cs" /> <Compile Include="Drawing\ImageHeader.cs" /> 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 /// </summary> class RemoteEpisodeProvider : BaseMetadataProvider { + /// <summary> + /// The _provider manager + /// </summary> private readonly IProviderManager _providerManager; - + /// <summary> /// Gets the HTTP client. /// </summary> @@ -36,6 +40,7 @@ namespace MediaBrowser.Controller.Providers.TV /// <param name="httpClient">The HTTP client.</param> /// <param name="logManager">The log manager.</param> /// <param name="configurationManager">The configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> 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; } } + /// <summary> + /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes + /// </summary> + /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value> protected override bool RefreshOnFileSystemStampChange { get @@ -89,6 +98,30 @@ namespace MediaBrowser.Controller.Providers.TV } /// <summary> + /// Gets a value indicating whether [refresh on version change]. + /// </summary> + /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value> + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + /// <summary> + /// Gets the provider version. + /// </summary> + /// <value>The provider version.</value> + protected override string ProviderVersion + { + get + { + return "1"; + } + } + + /// <summary> /// Needses the refresh internal. /// </summary> /// <param name="item">The item.</param> @@ -101,34 +134,102 @@ namespace MediaBrowser.Controller.Providers.TV return false; } + if (GetComparisonData(item) != providerInfo.Data) + { + return true; + } + return base.NeedsRefreshInternal(item, providerInfo); } /// <summary> + /// Gets the comparison data. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>Guid.</returns> + 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; + } + + /// <summary> + /// Gets the comparison data. + /// </summary> + /// <param name="seriesXmlFileInfo">The series XML file info.</param> + /// <returns>Guid.</returns> + private Guid GetComparisonData(FileInfo seriesXmlFileInfo) + { + var date = seriesXmlFileInfo.Exists ? seriesXmlFileInfo.LastWriteTimeUtc : DateTime.MinValue; + + var key = date.Ticks + seriesXmlFileInfo.FullName; + + return key.GetMD5(); + } + + /// <summary> /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// </summary> /// <param name="item">The item.</param> /// <param name="force">if set to <c>true</c> [force].</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{System.Boolean}.</returns> public override async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) { + if (HasLocalMeta(item)) + { + return false; + } + cancellationToken.ThrowIfCancellationRequested(); - + var episode = (Episode)item; - if (!HasLocalMeta(episode)) + + var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; + + if (!string.IsNullOrEmpty(seriesId)) { - var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; + var seriesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"); + + var seriesXmlFileInfo = new FileInfo(seriesXmlPath); - if (seriesId != null) + var status = ProviderRefreshStatus.Success; + + if (seriesXmlFileInfo.Exists) { - var status = await FetchEpisodeData(episode, seriesId, cancellationToken).ConfigureAwait(false); - SetLastRefreshed(item, DateTime.UtcNow, status); - return true; + var xmlDoc = new XmlDocument(); + xmlDoc.Load(seriesXmlPath); + + status = await FetchEpisodeData(xmlDoc, episode, seriesId, cancellationToken).ConfigureAwait(false); } - Logger.Info("Episode provider not fetching because series does not have a tvdb id: " + item.Path); - return 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 local meta exists or requested to ignore: " + item.Name); + + 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 /// <summary> /// Fetches the episode data. /// </summary> + /// <param name="seriesXml">The series XML.</param> /// <param name="episode">The episode.</param> /// <param name="seriesId">The series id.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{System.Boolean}.</returns> - private async Task<ProviderRefreshStatus> FetchEpisodeData(Episode episode, string seriesId, CancellationToken cancellationToken) + private async Task<ProviderRefreshStatus> 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; - - if (string.IsNullOrEmpty(seriesId)) return status; + var seasonNumber = episode.ParentIndexNumber ?? TVUtils.GetSeasonNumberFromEpisodeFile(episode.Path); - 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. - - if (!string.IsNullOrEmpty(seasonNumber)) - { - seasonNumber = seasonNumber.TrimStart('0'); - - if (string.IsNullOrEmpty(seasonNumber)) - { - seasonNumber = "0"; // Specials - } - - var url = string.Format(EpisodeQuery, TVUtils.TvdbApiKey, seriesId, seasonNumber, episodeNumber, ConfigurationManager.Configuration.PreferredMetadataLanguage); - var doc = new XmlDocument(); + var usingAbsoluteData = false; - using (var result = await HttpClient.Get(new HttpRequestOptions - { - Url = url, - ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true + var episodeNode = seriesXml.SelectSingleNode("//Episode[EpisodeNumber='" + episode.IndexNumber.Value + "'][SeasonNumber='" + seasonNumber.Value + "']"); - }).ConfigureAwait(false)) + if (episodeNode == null) + { + if (seasonNumber.Value == 1) { - doc.Load(result); + episodeNode = seriesXml.SelectSingleNode("//Episode[absolute_number='" + episode.IndexNumber.Value + "']"); + usingAbsoluteData = true; } + } - //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); + // If still null, nothing we can do + if (episodeNode == null) + { + return status; + } - using (var result = await HttpClient.Get(new HttpRequestOptions - { - Url = url, - ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true + var doc = new XmlDocument(); + doc.LoadXml(episodeNode.OuterXml); - }).ConfigureAwait(false)) - { - if (result != null) doc.Load(result); - usingAbsoluteData = true; - } - } - - if (doc.HasChildNodes) + if (!episode.HasImage(ImageType.Primary)) + { + var p = doc.SafeGetString("//filename"); + if (p != null) { - 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; - } - } - } + if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation); - 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) + try { - episode.PremiereDate = airDate.ToUniversalTime(); - episode.ProductionYear = airDate.Year; + episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken); } - - episode.People.Clear(); - - var actors = doc.SafeGetString("//GuestStars"); - if (actors != null) + catch (HttpException) { - foreach (var person in actors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = PersonType.GuestStar, Name = str })) - { - episode.AddPerson(person); - } + 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; + } - 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); - } - } + 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 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); + 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); + } + } - await _providerManager.SaveToLibraryFilesystem(episode, Path.Combine(episode.MetaLocation, Path.GetFileNameWithoutExtension(episode.Path) + ".xml"), ms, cancellationToken).ConfigureAwait(false); - } - return status; + 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 /// <value>The HTTP client.</value> protected IHttpClient HttpClient { get; private set; } + /// <summary> + /// The _provider manager + /// </summary> private readonly IProviderManager _providerManager; - + + /// <summary> + /// Initializes a new instance of the <see cref="RemoteSeasonProvider"/> class. + /// </summary> + /// <param name="httpClient">The HTTP client.</param> + /// <param name="logManager">The log manager.</param> + /// <param name="configurationManager">The configuration manager.</param> + /// <param name="providerManager">The provider manager.</param> + /// <exception cref="System.ArgumentNullException">httpClient</exception> public RemoteSeasonProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) : base(logManager, configurationManager) { @@ -70,6 +82,10 @@ namespace MediaBrowser.Controller.Providers.TV } } + /// <summary> + /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes + /// </summary> + /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value> protected override bool RefreshOnFileSystemStampChange { get @@ -79,6 +95,30 @@ namespace MediaBrowser.Controller.Providers.TV } /// <summary> + /// Gets a value indicating whether [refresh on version change]. + /// </summary> + /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value> + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + /// <summary> + /// Gets the provider version. + /// </summary> + /// <value>The provider version.</value> + protected override string ProviderVersion + { + get + { + return "1"; + } + } + + /// <summary> /// Needses the refresh internal. /// </summary> /// <param name="item">The item.</param> @@ -86,15 +126,52 @@ namespace MediaBrowser.Controller.Providers.TV /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> 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); } /// <summary> + /// Gets the comparison data. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>Guid.</returns> + 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; + } + + /// <summary> + /// Gets the comparison data. + /// </summary> + /// <param name="imagesFileInfo">The images file info.</param> + /// <returns>Guid.</returns> + private Guid GetComparisonData(FileInfo imagesFileInfo) + { + var date = imagesFileInfo.Exists ? imagesFileInfo.LastWriteTimeUtc : DateTime.MinValue; + + var key = date.Ticks + imagesFileInfo.FullName; + + return key.GetMD5(); + } + + /// <summary> /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// </summary> /// <param name="item">The item.</param> @@ -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; + + if (!string.IsNullOrEmpty(seriesId)) { - var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null; + // Process images + var imagesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), "banners.xml"); + + var imagesFileInfo = new FileInfo(imagesXmlPath); - if (seriesId != null) + 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); + await FetchImages(season, xmlDoc, cancellationToken).ConfigureAwait(false); + } + } - return true; + BaseProviderInfo data; + if (!item.ProviderData.TryGetValue(Id, out data)) + { + data = new BaseProviderInfo(); + item.ProviderData[Id] = data; } - 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); + + data.Data = GetComparisonData(imagesFileInfo); + + SetLastRefreshed(item, DateTime.UtcNow); + return true; } + return false; } - /// <summary> - /// Fetches the season data. + /// Fetches the images. /// </summary> /// <param name="season">The season.</param> - /// <param name="seriesId">The series id.</param> + /// <param name="images">The images.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{System.Boolean}.</returns> - private async Task<ProviderRefreshStatus> FetchSeasonData(Season season, string seriesId, CancellationToken cancellationToken) + /// <returns>Task.</returns> + private async Task FetchImages(Season season, XmlDocument images, CancellationToken cancellationToken) { - var seasonNumber = TVUtils.GetSeasonNumberFromPath(season.Path) ?? -1; - - season.IndexNumber = seasonNumber; - - if (seasonNumber == 0) - { - season.Name = "Specials"; - } - - var status = ProviderRefreshStatus.Success; + var seasonNumber = season.IndexNumber ?? -1; - if (string.IsNullOrEmpty(seriesId)) + if (seasonNumber == -1) { - return status; + return; } - if ((season.PrimaryImagePath == null) || (!season.HasImage(ImageType.Banner)) || (season.BackdropImagePaths == null)) + if (ConfigurationManager.Configuration.RefreshItemImages || !season.HasImage(ImageType.Primary)) { - 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"); - - 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); - } - } + 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 (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); - } - } + season.SetImage(ImageType.Banner, bannerImagePath); } + } + } - if (ConfigurationManager.Configuration.DownloadSeasonImages.Backdrops && (ConfigurationManager.Configuration.RefreshItemImages || !season.HasLocalImage("backdrop"))) + 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) { - 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<string>(); - 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<string>(); - - 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)); - } - } - } + if (season.BackdropImagePaths == null) season.BackdropImagePaths = new List<string>(); + 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)); } } } - return status; - } - - /// <summary> - /// Determines whether [has local meta] [the specified item]. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if [has local meta] [the specified item]; otherwise, <c>false</c>.</returns> - private bool HasLocalMeta(BaseItem item) - { - //just folder.jpg/png - return (item.ResolveArgs.ContainsMetaFileByName("folder.jpg") || - item.ResolveArgs.ContainsMetaFileByName("folder.png")); } - } } 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,16 +27,28 @@ namespace MediaBrowser.Controller.Providers.TV /// </summary> class RemoteSeriesProvider : BaseMetadataProvider, IDisposable { + /// <summary> + /// The _provider manager + /// </summary> private readonly IProviderManager _providerManager; - + /// <summary> /// The tv db /// </summary> - internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(3, 3); + internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(1, 1); + /// <summary> + /// Gets the current. + /// </summary> + /// <value>The current.</value> internal static RemoteSeriesProvider Current { get; private set; } /// <summary> + /// The _zip client + /// </summary> + private readonly IZipClient _zipClient; + + /// <summary> /// Gets the HTTP client. /// </summary> /// <value>The HTTP client.</value> @@ -47,8 +61,9 @@ namespace MediaBrowser.Controller.Providers.TV /// <param name="logManager">The log manager.</param> /// <param name="configurationManager">The configuration manager.</param> /// <param name="providerManager">The provider manager.</param> + /// <param name="zipClient">The zip client.</param> /// <exception cref="System.ArgumentNullException">httpClient</exception> - 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 /// </summary> private const string SeriesQuery = "GetSeries.php?seriesname={0}"; /// <summary> - /// The series get - /// </summary> - private const string SeriesGet = "http://www.thetvdb.com/api/{0}/series/{1}/{2}.xml"; - /// <summary> - /// The get actors + /// The series get zip /// </summary> - 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"; /// <summary> /// The LOCA l_ MET a_ FIL e_ NAME @@ -126,6 +138,30 @@ namespace MediaBrowser.Controller.Providers.TV } /// <summary> + /// Gets a value indicating whether [refresh on version change]. + /// </summary> + /// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value> + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + /// <summary> + /// Gets the provider version. + /// </summary> + /// <value>The provider version.</value> + protected override string ProviderVersion + { + get + { + return "1"; + } + } + + /// <summary> /// Needses the refresh internal. /// </summary> /// <param name="item">The item.</param> @@ -133,10 +169,44 @@ namespace MediaBrowser.Controller.Providers.TV /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> 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); } /// <summary> + /// Gets the comparison data. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>Guid.</returns> + 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; + } + /// <summary> /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// </summary> /// <param name="item">The item.</param> @@ -146,30 +216,40 @@ namespace MediaBrowser.Controller.Providers.TV public override async Task<bool> 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); + seriesId = await GetSeriesId(series, cancellationToken); + } - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - var status = ProviderRefreshStatus.Success; + var status = ProviderRefreshStatus.Success; - if (!string.IsNullOrEmpty(seriesId)) - { - series.SetProviderId(MetadataProviders.Tvdb, seriesId); + if (!string.IsNullOrEmpty(seriesId)) + { + series.SetProviderId(MetadataProviders.Tvdb, seriesId); - status = await FetchSeriesData(series, seriesId, cancellationToken).ConfigureAwait(false); - } + var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); - SetLastRefreshed(item, DateTime.UtcNow, status); - return true; + status = await FetchSeriesData(series, seriesId, seriesDataPath, cancellationToken).ConfigureAwait(false); } - Logger.Info("Series provider not fetching because local meta exists or requested to ignore: " + item.Name); - return 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; } /// <summary> @@ -177,263 +257,291 @@ namespace MediaBrowser.Controller.Providers.TV /// </summary> /// <param name="series">The series.</param> /// <param name="seriesId">The series id.</param> + /// <param name="seriesDataPath">The series data path.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{System.Boolean}.</returns> - private async Task<ProviderRefreshStatus> FetchSeriesData(Series series, string seriesId, CancellationToken cancellationToken) + private async Task<ProviderRefreshStatus> 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); + } + + // 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"); - string url = string.Format(SeriesGet, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage); - var doc = new XmlDocument(); + var seriesDoc = new XmlDocument(); + seriesDoc.Load(seriesXmlPath); - using (var xml = await HttpClient.Get(new HttpRequestOptions - { - Url = url, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true + FetchMainInfo(series, seriesDoc); + + var actorsDoc = new XmlDocument(); + actorsDoc.Load(actorsXmlPath); + + FetchActors(series, actorsDoc, seriesDoc); - }).ConfigureAwait(false)) + if (ConfigurationManager.Configuration.SaveLocalMeta) { - doc.Load(xml); + var ms = new MemoryStream(); + seriesDoc.Save(ms); + + 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); - - 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 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)); - } + // Process images + var imagesXmlPath = Path.Combine(seriesDataPath, "banners.xml"); - string s = doc.SafeGetString("//Network"); + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(imagesXmlPath); - if (!string.IsNullOrWhiteSpace(s)) - { - series.Studios.Clear(); + 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; + } - foreach (var studio in s.Trim().Split('|')) - { - series.AddStudio(studio); - } - } + return status; + } + + /// <summary> + /// Downloads the series zip. + /// </summary> + /// <param name="seriesId">The series id.</param> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, CancellationToken cancellationToken) + { + var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage); + + using (var zipStream = await HttpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvDbResourcePool, + CancellationToken = cancellationToken - series.OfficialRating = doc.SafeGetString("//ContentRating"); + }).ConfigureAwait(false)) + { + // Copy to memory stream because we need a seekable stream + using (var ms = new MemoryStream()) + { + await zipStream.CopyToAsync(ms).ConfigureAwait(false); - string g = doc.SafeGetString("//Genre"); + ms.Position = 0; + _zipClient.ExtractAll(ms, seriesDataPath, true); + } + } + } - if (g != null) - { - string[] genres = g.Trim('|').Split('|'); - if (g.Length > 0) - { - series.Genres.Clear(); + /// <summary> + /// Gets the series data path. + /// </summary> + /// <param name="appPaths">The app paths.</param> + /// <param name="seriesId">The series id.</param> + /// <returns>System.String.</returns> + internal static string GetSeriesDataPath(IApplicationPaths appPaths, string seriesId) + { + var seriesDataPath = Path.Combine(GetSeriesDataPath(appPaths), seriesId); - foreach (var genre in genres) - { - series.AddGenre(genre); - } - } - } + if (!Directory.Exists(seriesDataPath)) + { + Directory.CreateDirectory(seriesDataPath); + } - try - { - //wait for other tasks - await Task.WhenAll(actorTask, imageTask).ConfigureAwait(false); - } - catch (HttpException) - { - status = ProviderRefreshStatus.CompletedWithErrors; - } + return seriesDataPath; + } - if (ConfigurationManager.Configuration.SaveLocalMeta) - { - var ms = new MemoryStream(); - doc.Save(ms); + /// <summary> + /// Gets the series data path. + /// </summary> + /// <param name="appPaths">The app paths.</param> + /// <returns>System.String.</returns> + internal static string GetSeriesDataPath(IApplicationPaths appPaths) + { + var dataPath = Path.Combine(appPaths.DataPath, "tvdb"); - await _providerManager.SaveToLibraryFilesystem(series, Path.Combine(series.MetaLocation, LocalMetaFileName), ms, cancellationToken).ConfigureAwait(false); - } - } + if (!Directory.Exists(dataPath)) + { + Directory.CreateDirectory(dataPath); } - return status; + return dataPath; } /// <summary> - /// Fetches the actors. + /// Fetches the main info. /// </summary> /// <param name="series">The series.</param> - /// <param name="seriesId">The series id.</param> /// <param name="doc">The doc.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task FetchActors(Series series, string seriesId, XmlDocument doc, CancellationToken cancellationToken) + private void FetchMainInfo(Series series, XmlDocument doc) { - string urlActors = string.Format(GetActors, TVUtils.TvdbApiKey, seriesId); - var docActors = new XmlDocument(); + 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"); - using (var actors = await HttpClient.Get(new HttpRequestOptions - { - Url = urlActors, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true + string s = doc.SafeGetString("//Network"); - }).ConfigureAwait(false)) + if (!string.IsNullOrWhiteSpace(s)) { - docActors.Load(actors); + series.Studios.Clear(); + + foreach (var studio in s.Trim().Split('|')) + { + series.AddStudio(studio); + } } - if (docActors.HasChildNodes) + series.OfficialRating = doc.SafeGetString("//ContentRating"); + + string g = doc.SafeGetString("//Genre"); + + if (g != null) { - XmlNode actorsNode = null; - if (ConfigurationManager.Configuration.SaveLocalMeta) + string[] genres = g.Trim('|').Split('|'); + if (g.Length > 0) { - //add to the main doc for saving - var seriesNode = doc.SelectSingleNode("//Series"); - if (seriesNode != null) + series.Genres.Clear(); + + foreach (var genre in genres) { - actorsNode = doc.CreateNode(XmlNodeType.Element, "Persons", null); - seriesNode.AppendChild(actorsNode); + series.AddGenre(genre); } } + } + } - var xmlNodeList = docActors.SelectNodes("Actors/Actor"); - - if (xmlNodeList != null) + /// <summary> + /// Fetches the actors. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="actorsDoc">The actors doc.</param> + /// <param name="seriesDoc">The seriesDoc.</param> + /// <returns>Task.</returns> + 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) { - series.People.Clear(); + 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) + foreach (XmlNode p in xmlNodeList) + { + string actorName = p.SafeGetString("Name"); + string actorRole = p.SafeGetString("Role"); + if (!string.IsNullOrWhiteSpace(actorName)) { - 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.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); } + } } } } + /// <summary> + /// The us culture + /// </summary> protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - + /// <summary> /// Fetches the images. /// </summary> /// <param name="series">The series.</param> - /// <param name="seriesId">The series id.</param> + /// <param name="images">The images.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - 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 - { - Url = url, - ResourcePool = TvDbResourcePool, - CancellationToken = cancellationToken, - EnableResponseCache = true - - }).ConfigureAwait(false)) + n = n.SelectSingleNode("./BannerPath"); + if (n != null) { - 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 (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !series.HasImage(ImageType.Banner))) + { + var n = images.SelectSingleNode("//Banner[BannerType='series']"); + if (n != null) { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) + n = n.SelectSingleNode("./BannerPath"); + if (n != null) { - // If a series has no images this will produce a 404. - // Return gracefully so we don't keep retrying on subsequent scans - return; - } + var bannerImagePath = await _providerManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), ConfigurationManager.Configuration.SaveLocalMeta, TvDbResourcePool, cancellationToken); - throw; + 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) - { - 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 p = b.SelectSingleNode("./BannerPath"); - if (ConfigurationManager.Configuration.DownloadSeriesImages.Banner && (ConfigurationManager.Configuration.RefreshItemImages || !series.HasLocalImage("banner"))) - { - var n = images.SelectSingleNode("//Banner[BannerType='series']"); - if (n != null) + if (p != 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 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++; } - } - var bdNo = 0; - var xmlNodeList = images.SelectNodes("//Banner[BannerType='fanart']"); - if (xmlNodeList != null) - foreach (XmlNode b in xmlNodeList) - { - series.BackdropImagePaths = new List<string>(); - 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; - } - } + if (series.BackdropImagePaths.Count >= ConfigurationManager.Configuration.MaxBackdrops) break; + } } } } @@ -573,6 +681,9 @@ namespace MediaBrowser.Controller.Providers.TV return name.Trim(); } + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> 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 +{ + /// <summary> + /// Class TvdbPrescanTask + /// </summary> + public class TvdbPrescanTask : ILibraryPrescanTask + { + /// <summary> + /// The server time URL + /// </summary> + private const string ServerTimeUrl = "http://thetvdb.com/api/Updates.php?type=none"; + + /// <summary> + /// The updates URL + /// </summary> + private const string UpdatesUrl = "http://thetvdb.com/api/Updates.php?type=all&time={0}"; + + /// <summary> + /// The _HTTP client + /// </summary> + private readonly IHttpClient _httpClient; + /// <summary> + /// The _logger + /// </summary> + private readonly ILogger _logger; + /// <summary> + /// The _config + /// </summary> + private readonly IConfigurationManager _config; + + /// <summary> + /// Initializes a new instance of the <see cref="TvdbPrescanTask"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="httpClient">The HTTP client.</param> + /// <param name="config">The config.</param> + public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IConfigurationManager config) + { + _logger = logger; + _httpClient = httpClient; + _config = config; + } + + /// <summary> + /// Runs the specified progress. + /// </summary> + /// <param name="progress">The progress.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + public async Task Run(IProgress<double> 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); + } + + /// <summary> + /// Gets the series ids to update. + /// </summary> + /// <param name="existingSeriesIds">The existing series ids.</param> + /// <param name="lastUpdateTime">The last update time.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{IEnumerable{System.String}}.</returns> + private async Task<Tuple<IEnumerable<string>, string>> GetSeriesIdsToUpdate(IEnumerable<string> 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<XmlNode>() + .Select(i => i.InnerText) + .Where(i => !string.IsNullOrWhiteSpace(i) && existingSeriesIds.Contains(i, StringComparer.OrdinalIgnoreCase)); + + return new Tuple<IEnumerable<string>, string>(seriesList, newUpdateTime); + } + } + + /// <summary> + /// Updates the series. + /// </summary> + /// <param name="seriesIds">The series ids.</param> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + private async Task UpdateSeries(IEnumerable<string> 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; + } + } + } + } + + /// <summary> + /// Updates the series. + /// </summary> + /// <param name="id">The id.</param> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + 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 /// </summary> public class LibraryManager : ILibraryManager { + private IEnumerable<ILibraryPrescanTask> PrescanTasks { get; set; } + /// <summary> /// Gets the intro providers. /// </summary> @@ -161,13 +163,15 @@ namespace MediaBrowser.Server.Implementations.Library IEnumerable<IVirtualFolderCreator> pluginFolders, IEnumerable<IItemResolver> resolvers, IEnumerable<IIntroProvider> introProviders, - IEnumerable<IBaseItemComparer> itemComparers) + IEnumerable<IBaseItemComparer> itemComparers, + IEnumerable<ILibraryPrescanTask> prescanTasks) { EntityResolutionIgnoreRules = rules; PluginFolderCreators = pluginFolders; EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray(); IntroProviders = introProviders; Comparers = itemComparers; + PrescanTasks = prescanTasks; } /// <summary> @@ -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<double>(), cancellationToken); + } + catch (Exception ex) + { + _logger.ErrorException("Error running prescan task", ex); + } + } + var innerProgress = new ActionableProgress<double>(); 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 /// <returns>Episode.</returns> 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 (episode == null) + { + episode = base.Resolve(args); + } - if (episide != null) + if (episode != null) { - episide.IndexNumber = TVUtils.GetEpisodeNumberFromFile(args.Path, isInSeason); + 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 episide; + 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<IResolverIgnoreRule>(), GetExports<IVirtualFolderCreator>(), GetExports<IItemResolver>(), GetExports<IIntroProvider>(), GetExports<IBaseItemComparer>()), + () => LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(), + GetExports<IVirtualFolderCreator>(), + GetExports<IItemResolver>(), + GetExports<IIntroProvider>(), + GetExports<IBaseItemComparer>(), + GetExports<ILibraryPrescanTask>()), () => ProviderManager.AddMetadataProviders(GetExports<BaseMetadataProvider>().ToArray()) |
