diff options
Diffstat (limited to 'MediaBrowser.Providers/TV/TvdbSeriesProvider.cs')
| -rw-r--r-- | MediaBrowser.Providers/TV/TvdbSeriesProvider.cs | 1228 |
1 files changed, 1228 insertions, 0 deletions
diff --git a/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs new file mode 100644 index 000000000..a22f4f1c3 --- /dev/null +++ b/MediaBrowser.Providers/TV/TvdbSeriesProvider.cs @@ -0,0 +1,1228 @@ +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.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +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; + +namespace MediaBrowser.Providers.TV +{ + /// <summary> + /// Class RemoteSeriesProvider + /// </summary> + class TvdbSeriesProvider : BaseMetadataProvider, IDisposable + { + /// <summary> + /// The tv db + /// </summary> + internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2); + + /// <summary> + /// Gets the current. + /// </summary> + /// <value>The current.</value> + internal static TvdbSeriesProvider Current { get; private set; } + + /// <summary> + /// The _zip client + /// </summary> + private readonly IZipClient _zipClient; + + /// <summary> + /// Gets the HTTP client. + /// </summary> + /// <value>The HTTP client.</value> + protected IHttpClient HttpClient { get; private set; } + + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="TvdbSeriesProvider" /> 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="zipClient">The zip client.</param> + /// <exception cref="System.ArgumentNullException">httpClient</exception> + public TvdbSeriesProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IZipClient zipClient, IFileSystem fileSystem) + : base(logManager, configurationManager) + { + if (httpClient == null) + { + throw new ArgumentNullException("httpClient"); + } + HttpClient = httpClient; + _zipClient = zipClient; + _fileSystem = fileSystem; + Current = this; + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + TvDbResourcePool.Dispose(); + } + } + + /// <summary> + /// The root URL + /// </summary> + private const string RootUrl = "http://www.thetvdb.com/api/"; + /// <summary> + /// The series query + /// </summary> + private const string SeriesQuery = "GetSeries.php?seriesname={0}"; + /// <summary> + /// The series get zip + /// </summary> + private const string SeriesGetZip = "http://www.thetvdb.com/api/{0}/series/{1}/all/{2}.zip"; + + /// <summary> + /// The LOCA l_ MET a_ FIL e_ NAME + /// </summary> + protected const string LocalMetaFileName = "series.xml"; + + /// <summary> + /// Supportses the specified item. + /// </summary> + /// <param name="item">The item.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + public override bool Supports(BaseItem item) + { + return item is Series; + } + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.Second; } + } + + /// <summary> + /// Gets a value indicating whether [requires internet]. + /// </summary> + /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value> + public override bool RequiresInternet + { + get + { + return true; + } + } + + /// <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 "2"; + } + } + + public override bool EnforceDontFetchMetadata + { + get + { + // Other providers depend on the xml downloaded here + return false; + } + } + + protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) + { + var seriesId = item.GetProviderId(MetadataProviders.Tvdb); + + if (!string.IsNullOrEmpty(seriesId)) + { + // Process images + var path = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); + + try + { + var files = new DirectoryInfo(path) + .EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly) + .Select(i => _fileSystem.GetLastWriteTimeUtc(i)) + .ToList(); + + if (files.Count > 0) + { + return files.Max() > providerInfo.LastRefreshed; + } + } + catch (DirectoryNotFoundException) + { + // Don't blow up + return true; + } + } + + return base.NeedsRefreshBasedOnCompareDate(item, providerInfo); + } + + /// <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 series = (Series)item; + + var seriesId = series.GetProviderId(MetadataProviders.Tvdb); + + if (string.IsNullOrEmpty(seriesId)) + { + seriesId = await FindSeries(series.Name, cancellationToken).ConfigureAwait(false); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (!string.IsNullOrEmpty(seriesId)) + { + var seriesDataPath = GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); + + await FetchSeriesData(series, seriesId, seriesDataPath, force, cancellationToken).ConfigureAwait(false); + } + + SetLastRefreshed(item, DateTime.UtcNow); + return true; + } + + /// <summary> + /// Fetches the series data. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="seriesId">The series id.</param> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="isForcedRefresh">if set to <c>true</c> [is forced refresh].</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{System.Boolean}.</returns> + private async Task FetchSeriesData(Series series, string seriesId, string seriesDataPath, bool isForcedRefresh, CancellationToken cancellationToken) + { + Directory.CreateDirectory(seriesDataPath); + + var files = Directory.EnumerateFiles(seriesDataPath, "*.xml", SearchOption.TopDirectoryOnly) + .Select(Path.GetFileName) + .ToList(); + + 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, null, cancellationToken).ConfigureAwait(false); + } + + // Have to check this here since we prevent the normal enforcement through ProviderManager + if (!series.DontFetchMeta) + { + // Examine if there's no local metadata, or save local is on (to get updates) + if (isForcedRefresh || ConfigurationManager.Configuration.EnableTvDbUpdates || !HasLocalMeta(series)) + { + series.SetProviderId(MetadataProviders.Tvdb, seriesId); + + var seriesXmlPath = Path.Combine(seriesDataPath, seriesXmlFilename); + var actorsXmlPath = Path.Combine(seriesDataPath, "actors.xml"); + + FetchSeriesInfo(series, seriesXmlPath, cancellationToken); + + if (!series.LockedFields.Contains(MetadataFields.Cast)) + { + series.People.Clear(); + + FetchActors(series, actorsXmlPath, cancellationToken); + } + } + } + } + + /// <summary> + /// Downloads the series zip. + /// </summary> + /// <param name="seriesId">The series id.</param> + /// <param name="seriesDataPath">The series data path.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + internal async Task DownloadSeriesZip(string seriesId, string seriesDataPath, long? lastTvDbUpdateTime, CancellationToken cancellationToken) + { + var url = string.Format(SeriesGetZip, TVUtils.TvdbApiKey, seriesId, ConfigurationManager.Configuration.PreferredMetadataLanguage); + + using (var zipStream = await HttpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvDbResourcePool, + CancellationToken = cancellationToken + + }).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.ExtractAll(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); + } + + await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, ConfigurationManager.Configuration.PreferredMetadataLanguage + ".xml"), lastTvDbUpdateTime).ConfigureAwait(false); + } + + private void DeleteXmlFiles(string path) + { + try + { + foreach (var file in new DirectoryInfo(path) + .EnumerateFiles("*.xml", SearchOption.AllDirectories) + .ToList()) + { + file.Delete(); + } + } + 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(); + } + + /// <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.IsNullOrEmpty(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 || !File.Exists(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 || !File.Exists(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.DataPath, "tvdb-v3"); + + return dataPath; + } + + 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 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": + { + if (!item.LockedFields.Contains(MetadataFields.Name)) + { + item.Name = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); + } + break; + } + + case "Overview": + { + if (!item.LockedFields.Contains(MetadataFields.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)) + { + if (!item.LockedFields.Contains(MetadataFields.OfficialRating)) + { + 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) && !item.LockedFields.Contains(MetadataFields.Runtime)) + { + 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)) + { + // Only fill this in if there's no existing genres, because Imdb data from Omdb is preferred + if (!item.LockedFields.Contains(MetadataFields.Genres) && (item.Genres.Count == 0 || !string.Equals(ConfigurationManager.Configuration.PreferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase))) + { + 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)) + { + if (!item.LockedFields.Contains(MetadataFields.Studios)) + { + 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; + } + } + } + } + + 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="series">The series.</param> + /// <param name="actorsXmlPath">The actors XML path.</param> + /// <param name="cancellationToken">The cancellation token.</param> + private void FetchActors(Series series, string actorsXmlPath, CancellationToken cancellationToken) + { + 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()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "Actor": + { + using (var subtree = reader.ReadSubtree()) + { + FetchDataFromActorNode(series, subtree); + } + break; + } + default: + reader.Skip(); + break; + } + } + } + } + } + } + + /// <summary> + /// Fetches the data from actor node. + /// </summary> + /// <param name="series">The series.</param> + /// <param name="reader">The reader.</param> + private void FetchDataFromActorNode(Series series, 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; + } + + default: + reader.Skip(); + break; + } + } + } + + personInfo.Type = PersonType.Actor; + + if (!string.IsNullOrEmpty(personInfo.Name)) + { + series.AddPerson(personInfo); + } + } + + /// <summary> + /// The us culture + /// </summary> + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// <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) + { + return item.ResolveArgs.ContainsMetaFileByName(LocalMetaFileName); + } + + /// <summary> + /// Finds the series. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task{System.String}.</returns> + private async Task<string> FindSeries(string name, CancellationToken cancellationToken) + { + var url = string.Format(RootUrl + SeriesQuery, WebUtility.UrlEncode(name)); + var doc = new XmlDocument(); + + using (var results = await HttpClient.Get(new HttpRequestOptions + { + Url = url, + ResourcePool = TvDbResourcePool, + CancellationToken = cancellationToken + + }).ConfigureAwait(false)) + { + doc.Load(results); + } + + if (doc.HasChildNodes) + { + var nodes = doc.SelectNodes("//Series"); + var comparableName = GetComparableName(name); + if (nodes != null) + { + foreach (XmlNode node in nodes) + { + 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); + } + + if (titles.Any(t => string.Equals(t, comparableName, StringComparison.OrdinalIgnoreCase))) + { + var id = node.SelectSingleNode("./seriesid"); + if (id != null) + return id.InnerText; + } + + foreach (var title in titles) + { + Logger.Info("TVDb Provider - " + title + " did not match " + comparableName); + } + } + } + } + + // Try stripping off the year if it was supplied + var parenthIndex = name.LastIndexOf('('); + + if (parenthIndex != -1) + { + var newName = name.Substring(0, parenthIndex); + + return await FindSeries(newName, cancellationToken); + } + + Logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org."); + return null; + } + + /// <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(); + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + } + } +} |
