2
0
Эх сурвалжийг харах

change remote episode provider to xml reader

Luke Pulverenti 11 жил өмнө
parent
commit
a906b7358c

+ 8 - 1
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<TaskResult>(GetHistoryFilePath(false));
+                        return JsonSerializer.DeserializeFromFile<TaskResult>(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;

+ 0 - 1
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;
 

+ 0 - 18
MediaBrowser.Providers/Extensions/XDocumentExtensions.cs

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

+ 0 - 238
MediaBrowser.Providers/Extensions/XmlExtensions.cs

@@ -1,238 +0,0 @@
-using System;
-using System.Globalization;
-using System.Xml;
-
-namespace MediaBrowser.Providers.Extensions
-{
-    /// <summary>
-    /// Class XmlExtensions
-    /// </summary>
-    public static class XmlExtensions
-    {
-
-        /// <summary>
-        /// Safes the get int32.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <returns>System.Int32.</returns>
-        public static int SafeGetInt32(this XmlDocument doc, string path)
-        {
-            return SafeGetInt32(doc, path, 0);
-        }
-
-        /// <summary>
-        /// Safes the get int32.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <param name="defaultInt">The default int.</param>
-        /// <returns>System.Int32.</returns>
-        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;
-        }
-
-        /// <summary>
-        /// The _us culture
-        /// </summary>
-        private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
-        /// <summary>
-        /// Safes the get single.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <param name="minValue">The min value.</param>
-        /// <param name="maxValue">The max value.</param>
-        /// <returns>System.Single.</returns>
-        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;
-        }
-
-
-        /// <summary>
-        /// Safes the get string.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <returns>System.String.</returns>
-        public static string SafeGetString(this XmlDocument doc, string path)
-        {
-            return SafeGetString(doc, path, null);
-        }
-
-        /// <summary>
-        /// Safes the get string.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <param name="defaultString">The default string.</param>
-        /// <returns>System.String.</returns>
-        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;
-        }
-
-        /// <summary>
-        /// Safes the get DateTime.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <returns>System.DateTime.</returns>
-        public static DateTime? SafeGetDateTime(this XmlDocument doc, string path)
-        {
-            return SafeGetDateTime(doc, path, null);
-        }
-
-        /// <summary>
-        /// Safes the get DateTime.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <param name="defaultDate">The default date.</param>
-        /// <returns>System.DateTime.</returns>
-        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;
-        }
-
-        /// <summary>
-        /// Safes the get string.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <returns>System.String.</returns>
-        public static string SafeGetString(this XmlNode doc, string path)
-        {
-            return SafeGetString(doc, path, null);
-        }
-
-        /// <summary>
-        /// Safes the get string.
-        /// </summary>
-        /// <param name="doc">The doc.</param>
-        /// <param name="path">The path.</param>
-        /// <param name="defaultValue">The default value.</param>
-        /// <returns>System.String.</returns>
-        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;
-        }
-
-        /// <summary>
-        /// Reads the string safe.
-        /// </summary>
-        /// <param name="reader">The reader.</param>
-        /// <returns>System.String.</returns>
-        public static string ReadStringSafe(this XmlReader reader)
-        {
-            var val = reader.ReadElementContentAsString();
-
-            return string.IsNullOrWhiteSpace(val) ? null : val;
-        }
-
-        /// <summary>
-        /// Reads the value safe.
-        /// </summary>
-        /// <param name="reader">The reader.</param>
-        /// <returns>System.String.</returns>
-        public static string ReadValueSafe(this XmlReader reader)
-        {
-            reader.Read();
-
-            var val = reader.Value;
-
-            return string.IsNullOrWhiteSpace(val) ? null : val;
-        }
-
-        /// <summary>
-        /// Reads a float from the current element of an XmlReader
-        /// </summary>
-        /// <param name="reader">The reader.</param>
-        /// <returns>System.Single.</returns>
-        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;
-        }
-
-        /// <summary>
-        /// Reads an int from the current element of an XmlReader
-        /// </summary>
-        /// <param name="reader">The reader.</param>
-        /// <returns>System.Int32.</returns>
-        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;
-        }
-    }
-}

+ 0 - 2
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -47,8 +47,6 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="Extensions\XDocumentExtensions.cs" />
-    <Compile Include="Extensions\XmlExtensions.cs" />
     <Compile Include="FanartBaseProvider.cs" />
     <Compile Include="FolderProviderFromXml.cs" />
     <Compile Include="Games\GameProviderFromXml.cs" />

+ 381 - 144
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
         /// <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="seriesDataPath">The series data path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{System.Boolean}.</returns>
-        private async Task<ProviderRefreshStatus> FetchEpisodeData(XmlDocument seriesXml, Episode episode, string seriesId, CancellationToken cancellationToken)
+        private async Task<ProviderRefreshStatus> 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<XmlDocument> 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<ProviderRefreshStatus> 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, @"(?<delimitators>([^|,(])|(?<ignoreinParentheses>\([^)]*\)*))+")
+                .Cast<Match>()
+                .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, @"(?<delimitators>([^|,(])|(?<ignoreinParentheses>\([^)]*\)*))+")
-                        .Cast<Match>()
-                        .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, @"(?<delimitators>([^|,(])|(?<ignoreinParentheses>\([^)]*\)*))+")
-                        .Cast<Match>()
-                        .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;
         }
 
         /// <summary>

+ 174 - 2
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);
         }
 
         /// <summary>
@@ -362,6 +364,159 @@ namespace MediaBrowser.Providers.TV
             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>
+        /// <returns>Task.</returns>
+        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);
+                }
+            }
+        }
+
         /// <summary>
         /// Gets the series data path.
         /// </summary>
@@ -382,7 +537,7 @@ namespace MediaBrowser.Providers.TV
         /// <returns>System.String.</returns>
         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":
                             {