using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using MediaBrowser.Providers.Extensions; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; namespace MediaBrowser.Providers.TV { /// /// Class RemoteEpisodeProvider /// class RemoteEpisodeProvider : BaseMetadataProvider { /// /// The _provider manager /// private readonly IProviderManager _providerManager; /// /// Gets the HTTP client. /// /// The HTTP client. protected IHttpClient HttpClient { get; private set; } /// /// Initializes a new instance of the class. /// /// The HTTP client. /// The log manager. /// The configuration manager. /// The provider manager. public RemoteEpisodeProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager) : base(logManager, configurationManager) { HttpClient = httpClient; _providerManager = providerManager; } /// /// Supportses the specified item. /// /// The item. /// true if XXXX, false otherwise public override bool Supports(BaseItem item) { return item is Episode; } public override ItemUpdateType ItemUpdateType { get { return ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataDownload; } } /// /// Gets the priority. /// /// The priority. public override MetadataProviderPriority Priority { get { return MetadataProviderPriority.Third; } } /// /// Gets a value indicating whether [requires internet]. /// /// true if [requires internet]; otherwise, false. public override bool RequiresInternet { get { return true; } } /// /// Gets a value indicating whether [refresh on version change]. /// /// true if [refresh on version change]; otherwise, false. protected override bool RefreshOnVersionChange { get { return true; } } /// /// Gets the provider version. /// /// The provider version. protected override string ProviderVersion { get { return "1"; } } /// /// Needses the refresh internal. /// /// The item. /// The provider info. /// true if XXXX, false otherwise protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) { // Don't proceed if there's local metadata if (HasLocalMeta(item) && !ConfigurationManager.Configuration.EnableTvDbUpdates) { return false; } return base.NeedsRefreshInternal(item, providerInfo); } protected override DateTime CompareDate(BaseItem item) { var episode = (Episode)item; var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; if (!string.IsNullOrEmpty(seriesId)) { // Process images var seriesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"); var seriesXmlFileInfo = new FileInfo(seriesXmlPath); if (seriesXmlFileInfo.Exists) { return seriesXmlFileInfo.LastWriteTimeUtc; } } return base.CompareDate(item); } /// /// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// /// The item. /// if set to true [force]. /// The cancellation token. /// Task{System.Boolean}. public override async Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var episode = (Episode)item; var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; if (!string.IsNullOrEmpty(seriesId)) { var seriesXmlPath = Path.Combine(RemoteSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId), ConfigurationManager.Configuration.PreferredMetadataLanguage.ToLower() + ".xml"); var seriesXmlFileInfo = new FileInfo(seriesXmlPath); var status = ProviderRefreshStatus.Success; if (seriesXmlFileInfo.Exists) { var xmlDoc = new XmlDocument(); xmlDoc.Load(seriesXmlPath); status = await FetchEpisodeData(xmlDoc, episode, seriesId, cancellationToken).ConfigureAwait(false); } BaseProviderInfo data; if (!item.ProviderData.TryGetValue(Id, out data)) { data = new BaseProviderInfo(); item.ProviderData[Id] = data; } SetLastRefreshed(item, DateTime.UtcNow, status); return true; } Logger.Info("Episode provider not fetching because series does not have a tvdb id: " + item.Path); return false; } /// /// Fetches the episode data. /// /// The series XML. /// The episode. /// The series id. /// The cancellation token. /// Task{System.Boolean}. private async Task FetchEpisodeData(XmlDocument seriesXml, Episode episode, string seriesId, CancellationToken cancellationToken) { var status = ProviderRefreshStatus.Success; if (episode.IndexNumber == null) { return status; } var seasonNumber = episode.ParentIndexNumber ?? TVUtils.GetSeasonNumberFromEpisodeFile(episode.Path); if (seasonNumber == null) { return status; } var usingAbsoluteData = false; var episodeNode = seriesXml.SelectSingleNode("//Episode[EpisodeNumber='" + episode.IndexNumber.Value + "'][SeasonNumber='" + seasonNumber.Value + "']"); if (episodeNode == null) { if (seasonNumber.Value == 1) { episodeNode = seriesXml.SelectSingleNode("//Episode[absolute_number='" + episode.IndexNumber.Value + "']"); usingAbsoluteData = true; } } // If still null, nothing we can do if (episodeNode == null) { return status; } IEnumerable extraEpisodesNode = new XmlDocument[] { }; if (episode.IndexNumberEnd.HasValue) { 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()); } else { var all = seriesXDocument.Descendants("Episode").Where(x => int.Parse(x.Element("SeasonNumber").Value) == seasonNumber.Value); 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()); } } var doc = new XmlDocument(); doc.LoadXml(episodeNode.OuterXml); if (!episode.HasImage(ImageType.Primary)) { var p = doc.SafeGetString("//filename"); if (p != null) { try { var url = TVUtils.BannerUrl + p; await _providerManager.SaveImage(episode, url, RemoteSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken) .ConfigureAwait(false); } catch (HttpException) { status = ProviderRefreshStatus.CompletedWithErrors; } } } 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) { episode.PremiereDate = airDate.ToUniversalTime(); episode.ProductionYear = airDate.Year; } if (!episode.LockedFields.Contains(MetadataFields.Cast)) { episode.People.Clear(); var actors = doc.SafeGetString("//GuestStars"); if (actors != null) { // 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) { 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); } } 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() })) { 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); } } } return status; } /// /// Determines whether [has local meta] [the specified episode]. /// /// The episode. /// true if [has local meta] [the specified episode]; otherwise, false. private bool HasLocalMeta(BaseItem episode) { return (episode.Parent.ResolveArgs.ContainsMetaFileByName(Path.GetFileNameWithoutExtension(episode.Path) + ".xml")); } } }