using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using System; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml; namespace MediaBrowser.Controller.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; } /// /// The episode query /// private const string EpisodeQuery = "http://www.thetvdb.com/api/{0}/series/{1}/default/{2}/{3}/{4}.xml"; /// /// The abs episode query /// private const string AbsEpisodeQuery = "http://www.thetvdb.com/api/{0}/series/{1}/absolute/{2}/{3}.xml"; /// /// Supportses the specified item. /// /// The item. /// true if XXXX, false otherwise public override bool Supports(BaseItem item) { return item is Episode; } /// /// Gets the priority. /// /// The priority. public override MetadataProviderPriority Priority { get { return MetadataProviderPriority.Second; } } /// /// Gets a value indicating whether [requires internet]. /// /// true if [requires internet]; otherwise, false. public override bool RequiresInternet { get { return true; } } /// /// Returns true or false indicating if the provider should refresh when the contents of it's directory changes /// /// true if [refresh on file system stamp change]; otherwise, false. protected override bool RefreshOnFileSystemStampChange { 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) { if (HasLocalMeta(item)) { return false; } if (GetComparisonData(item) != providerInfo.Data) { return true; } return base.NeedsRefreshInternal(item, providerInfo); } /// /// Gets the comparison data. /// /// The item. /// Guid. private Guid GetComparisonData(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); return GetComparisonData(seriesXmlFileInfo); } return Guid.Empty; } /// /// Gets the comparison data. /// /// The series XML file info. /// Guid. private Guid GetComparisonData(FileInfo seriesXmlFileInfo) { var date = seriesXmlFileInfo.Exists ? seriesXmlFileInfo.LastWriteTimeUtc : DateTime.MinValue; var key = date.Ticks + seriesXmlFileInfo.FullName; return key.GetMD5(); } /// /// 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) { if (HasLocalMeta(item)) { return false; } 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; } data.Data = GetComparisonData(seriesXmlFileInfo); 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; } var doc = new XmlDocument(); doc.LoadXml(episodeNode.OuterXml); if (!episode.HasImage(ImageType.Primary)) { var p = doc.SafeGetString("//filename"); if (p != null) { if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation); try { episode.PrimaryImagePath = await _providerManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), ConfigurationManager.Configuration.SaveLocalMeta, RemoteSeriesProvider.Current.TvDbResourcePool, cancellationToken); } catch (HttpException) { status = ProviderRefreshStatus.CompletedWithErrors; } } } episode.Overview = doc.SafeGetString("//Overview"); if (usingAbsoluteData) episode.IndexNumber = doc.SafeGetInt32("//absolute_number", -1); if (episode.IndexNumber < 0) episode.IndexNumber = doc.SafeGetInt32("//EpisodeNumber"); episode.Name = doc.SafeGetString("//EpisodeName"); 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; } episode.People.Clear(); var actors = doc.SafeGetString("//GuestStars"); if (actors != null) { foreach (var person in actors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) .Select(str => new PersonInfo { Type = PersonType.GuestStar, Name = str })) { episode.AddPerson(person); } } var directors = doc.SafeGetString("//Director"); if (directors != null) { foreach (var person in directors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) .Select(str => new PersonInfo { Type = PersonType.Director, Name = str })) { episode.AddPerson(person); } } var writers = doc.SafeGetString("//Writer"); if (writers != null) { foreach (var person in writers.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .Where(i => !string.IsNullOrWhiteSpace(i)) .Select(str => new PersonInfo { Type = PersonType.Writer, Name = str })) { episode.AddPerson(person); } } if (ConfigurationManager.Configuration.SaveLocalMeta) { if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation); var ms = new MemoryStream(); doc.Save(ms); await _providerManager.SaveToLibraryFilesystem(episode, Path.Combine(episode.MetaLocation, Path.GetFileNameWithoutExtension(episode.Path) + ".xml"), ms, cancellationToken).ConfigureAwait(false); } 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")); } } }