You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jellyfin/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs

1329 lines
51 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using CommonIO;
namespace MediaBrowser.Providers.TV
{
public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IItemIdentityProvider<SeriesInfo>, IHasOrder
{
private const string TvdbSeriesOffset = "TvdbSeriesOffset";
private const string TvdbSeriesOffsetFormat = "{0}-{1}";
internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2);
internal static TvdbSeriesProvider Current { get; private set; }
private readonly IZipClient _zipClient;
private readonly IHttpClient _httpClient;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _config;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly ILogger _logger;
private readonly ISeriesOrderManager _seriesOrder;
private readonly ILibraryManager _libraryManager;
public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ISeriesOrderManager seriesOrder, ILibraryManager libraryManager)
{
_zipClient = zipClient;
_httpClient = httpClient;
_fileSystem = fileSystem;
_config = config;
_logger = logger;
_seriesOrder = seriesOrder;
_libraryManager = libraryManager;
Current = this;
}
private const string SeriesSearchUrl = "http://www.thetvdb.com/api/GetSeries.php?seriesname={0}&language={1}";
private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip";
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
if (IsValidSeries(searchInfo.ProviderIds))
{
var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
if (metadata.HasMetadata)
{
return new List<RemoteSearchResult>
{
new RemoteSearchResult
{
Name = metadata.Item.Name,
PremiereDate = metadata.Item.PremiereDate,
ProductionYear = metadata.Item.ProductionYear,
ProviderIds = metadata.Item.ProviderIds,
SearchProviderName = Name
}
};
}
}
return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
}
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken)
{
var result = new MetadataResult<Series>();
if (!IsValidSeries(itemId.ProviderIds))
{
await Identify(itemId).ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
if (IsValidSeries(itemId.ProviderIds))
{
await EnsureSeriesInfo(itemId.ProviderIds, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false);
result.Item = new Series();
result.HasMetadata = true;
FetchSeriesData(result, itemId.GetProviderId(MetadataProviders.Tvdb), cancellationToken);
await FindAnimeSeriesIndex(result.Item, itemId).ConfigureAwait(false);
}
return result;
}
private async Task FindAnimeSeriesIndex(Series series, SeriesInfo info)
{
var index = await _seriesOrder.FindSeriesIndex(SeriesOrderTypes.Anime, series.Name);
if (index == null)
return;
var offset = info.AnimeSeriesIndex - index;
var id = string.Format(TvdbSeriesOffsetFormat, series.GetProviderId(MetadataProviders.Tvdb), offset);
series.SetProviderId(TvdbSeriesOffset, id);
}
internal static int? GetSeriesOffset(Dictionary<string, string> seriesProviderIds)
{
string idString;
if (!seriesProviderIds.TryGetValue(TvdbSeriesOffset, out idString))
return null;
var parts = idString.Split('-');
if (parts.Length < 2)
return null;
int offset;
if (int.TryParse(parts[1], out offset))
return offset;
return null;
}
/// <summary>
/// Fetches the series data.
/// </summary>
/// <param name="result">The result.</param>
/// <param name="seriesId">The series id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.Boolean}.</returns>
private void FetchSeriesData(MetadataResult<Series> result, string seriesId, CancellationToken cancellationToken)
{
var series = result.Item;
series.SetProviderId(MetadataProviders.Tvdb, seriesId);
var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesId);
var seriesXmlFilename = series.GetPreferredMetadataLanguage().ToLower() + ".xml";
var seriesXmlPath = Path.Combine(seriesDataPath, seriesXmlFilename);
var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml");
FetchSeriesInfo(series, seriesXmlPath, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
result.ResetPeople();
FetchActors(result, actorsXmlPath);
}
/// <summary>
/// Downloads the series zip.
/// </summary>
/// <param name="seriesId">The series id.</param>
/// <param name="seriesDataPath">The series data path.</param>
/// <param name="lastTvDbUpdateTime">The last tv database update time.</param>
/// <param name="preferredMetadataLanguage">The preferred metadata language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken)
{
try
{
await DownloadSeriesZip(seriesId, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
return;
}
catch (HttpException ex)
{
if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
{
throw;
}
}
if (!string.Equals(preferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase))
{
await DownloadSeriesZip(seriesId, seriesDataPath, lastTvDbUpdateTime, "en", preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
}
}
private async Task DownloadSeriesZip(string seriesId, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, string saveAsMetadataLanguage, CancellationToken cancellationToken)
{
var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, preferredMetadataLanguage);
using (var zipStream = await _httpClient.Get(new HttpRequestOptions
{
Url = url,
ResourcePool = TvDbResourcePool,
CancellationToken = cancellationToken
}).ConfigureAwait(false))
{
// Delete existing files
DeleteXmlFiles(seriesDataPath);
// 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.ExtractAllFromZip(ms, seriesDataPath, true);
}
}
// Sanitize all files, except for extracted episode files
foreach (var file in Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.AllDirectories).ToList()
.Where(i => !Path.GetFileName(i).StartsWith("episode-", StringComparison.OrdinalIgnoreCase)))
{
await SanitizeXmlFile(file).ConfigureAwait(false);
}
var downloadLangaugeXmlFile = Path.Combine(seriesDataPath, preferredMetadataLanguage + ".xml");
var saveAsLanguageXmlFile = Path.Combine(seriesDataPath, saveAsMetadataLanguage + ".xml");
if (!string.Equals(downloadLangaugeXmlFile, saveAsLanguageXmlFile, StringComparison.OrdinalIgnoreCase))
{
_fileSystem.CopyFile(downloadLangaugeXmlFile, saveAsLanguageXmlFile, true);
}
await ExtractEpisodes(seriesDataPath, downloadLangaugeXmlFile, lastTvDbUpdateTime).ConfigureAwait(false);
}
public TvdbOptions GetTvDbOptions()
{
return _config.GetConfiguration<TvdbOptions>("tvdb");
}
internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
{
string id;
if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out id))
{
return true;
}
//if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id))
//{
// return true;
//}
return false;
}
internal async Task<string> EnsureSeriesInfo(Dictionary<string,string> seriesProviderIds, string preferredMetadataLanguage, CancellationToken cancellationToken)
{
string seriesId;
if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesId))
{
var seriesDataPath = GetSeriesDataPath(_config.ApplicationPaths, seriesId);
// Only download if not already there
// The post-scan task will take care of updates so we don't need to re-download here
if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage))
{
await DownloadSeriesZip(seriesId, seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
}
return seriesDataPath;
}
return null;
}
private bool IsCacheValid(string seriesDataPath, string preferredMetadataLanguage)
{
try
{
var files = _fileSystem.GetFiles(seriesDataPath)
.ToList();
var seriesXmlFilename = preferredMetadataLanguage + ".xml";
var automaticUpdatesEnabled = GetTvDbOptions().EnableAutomaticUpdates;
const int cacheDays = 2;
var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase));
// No need to check age if automatic updates are enabled
if (seriesFile == null || !seriesFile.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(seriesFile)).TotalDays > cacheDays))
{
return false;
}
var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase));
// No need to check age if automatic updates are enabled
if (actorsXml == null || !actorsXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(actorsXml)).TotalDays > cacheDays))
{
return false;
}
var bannersXml = files.FirstOrDefault(i => string.Equals("banners.xml", i.Name, StringComparison.OrdinalIgnoreCase));
// No need to check age if automatic updates are enabled
if (bannersXml == null || !bannersXml.Exists || (!automaticUpdatesEnabled && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(bannersXml)).TotalDays > cacheDays))
{
return false;
}
return true;
}
catch (DirectoryNotFoundException)
{
return false;
}
catch (FileNotFoundException)
{
return false;
}
}
/// <summary>
/// Finds the series.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="year">The year.</param>
/// <param name="language">The language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.String}.</returns>
private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken)
{
var results = (await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false)).ToList();
if (results.Count == 0)
{
var parsedName = _libraryManager.ParseName(name);
var nameWithoutYear = parsedName.Name;
if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase))
{
results = (await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false)).ToList();
}
}
return results.Where(i =>
{
if (year.HasValue && i.ProductionYear.HasValue)
{
// Allow one year tolerance
return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
}
return true;
});
}
private async Task<IEnumerable<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken)
{
var url = string.Format(SeriesSearchUrl, WebUtility.UrlEncode(name), language.ToLower());
var doc = new XmlDocument();
using (var results = await _httpClient.Get(new HttpRequestOptions
{
Url = url,
ResourcePool = TvDbResourcePool,
CancellationToken = cancellationToken
}).ConfigureAwait(false))
{
doc.Load(results);
}
var searchResults = new List<RemoteSearchResult>();
if (doc.HasChildNodes)
{
var nodes = doc.SelectNodes("//Series");
var comparableName = GetComparableName(name);
if (nodes != null)
{
foreach (XmlNode node in nodes)
{
var searchResult = new RemoteSearchResult
{
SearchProviderName = Name
};
var titles = new List<string>();
var nameNode = node.SelectSingleNode("./SeriesName");
if (nameNode != null)
{
titles.Add(GetComparableName(nameNode.InnerText));
}
var aliasNode = node.SelectSingleNode("./AliasNames");
if (aliasNode != null)
{
var alias = aliasNode.InnerText.Split('|').Select(GetComparableName);
titles.AddRange(alias);
}
var imdbIdNode = node.SelectSingleNode("./IMDB_ID");
if (imdbIdNode != null)
{
var val = imdbIdNode.InnerText;
if (!string.IsNullOrWhiteSpace(val))
{
searchResult.SetProviderId(MetadataProviders.Imdb, val);
}
}
var bannerNode = node.SelectSingleNode("./banner");
if (bannerNode != null)
{
var val = bannerNode.InnerText;
if (!string.IsNullOrWhiteSpace(val))
{
searchResult.ImageUrl = TVUtils.BannerUrl + val;
}
}
var airDateNode = node.SelectSingleNode("./FirstAired");
if (airDateNode != null)
{
var val = airDateNode.InnerText;
if (!string.IsNullOrWhiteSpace(val))
{
DateTime date;
if (DateTime.TryParse(val, out date))
{
searchResult.ProductionYear = date.Year;
}
}
}
foreach (var title in titles)
{
if (string.Equals(title, comparableName, StringComparison.OrdinalIgnoreCase))
{
var id = node.SelectSingleNode("./seriesid") ??
node.SelectSingleNode("./id");
if (id != null)
{
searchResult.Name = title;
searchResult.SetProviderId(MetadataProviders.Tvdb, id.InnerText);
searchResults.Add(searchResult);
}
break;
}
_logger.Info("TVDb Provider - " + title + " did not match " + comparableName);
}
}
}
}
if (searchResults.Count == 0)
{
_logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org.");
}
return searchResults;
}
/// <summary>
/// The remove
/// </summary>
const string remove = "\"'!`?";
/// <summary>
/// The spacers
/// </summary>
const string spacers = "/,.:;\\(){}[]+-_=*"; // (there are not actually two - in the they are different char codes)
/// <summary>
/// Gets the name of the comparable.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>System.String.</returns>
internal static string GetComparableName(string name)
{
name = name.ToLower();
name = name.Normalize(NormalizationForm.FormKD);
var sb = new StringBuilder();
foreach (var c in name)
{
if ((int)c >= 0x2B0 && (int)c <= 0x0333)
{
// skip char modifier and diacritics
}
else if (remove.IndexOf(c) > -1)
{
// skip chars we are removing
}
else if (spacers.IndexOf(c) > -1)
{
sb.Append(" ");
}
else if (c == '&')
{
sb.Append(" and ");
}
else
{
sb.Append(c);
}
}
name = sb.ToString();
name = name.Replace(", the", "");
name = name.Replace("the ", " ");
name = name.Replace(" the ", " ");
string prevName;
do
{
prevName = name;
name = name.Replace(" ", " ");
} while (name.Length != prevName.Length);
return name.Trim();
}
private void FetchSeriesInfo(Series item, string seriesXmlPath, CancellationToken cancellationToken)
{
var settings = new XmlReaderSettings
{
CheckCharacters = false,
IgnoreProcessingInstructions = true,
IgnoreComments = true,
ValidationType = ValidationType.None
};
var episiodeAirDates = new List<DateTime>();
using (var streamReader = new StreamReader(seriesXmlPath, Encoding.UTF8))
{
// Use XmlReader for best performance
using (var reader = XmlReader.Create(streamReader, settings))
{
reader.MoveToContent();
// Loop through each element
while (reader.Read())
{
cancellationToken.ThrowIfCancellationRequested();
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "Series":
{
using (var subtree = reader.ReadSubtree())
{
FetchDataFromSeriesNode(item, subtree, cancellationToken);
}
break;
}
case "Episode":
{
using (var subtree = reader.ReadSubtree())
{
var date = GetFirstAiredDateFromEpisodeNode(subtree, cancellationToken);
if (date.HasValue)
{
episiodeAirDates.Add(date.Value);
}
}
break;
}
default:
reader.Skip();
break;
}
}
}
}
}
if (item.Status.HasValue && item.Status.Value == SeriesStatus.Ended && episiodeAirDates.Count > 0)
{
item.EndDate = episiodeAirDates.Max();
}
}
private DateTime? GetFirstAiredDateFromEpisodeNode(XmlReader reader, CancellationToken cancellationToken)
{
DateTime? airDate = null;
int? seasonNumber = null;
reader.MoveToContent();
// Loop through each element
while (reader.Read())
{
cancellationToken.ThrowIfCancellationRequested();
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "FirstAired":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
DateTime date;
if (DateTime.TryParse(val, out date))
{
airDate = date.ToUniversalTime();
}
}
break;
}
case "SeasonNumber":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
int rval;
// int.TryParse is local aware, so it can be probamatic, force us culture
if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
{
seasonNumber = rval;
}
}
break;
}
default:
reader.Skip();
break;
}
}
}
if (seasonNumber.HasValue && seasonNumber.Value != 0)
{
return airDate;
}
return null;
}
/// <summary>
/// Fetches the actors.
/// </summary>
/// <param name="result">The result.</param>
/// <param name="actorsXmlPath">The actors XML path.</param>
private void FetchActors(MetadataResult<Series> result, string actorsXmlPath)
{
var settings = new XmlReaderSettings
{
CheckCharacters = false,
IgnoreProcessingInstructions = true,
IgnoreComments = true,
ValidationType = ValidationType.None
};
using (var streamReader = new StreamReader(actorsXmlPath, Encoding.UTF8))
{
// Use XmlReader for best performance
using (var reader = XmlReader.Create(streamReader, settings))
{
reader.MoveToContent();
// Loop through each element
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "Actor":
{
using (var subtree = reader.ReadSubtree())
{
FetchDataFromActorNode(result, subtree);
}
break;
}
default:
reader.Skip();
break;
}
}
}
}
}
}
/// <summary>
/// Fetches the data from actor node.
/// </summary>
/// <param name="result">The result.</param>
/// <param name="reader">The reader.</param>
private void FetchDataFromActorNode(MetadataResult<Series> result, XmlReader reader)
{
reader.MoveToContent();
var personInfo = new PersonInfo();
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "Name":
{
personInfo.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
break;
}
case "Role":
{
personInfo.Role = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
break;
}
case "SortOrder":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
int rval;
// int.TryParse is local aware, so it can be probamatic, force us culture
if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
{
personInfo.SortOrder = rval;
}
}
break;
}
default:
reader.Skip();
break;
}
}
}
personInfo.Type = PersonType.Actor;
if (!string.IsNullOrWhiteSpace(personInfo.Name))
{
result.AddPerson(personInfo);
}
}
private void FetchDataFromSeriesNode(Series item, XmlReader reader, CancellationToken cancellationToken)
{
reader.MoveToContent();
// Loop through each element
while (reader.Read())
{
cancellationToken.ThrowIfCancellationRequested();
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "SeriesName":
{
item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
break;
}
case "Overview":
{
item.Overview = (reader.ReadElementContentAsString() ?? string.Empty).Trim();
break;
}
case "Airs_DayOfWeek":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.AirDays = TVUtils.GetAirDays(val);
}
break;
}
case "Airs_Time":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.AirTime = val;
}
break;
}
case "ContentRating":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.OfficialRating = val;
}
break;
}
case "Rating":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
// Only fill this if it doesn't already have a value, since we get it from imdb which has better data
if (!item.CommunityRating.HasValue || string.IsNullOrWhiteSpace(item.GetProviderId(MetadataProviders.Imdb)))
{
float rval;
// float.TryParse is local aware, so it can be probamatic, force us culture
if (float.TryParse(val, NumberStyles.AllowDecimalPoint, _usCulture, out rval))
{
item.CommunityRating = rval;
}
}
}
break;
}
case "RatingCount":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
int rval;
// int.TryParse is local aware, so it can be probamatic, force us culture
if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
{
item.VoteCount = rval;
}
}
break;
}
case "IMDB_ID":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.SetProviderId(MetadataProviders.Imdb, val);
}
break;
}
case "zap2it_id":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.SetProviderId(MetadataProviders.Zap2It, val);
}
break;
}
case "Status":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
SeriesStatus seriesStatus;
if (Enum.TryParse(val, true, out seriesStatus))
item.Status = seriesStatus;
}
break;
}
case "FirstAired":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
DateTime date;
if (DateTime.TryParse(val, out date))
{
date = date.ToUniversalTime();
item.PremiereDate = date;
item.ProductionYear = date.Year;
}
}
break;
}
case "Runtime":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
int rval;
// int.TryParse is local aware, so it can be probamatic, force us culture
if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
{
item.RunTimeTicks = TimeSpan.FromMinutes(rval).Ticks;
}
}
break;
}
case "Genre":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
var vals = val
.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.Where(i => !string.IsNullOrWhiteSpace(i))
.ToList();
if (vals.Count > 0)
{
item.Genres.Clear();
foreach (var genre in vals)
{
item.AddGenre(genre);
}
}
}
break;
}
case "Network":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
var vals = val
.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.Where(i => !string.IsNullOrWhiteSpace(i))
.ToList();
if (vals.Count > 0)
{
item.Studios.Clear();
foreach (var genre in vals)
{
item.AddStudio(genre);
}
}
}
break;
}
default:
reader.Skip();
break;
}
}
}
}
/// <summary>
/// Extracts info for each episode into invididual xml files so that they can be easily accessed without having to step through the entire series xml
/// </summary>
/// <param name="seriesDataPath">The series data path.</param>
/// <param name="xmlFile">The XML file.</param>
/// <param name="lastTvDbUpdateTime">The last tv db update time.</param>
/// <returns>Task.</returns>
private async Task ExtractEpisodes(string seriesDataPath, string xmlFile, long? lastTvDbUpdateTime)
{
var settings = new XmlReaderSettings
{
CheckCharacters = false,
IgnoreProcessingInstructions = true,
IgnoreComments = true,
ValidationType = ValidationType.None
};
using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8))
{
// Use XmlReader for best performance
using (var reader = XmlReader.Create(streamReader, settings))
{
reader.MoveToContent();
// Loop through each element
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "Episode":
{
var outerXml = reader.ReadOuterXml();
await SaveEpsiodeXml(seriesDataPath, outerXml, lastTvDbUpdateTime).ConfigureAwait(false);
break;
}
default:
reader.Skip();
break;
}
}
}
}
}
}
private async Task SaveEpsiodeXml(string seriesDataPath, string xml, long? lastTvDbUpdateTime)
{
var settings = new XmlReaderSettings
{
CheckCharacters = false,
IgnoreProcessingInstructions = true,
IgnoreComments = true,
ValidationType = ValidationType.None
};
var seasonNumber = -1;
var episodeNumber = -1;
var absoluteNumber = -1;
var lastUpdateString = string.Empty;
using (var streamReader = new StringReader(xml))
{
// Use XmlReader for best performance
using (var reader = XmlReader.Create(streamReader, settings))
{
reader.MoveToContent();
// Loop through each element
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "lastupdated":
{
lastUpdateString = reader.ReadElementContentAsString();
break;
}
case "EpisodeNumber":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
int num;
if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
{
episodeNumber = num;
}
}
break;
}
case "absolute_number":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
int num;
if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
{
absoluteNumber = num;
}
}
break;
}
case "SeasonNumber":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
int num;
if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num))
{
seasonNumber = num;
}
}
break;
}
default:
reader.Skip();
break;
}
}
}
}
}
var hasEpisodeChanged = true;
if (!string.IsNullOrWhiteSpace(lastUpdateString) && lastTvDbUpdateTime.HasValue)
{
long num;
if (long.TryParse(lastUpdateString, NumberStyles.Any, _usCulture, out num))
{
hasEpisodeChanged = num >= lastTvDbUpdateTime.Value;
}
}
var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber));
// Only save the file if not already there, or if the episode has changed
if (hasEpisodeChanged || !_fileSystem.FileExists(file))
{
using (var writer = XmlWriter.Create(file, new XmlWriterSettings
{
Encoding = Encoding.UTF8,
Async = true
}))
{
await writer.WriteRawAsync(xml).ConfigureAwait(false);
}
}
if (absoluteNumber != -1)
{
file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", absoluteNumber));
// Only save the file if not already there, or if the episode has changed
if (hasEpisodeChanged || !_fileSystem.FileExists(file))
{
using (var writer = XmlWriter.Create(file, new XmlWriterSettings
{
Encoding = Encoding.UTF8,
Async = true
}))
{
await writer.WriteRawAsync(xml).ConfigureAwait(false);
}
}
}
}
/// <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);
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.CachePath, "tvdb");
return dataPath;
}
private void DeleteXmlFiles(string path)
{
try
{
foreach (var file in _fileSystem.GetFilePaths(path, true)
.ToList())
{
_fileSystem.DeleteFile(file);
}
}
catch (DirectoryNotFoundException)
{
// No biggie
}
}
/// <summary>
/// Sanitizes the XML file.
/// </summary>
/// <param name="file">The file.</param>
/// <returns>Task.</returns>
private async Task SanitizeXmlFile(string file)
{
string validXml;
using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, true))
{
using (var reader = new StreamReader(fileStream))
{
var xml = await reader.ReadToEndAsync().ConfigureAwait(false);
validXml = StripInvalidXmlCharacters(xml);
}
}
using (var fileStream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, true))
{
using (var writer = new StreamWriter(fileStream))
{
await writer.WriteAsync(validXml).ConfigureAwait(false);
}
}
}
/// <summary>
/// Strips the invalid XML characters.
/// </summary>
/// <param name="inString">The in string.</param>
/// <returns>System.String.</returns>
public static string StripInvalidXmlCharacters(string inString)
{
if (inString == null) return null;
var sbOutput = new StringBuilder();
char ch;
for (int i = 0; i < inString.Length; i++)
{
ch = inString[i];
if ((ch >= 0x0020 && ch <= 0xD7FF) ||
(ch >= 0xE000 && ch <= 0xFFFD) ||
ch == 0x0009 ||
ch == 0x000A ||
ch == 0x000D)
{
sbOutput.Append(ch);
}
}
return sbOutput.ToString();
}
public string Name
{
get { return "TheTVDB"; }
}
public async Task Identify(SeriesInfo info)
{
if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProviders.Tvdb)))
{
return;
}
var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None).ConfigureAwait(false);
var entry = srch.FirstOrDefault();
if (entry != null)
{
var id = entry.GetProviderId(MetadataProviders.Tvdb);
info.SetProviderId(MetadataProviders.Tvdb, id);
}
}
public int Order
{
get
{
// After Omdb
return 1;
}
}
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClient.GetResponse(new HttpRequestOptions
{
CancellationToken = cancellationToken,
Url = url,
ResourcePool = TvDbResourcePool
});
}
}
public class TvdbConfigStore : IConfigurationFactory
{
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new List<ConfigurationStore>
{
new ConfigurationStore
{
Key = "tvdb",
ConfigurationType = typeof(TvdbOptions)
}
};
}
}
}