diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index fbdbf15ff6..2406d0470e 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -121,9 +121,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks { LazyInitializer.EnsureInitialized(ref _lastExecutionResult, ref _lastExecutionResultinitialized, ref _lastExecutionResultSyncLock, () => { + var path = GetHistoryFilePath(false); + try { - return JsonSerializer.DeserializeFromFile(GetHistoryFilePath(false)); + return JsonSerializer.DeserializeFromFile(path); } catch (DirectoryNotFoundException) { @@ -135,6 +137,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks // File doesn't exist. No biggie return null; } + catch (Exception ex) + { + Logger.ErrorException("Error deserializing {0}", ex, path); + return null; + } }); return _lastExecutionResult; diff --git a/MediaBrowser.Controller/Providers/IImageEnhancer.cs b/MediaBrowser.Controller/Providers/IImageEnhancer.cs index 21604e9d60..54ba6d3228 100644 --- a/MediaBrowser.Controller/Providers/IImageEnhancer.cs +++ b/MediaBrowser.Controller/Providers/IImageEnhancer.cs @@ -1,7 +1,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; -using System; using System.Drawing; using System.Threading.Tasks; diff --git a/MediaBrowser.Providers/Extensions/XDocumentExtensions.cs b/MediaBrowser.Providers/Extensions/XDocumentExtensions.cs deleted file mode 100644 index 9df920955f..0000000000 --- a/MediaBrowser.Providers/Extensions/XDocumentExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Xml; -using System.Xml.Linq; - -namespace MediaBrowser.Providers.Extensions -{ - public static class XDocumentExtensions - { - public static XmlDocument ToXmlDocument(this XElement xDocument) - { - var xmlDocument = new XmlDocument(); - using (var xmlReader = xDocument.CreateReader()) - { - xmlDocument.Load(xmlReader); - } - return xmlDocument; - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Providers/Extensions/XmlExtensions.cs b/MediaBrowser.Providers/Extensions/XmlExtensions.cs deleted file mode 100644 index 9d1fd28d83..0000000000 --- a/MediaBrowser.Providers/Extensions/XmlExtensions.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Globalization; -using System.Xml; - -namespace MediaBrowser.Providers.Extensions -{ - /// - /// Class XmlExtensions - /// - public static class XmlExtensions - { - - /// - /// Safes the get int32. - /// - /// The doc. - /// The path. - /// System.Int32. - public static int SafeGetInt32(this XmlDocument doc, string path) - { - return SafeGetInt32(doc, path, 0); - } - - /// - /// Safes the get int32. - /// - /// The doc. - /// The path. - /// The default int. - /// System.Int32. - public static int SafeGetInt32(this XmlDocument doc, string path, int defaultInt) - { - XmlNode rvalNode = doc.SelectSingleNode(path); - if (rvalNode != null && rvalNode.InnerText.Length > 0) - { - int rval; - if (Int32.TryParse(rvalNode.InnerText, out rval)) - { - return rval; - } - - } - return defaultInt; - } - - /// - /// The _us culture - /// - private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - /// - /// Safes the get single. - /// - /// The doc. - /// The path. - /// The min value. - /// The max value. - /// System.Single. - public static float SafeGetSingle(this XmlDocument doc, string path, float minValue, float maxValue) - { - XmlNode rvalNode = doc.SelectSingleNode(path); - if (rvalNode != null && rvalNode.InnerText.Length > 0) - { - float rval; - // float.TryParse is local aware, so it can be probamatic, force us culture - if (float.TryParse(rvalNode.InnerText, NumberStyles.AllowDecimalPoint, _usCulture, out rval)) - { - if (rval >= minValue && rval <= maxValue) - { - return rval; - } - } - - } - return minValue; - } - - - /// - /// Safes the get string. - /// - /// The doc. - /// The path. - /// System.String. - public static string SafeGetString(this XmlDocument doc, string path) - { - return SafeGetString(doc, path, null); - } - - /// - /// Safes the get string. - /// - /// The doc. - /// The path. - /// The default string. - /// System.String. - public static string SafeGetString(this XmlDocument doc, string path, string defaultString) - { - var rvalNode = doc.SelectSingleNode(path); - - if (rvalNode != null) - { - var text = rvalNode.InnerText; - - return !string.IsNullOrWhiteSpace(text) ? text : defaultString; - } - - return defaultString; - } - - /// - /// Safes the get DateTime. - /// - /// The doc. - /// The path. - /// System.DateTime. - public static DateTime? SafeGetDateTime(this XmlDocument doc, string path) - { - return SafeGetDateTime(doc, path, null); - } - - /// - /// Safes the get DateTime. - /// - /// The doc. - /// The path. - /// The default date. - /// System.DateTime. - public static DateTime? SafeGetDateTime(this XmlDocument doc, string path, DateTime? defaultDate) - { - var rvalNode = doc.SelectSingleNode(path); - - if (rvalNode != null) - { - var text = rvalNode.InnerText; - DateTime date; - if (DateTime.TryParse(text, out date)) - return date.ToUniversalTime(); - } - return defaultDate; - } - - /// - /// Safes the get string. - /// - /// The doc. - /// The path. - /// System.String. - public static string SafeGetString(this XmlNode doc, string path) - { - return SafeGetString(doc, path, null); - } - - /// - /// Safes the get string. - /// - /// The doc. - /// The path. - /// The default value. - /// System.String. - public static string SafeGetString(this XmlNode doc, string path, string defaultValue) - { - var rvalNode = doc.SelectSingleNode(path); - if (rvalNode != null) - { - var text = rvalNode.InnerText; - - return !string.IsNullOrWhiteSpace(text) ? text : defaultValue; - } - return defaultValue; - } - - /// - /// Reads the string safe. - /// - /// The reader. - /// System.String. - public static string ReadStringSafe(this XmlReader reader) - { - var val = reader.ReadElementContentAsString(); - - return string.IsNullOrWhiteSpace(val) ? null : val; - } - - /// - /// Reads the value safe. - /// - /// The reader. - /// System.String. - public static string ReadValueSafe(this XmlReader reader) - { - reader.Read(); - - var val = reader.Value; - - return string.IsNullOrWhiteSpace(val) ? null : val; - } - - /// - /// Reads a float from the current element of an XmlReader - /// - /// The reader. - /// System.Single. - public static float ReadFloatSafe(this XmlReader reader) - { - string valueString = reader.ReadElementContentAsString(); - - float value = 0; - - if (!string.IsNullOrWhiteSpace(valueString)) - { - // float.TryParse is local aware, so it can be probamatic, force us culture - float.TryParse(valueString, NumberStyles.AllowDecimalPoint, _usCulture, out value); - } - - return value; - } - - /// - /// Reads an int from the current element of an XmlReader - /// - /// The reader. - /// System.Int32. - public static int ReadIntSafe(this XmlReader reader) - { - string valueString = reader.ReadElementContentAsString(); - - int value = 0; - - if (!string.IsNullOrWhiteSpace(valueString)) - { - int.TryParse(valueString, out value); - } - - return value; - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 3690a1b80e..9d664b3708 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -47,8 +47,6 @@ - - diff --git a/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs b/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs index e34c41238b..ea4ef5e219 100644 --- a/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs @@ -7,16 +7,15 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; -using MediaBrowser.Providers.Extensions; using System; -using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml; -using System.Xml.Linq; namespace MediaBrowser.Providers.TV { @@ -167,18 +166,18 @@ namespace MediaBrowser.Providers.TV if (!string.IsNullOrEmpty(seriesId)) { - var seriesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"); - - var seriesXmlFileInfo = new FileInfo(seriesXmlPath); + var seriesDataPath = RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, + seriesId); var status = ProviderRefreshStatus.Success; - - if (seriesXmlFileInfo.Exists) + + try { - var xmlDoc = new XmlDocument(); - xmlDoc.Load(seriesXmlPath); - - status = await FetchEpisodeData(xmlDoc, episode, seriesId, cancellationToken).ConfigureAwait(false); + status = await FetchEpisodeData(episode, seriesDataPath, cancellationToken).ConfigureAwait(false); + } + catch (FileNotFoundException) + { + // Don't fail the provider because this will just keep on going and going. } BaseProviderInfo data; @@ -200,12 +199,11 @@ namespace MediaBrowser.Providers.TV /// /// Fetches the episode data. /// - /// The series XML. /// The episode. - /// The series id. + /// The series data path. /// The cancellation token. /// Task{System.Boolean}. - private async Task FetchEpisodeData(XmlDocument seriesXml, Episode episode, string seriesId, CancellationToken cancellationToken) + private async Task FetchEpisodeData(Episode episode, string seriesDataPath, CancellationToken cancellationToken) { var status = ProviderRefreshStatus.Success; @@ -214,6 +212,7 @@ namespace MediaBrowser.Providers.TV return status; } + var episodeNumber = episode.IndexNumber.Value; var seasonNumber = episode.ParentIndexNumber ?? TVUtils.GetSeasonNumberFromEpisodeFile(episode.Path); if (seasonNumber == null) @@ -221,178 +220,416 @@ namespace MediaBrowser.Providers.TV return status; } + var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); + var success = false; var usingAbsoluteData = false; - var episodeNode = seriesXml.SelectSingleNode("//Episode[EpisodeNumber='" + episode.IndexNumber.Value + "'][SeasonNumber='" + seasonNumber.Value + "']"); + try + { + status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); - if (episodeNode == null) + success = true; + } + catch (FileNotFoundException) { - if (seasonNumber.Value == 1) + // Could be using absolute numbering + if (seasonNumber.Value != 1) { - episodeNode = seriesXml.SelectSingleNode("//Episode[absolute_number='" + episode.IndexNumber.Value + "']"); - usingAbsoluteData = true; + throw; } } - // If still null, nothing we can do - if (episodeNode == null) + if (!success) { - return status; + file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); + + status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); + usingAbsoluteData = true; } - IEnumerable extraEpisodesNode = new XmlDocument[] { }; - if (episode.IndexNumberEnd.HasValue) + var end = episode.IndexNumberEnd ?? episodeNumber; + episodeNumber++; + + while (episodeNumber <= end) { - var seriesXDocument = XDocument.Load(new XmlNodeReader(seriesXml)); if (usingAbsoluteData) { - extraEpisodesNode = - seriesXDocument.Descendants("Episode") - .Where( - x => - int.Parse(x.Element("absolute_number").Value) > episode.IndexNumber && - int.Parse(x.Element("absolute_number").Value) <= episode.IndexNumberEnd.Value).OrderBy(x => x.Element("absolute_number").Value).Select(x => x.ToXmlDocument()); + file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); } else { - var all = - seriesXDocument.Descendants("Episode").Where(x => int.Parse(x.Element("SeasonNumber").Value) == seasonNumber.Value); + file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); + } - var xElements = all.Where(x => int.Parse(x.Element("EpisodeNumber").Value) > episode.IndexNumber && int.Parse(x.Element("EpisodeNumber").Value) <= episode.IndexNumberEnd.Value); - extraEpisodesNode = xElements.OrderBy(x => x.Element("EpisodeNumber").Value).Select(x => x.ToXmlDocument()); + try + { + FetchAdditionalPartInfo(episode, file, cancellationToken); + } + catch (FileNotFoundException) + { + break; } + episodeNumber++; } - var doc = new XmlDocument(); - doc.LoadXml(episodeNode.OuterXml); - if (!episode.HasImage(ImageType.Primary)) + return status; + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private async Task FetchMainEpisodeInfo(Episode item, string xmlFile, CancellationToken cancellationToken) + { + var status = ProviderRefreshStatus.Success; + + using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8)) { - var p = doc.SafeGetString("//filename"); - if (p != null) + if (!item.LockedFields.Contains(MetadataFields.Cast)) { - try - { - var url = TVUtils.BannerUrl + p; + item.People.Clear(); + } - await _providerManager.SaveImage(episode, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) - .ConfigureAwait(false); - } - catch (HttpException) + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + })) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) { - status = ProviderRefreshStatus.CompletedWithErrors; + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "id": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.Tvdb, val); + } + break; + } + + case "IMDB_ID": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.SetProviderId(MetadataProviders.Imdb, val); + } + break; + } + + case "EpisodeName": + { + if (!item.LockedFields.Contains(MetadataFields.Name)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Name = val; + } + } + break; + } + + case "Language": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Language = val; + } + break; + } + + case "filename": + { + if (string.IsNullOrEmpty(item.PrimaryImagePath)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + try + { + var url = TVUtils.BannerUrl + val; + + await _providerManager.SaveImage(item, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); + } + catch (HttpException) + { + status = ProviderRefreshStatus.CompletedWithErrors; + } + } + } + break; + } + + case "Overview": + { + if (!item.LockedFields.Contains(MetadataFields.Overview)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Overview = val; + } + } + break; + } + case "Rating": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + 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 "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 "Director": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(item, val, PersonType.Director); + } + } + + break; + } + case "GuestStars": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddGuestStars(item, val); + } + } + + break; + } + case "Writer": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(item, val, PersonType.Writer); + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } } } } - if (!episode.LockedFields.Contains(MetadataFields.Overview)) - { - var extraOverview = extraEpisodesNode.Aggregate("", (current, xmlDocument) => current + ("\r\n\r\n" + xmlDocument.SafeGetString("//Overview"))); - episode.Overview = doc.SafeGetString("//Overview") + extraOverview; - } - if (usingAbsoluteData) - episode.IndexNumber = doc.SafeGetInt32("//absolute_number", -1); - if (episode.IndexNumber < 0) - episode.IndexNumber = doc.SafeGetInt32("//EpisodeNumber"); - if (!episode.LockedFields.Contains(MetadataFields.Name)) - { - var extraNames = extraEpisodesNode.Aggregate("", (current, xmlDocument) => current + (", " + xmlDocument.SafeGetString("//EpisodeName"))); - episode.Name = doc.SafeGetString("//EpisodeName") + extraNames; - } - episode.CommunityRating = doc.SafeGetSingle("//Rating", -1, 10); - var firstAired = doc.SafeGetString("//FirstAired"); - DateTime airDate; - if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850) + + return status; + } + + private void AddPeople(BaseItem item, string val, string personType) + { + // Sometimes tvdb actors have leading spaces + foreach (var person in val.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Select(str => new PersonInfo { Type = personType, Name = str.Trim() })) { - episode.PremiereDate = airDate.ToUniversalTime(); - episode.ProductionYear = airDate.Year; + item.AddPerson(person); } + } - var imdbId = doc.SafeGetString("//IMDB_ID"); - if (!string.IsNullOrEmpty(imdbId)) + private void AddGuestStars(BaseItem item, string val) + { + // Sometimes tvdb actors have leading spaces + //Regex Info: + //The first block are the posible delimitators (open-parentheses should be there cause if dont the next block will fail) + //The second block Allow the delimitators to be part of the text if they're inside parentheses + var persons = Regex.Matches(val, @"(?([^|,(])|(?\([^)]*\)*))+") + .Cast() + .Select(m => m.Value) + .Where(i => !string.IsNullOrWhiteSpace(i) && !string.IsNullOrEmpty(i)); + + foreach (var person in persons.Select(str => { - episode.SetProviderId(MetadataProviders.Imdb, imdbId); + var nameGroup = str.Split(new[] { '(' }, 2, StringSplitOptions.RemoveEmptyEntries); + var name = nameGroup[0].Trim(); + var roles = nameGroup.Count() > 1 ? nameGroup[1].Trim() : null; + if (roles != null) + roles = roles.EndsWith(")") ? roles.Substring(0, roles.Length - 1) : roles; + return new PersonInfo { Type = PersonType.GuestStar, Name = name, Role = roles }; + })) + { + item.AddPerson(person); } + } - if (!episode.LockedFields.Contains(MetadataFields.Cast)) + private void FetchAdditionalPartInfo(Episode item, string xmlFile, CancellationToken cancellationToken) + { + using (var streamReader = new StreamReader(xmlFile, Encoding.UTF8)) { - episode.People.Clear(); - - var actors = doc.SafeGetString("//GuestStars"); - if (actors != null) + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, new XmlReaderSettings { - // Sometimes tvdb actors have leading spaces - //Regex Info: - //The first block are the posible delimitators (open-parentheses should be there cause if dont the next block will fail) - //The second block Allow the delimitators to be part of the text if they're inside parentheses - var persons = Regex.Matches(actors, @"(?([^|,(])|(?\([^)]*\)*))+") - .Cast() - .Select(m => m.Value) - .Where(i => !string.IsNullOrWhiteSpace(i) && !string.IsNullOrEmpty(i)); - - foreach (var person in persons.Select(str => - { - var nameGroup = str.Split(new[] { '(' }, 2, StringSplitOptions.RemoveEmptyEntries); - var name = nameGroup[0].Trim(); - var roles = nameGroup.Count() > 1 ? nameGroup[1].Trim() : null; - if (roles != null) - roles = roles.EndsWith(")") ? roles.Substring(0, roles.Length - 1) : roles; - return new PersonInfo { Type = PersonType.GuestStar, Name = name, Role = roles }; - })) - { - episode.AddPerson(person); - } - } - foreach (var xmlDocument in extraEpisodesNode) + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + })) { - var extraActors = xmlDocument.SafeGetString("//GuestStars"); - if (extraActors == null) continue; - // Sometimes tvdb actors have leading spaces - var persons = Regex.Matches(extraActors, @"(?([^|,(])|(?\([^)]*\)*))+") - .Cast() - .Select(m => m.Value) - .Where(i => !string.IsNullOrWhiteSpace(i) && !string.IsNullOrEmpty(i)); - - foreach (var person in persons.Select(str => - { - var nameGroup = str.Split(new[] { '(' }, 2, StringSplitOptions.RemoveEmptyEntries); - var name = nameGroup[0].Trim(); - var roles = nameGroup.Count() > 1 ? nameGroup[1].Trim() : null; - if (roles != null) - roles = roles.EndsWith(")") ? roles.Substring(0, roles.Length - 1) : roles; - return new PersonInfo { Type = PersonType.GuestStar, Name = name, Role = roles }; - })) - { - episode.AddPerson(person); - } - } + reader.MoveToContent(); - var directors = doc.SafeGetString("//Director"); - if (directors != null) - { - // Sometimes tvdb actors have leading spaces - foreach (var person in directors.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Select(str => new PersonInfo { Type = PersonType.Director, Name = str.Trim() })) + // Loop through each element + while (reader.Read()) { - episode.AddPerson(person); - } - } - - - var writers = doc.SafeGetString("//Writer"); - if (writers != null) - { - // Sometimes tvdb actors have leading spaces - foreach (var person in writers.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Select(str => new PersonInfo { Type = PersonType.Writer, Name = str.Trim() })) - { - episode.AddPerson(person); + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "EpisodeName": + { + if (!item.LockedFields.Contains(MetadataFields.Name)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Name += ", " + val; + } + } + break; + } + + case "Overview": + { + if (!item.LockedFields.Contains(MetadataFields.Overview)) + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.Overview += Environment.NewLine + Environment.NewLine + val; + } + } + break; + } + case "Director": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(item, val, PersonType.Director); + } + } + + break; + } + case "GuestStars": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddGuestStars(item, val); + } + } + + break; + } + case "Writer": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + if (!item.LockedFields.Contains(MetadataFields.Cast)) + { + AddPeople(item, val, PersonType.Writer); + } + } + + break; + } + + default: + reader.Skip(); + break; + } + } } } } - - return status; } /// diff --git a/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs b/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs index a921e2036a..e86e53e3b9 100644 --- a/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/RemoteSeriesProvider.cs @@ -190,7 +190,7 @@ namespace MediaBrowser.Providers.TV return true; } } - + return base.NeedsRefreshBasedOnCompareDate(item, providerInfo); } @@ -305,6 +305,8 @@ namespace MediaBrowser.Providers.TV { await SanitizeXmlFile(file).ConfigureAwait(false); } + + await ExtractEpisodes(seriesDataPath, Path.Combine(seriesDataPath, ConfigurationManager.Configuration.PreferredMetadataLanguage + ".xml")).ConfigureAwait(false); } /// @@ -362,6 +364,159 @@ namespace MediaBrowser.Providers.TV return sbOutput.ToString(); } + /// + /// 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 + /// + /// The series data path. + /// The XML file. + /// Task. + private async Task ExtractEpisodes(string seriesDataPath, string xmlFile) + { + 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).ConfigureAwait(false); + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + } + } + + private async Task SaveEpsiodeXml(string seriesDataPath, string xml) + { + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + var seasonNumber = -1; + var episodeNumber = -1; + var absoluteNumber = -1; + + 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 "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 file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber, episodeNumber)); + + 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)); + + using (var writer = XmlWriter.Create(file, new XmlWriterSettings + { + Encoding = Encoding.UTF8, + Async = true + })) + { + await writer.WriteRawAsync(xml).ConfigureAwait(false); + } + } + } + /// /// Gets the series data path. /// @@ -382,7 +537,7 @@ namespace MediaBrowser.Providers.TV /// System.String. internal static string GetSeriesDataPath(IApplicationPaths appPaths) { - var dataPath = Path.Combine(appPaths.DataPath, "tvdb-v2"); + var dataPath = Path.Combine(appPaths.DataPath, "tvdb-v3"); return dataPath; } @@ -540,6 +695,23 @@ namespace MediaBrowser.Providers.TV } 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": {