reduce requests against tvdb by getting entire series metadata at once

pull/702/head
Luke Pulverenti 12 years ago
parent 96e8f053b5
commit f3a7307ebb

@ -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;
}

@ -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>

@ -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);
}
}

@ -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;
}
}

@ -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" />

@ -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)
{

@ -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
@ -88,6 +97,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>
@ -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;

@ -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
@ -78,6 +94,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>
@ -86,14 +126,51 @@ 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>
@ -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"));
}
}
}

@ -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
/// </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>
@ -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
@ -125,6 +137,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>
@ -133,9 +169,43 @@ 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>
@ -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);

@ -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);
}
}
}

@ -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));

@ -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;

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

Loading…
Cancel
Save