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())