reduce requests against tvdb by getting entire series metadata at once
This commit is contained in:
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>
|
||||
|
|
20
MediaBrowser.Controller/Library/ILibraryPrescanTask.cs
Normal file
20
MediaBrowser.Controller/Library/ILibraryPrescanTask.cs
Normal file
|
@ -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)
|
||||
{
|
||||
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
|
|||
/// <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;
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/// <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;
|
||||
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<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));
|
||||
}
|
||||
}
|
||||
}
|
||||
season.SetImage(ImageType.Banner, bannerImagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
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"));
|
||||
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<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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
/// The series get zip
|
||||
/// </summary>
|
||||
private const string SeriesGet = "http://www.thetvdb.com/api/{0}/series/{1}/{2}.xml";
|
||||
/// <summary>
|
||||
/// The get actors
|
||||
/// </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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the actors.
|
||||
/// Downloads the series zip.
|
||||
/// </summary>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="seriesId">The series id.</param>
|
||||
/// <param name="doc">The doc.</param>
|
||||
/// <param name="seriesDataPath">The series data path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
if (!Directory.Exists(seriesDataPath))
|
||||
{
|
||||
Directory.CreateDirectory(seriesDataPath);
|
||||
}
|
||||
|
||||
if (docActors.HasChildNodes)
|
||||
return seriesDataPath;
|
||||
}
|
||||
|
||||
/// <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");
|
||||
|
||||
if (!Directory.Exists(dataPath))
|
||||
{
|
||||
XmlNode actorsNode = null;
|
||||
if (ConfigurationManager.Configuration.SaveLocalMeta)
|
||||
Directory.CreateDirectory(dataPath);
|
||||
}
|
||||
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the main info.
|
||||
/// </summary>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="doc">The doc.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
|
204
MediaBrowser.Controller/Providers/TV/TvdbPrescanTask.cs
Normal file
204
MediaBrowser.Controller/Providers/TV/TvdbPrescanTask.cs
Normal file
|
@ -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 (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;
|
||||
|
|
|
@ -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…
Reference in New Issue
Block a user