using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; namespace MediaBrowser.Providers.TV { /// /// Class TvdbPrescanTask /// public class TvdbPrescanTask : ILibraryPrescanTask { /// /// The server time URL /// private const string ServerTimeUrl = "http://thetvdb.com/api/Updates.php?type=none"; /// /// The updates URL /// private const string UpdatesUrl = "http://thetvdb.com/api/Updates.php?type=all&time={0}"; /// /// The _HTTP client /// private readonly IHttpClient _httpClient; /// /// The _logger /// private readonly ILogger _logger; /// /// The _config /// private readonly IServerConfigurationManager _config; /// /// Initializes a new instance of the class. /// /// The logger. /// The HTTP client. /// The config. public TvdbPrescanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config) { _logger = logger; _httpClient = httpClient; _config = config; } /// /// Runs the specified progress. /// /// The progress. /// The cancellation token. /// Task. public async Task Run(IProgress progress, CancellationToken cancellationToken) { if (!_config.Configuration.EnableInternetProviders) { progress.Report(100); return; } var path = RemoteSeriesProvider.GetSeriesDataPath(_config.CommonApplicationPaths); Directory.CreateDirectory(path); var timestampFile = Path.Combine(path, "time.txt"); var timestampFileInfo = new FileInfo(timestampFile); // Don't check for tvdb updates anymore frequently than 24 hours if (timestampFileInfo.Exists && (DateTime.UtcNow - timestampFileInfo.LastWriteTimeUtc).TotalDays < 1) { return; } // Find out the last time we queried tvdb for updates var lastUpdateTime = timestampFileInfo.Exists ? File.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; string newUpdateTime; var existingDirectories = Directory.EnumerateDirectories(path).Select(Path.GetFileName).ToList(); // If this is our first time, update all series if (string.IsNullOrEmpty(lastUpdateTime)) { // First get tvdb server time using (var stream = await _httpClient.Get(new HttpRequestOptions { Url = ServerTimeUrl, CancellationToken = cancellationToken, EnableHttpCompression = true, ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool }).ConfigureAwait(false)) { newUpdateTime = GetUpdateTime(stream); } await UpdateSeries(existingDirectories, path, progress, cancellationToken).ConfigureAwait(false); } else { var seriesToUpdate = await GetSeriesIdsToUpdate(existingDirectories, lastUpdateTime, cancellationToken).ConfigureAwait(false); newUpdateTime = seriesToUpdate.Item2; await UpdateSeries(seriesToUpdate.Item1, path, progress, cancellationToken).ConfigureAwait(false); } File.WriteAllText(timestampFile, newUpdateTime, Encoding.UTF8); progress.Report(100); } /// /// Gets the update time. /// /// The response. /// System.String. private string GetUpdateTime(Stream response) { var settings = new XmlReaderSettings { CheckCharacters = false, IgnoreProcessingInstructions = true, IgnoreComments = true, ValidationType = ValidationType.None }; using (var streamReader = new StreamReader(response, 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 "Time": { return (reader.ReadElementContentAsString() ?? string.Empty).Trim(); } default: reader.Skip(); break; } } } } } return null; } /// /// Gets the series ids to update. /// /// The existing series ids. /// The last update time. /// The cancellation token. /// Task{IEnumerable{System.String}}. private async Task, string>> GetSeriesIdsToUpdate(IEnumerable existingSeriesIds, string lastUpdateTime, CancellationToken cancellationToken) { // First get last time using (var stream = await _httpClient.Get(new HttpRequestOptions { Url = string.Format(UpdatesUrl, lastUpdateTime), CancellationToken = cancellationToken, EnableHttpCompression = true, ResourcePool = RemoteSeriesProvider.Current.TvDbResourcePool }).ConfigureAwait(false)) { var data = GetUpdatedSeriesIdList(stream); var existingDictionary = existingSeriesIds.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); var seriesList = data.Item1 .Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); return new Tuple, string>(seriesList, data.Item2); } } private Tuple, string> GetUpdatedSeriesIdList(Stream stream) { string updateTime = null; var idList = new List(); var settings = new XmlReaderSettings { CheckCharacters = false, IgnoreProcessingInstructions = true, IgnoreComments = true, ValidationType = ValidationType.None }; using (var streamReader = new StreamReader(stream, 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 "Time": { updateTime = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); break; } case "Series": { var id = (reader.ReadElementContentAsString() ?? string.Empty).Trim(); idList.Add(id); break; } default: reader.Skip(); break; } } } } } return new Tuple, string>(idList, updateTime); } /// /// Updates the series. /// /// The series ids. /// The series data path. /// The progress. /// The cancellation token. /// Task. private async Task UpdateSeries(IEnumerable seriesIds, string seriesDataPath, IProgress progress, CancellationToken cancellationToken) { var list = seriesIds.ToList(); var numComplete = 0; foreach (var seriesId in list) { try { await UpdateSeries(seriesId, seriesDataPath, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { // Already logged at lower levels, but don't fail the whole operation, unless timed out // We have to fail this to make it run again otherwise new episode data could potentially be missing if (ex.IsTimedOut) { throw; } } numComplete++; double percent = numComplete; percent /= list.Count; percent *= 100; progress.Report(percent); } } /// /// Updates the series. /// /// The id. /// The series data path. /// The cancellation token. /// Task. private Task UpdateSeries(string id, string seriesDataPath, CancellationToken cancellationToken) { _logger.Info("Updating series " + id); seriesDataPath = Path.Combine(seriesDataPath, id); Directory.CreateDirectory(seriesDataPath); return RemoteSeriesProvider.Current.DownloadSeriesZip(id, seriesDataPath, cancellationToken); } } }