using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Providers.Movies { public class MovieUpdatesPreScanTask : ILibraryPostScanTask { /// /// The updates URL /// private const string UpdatesUrl = "http://api.themoviedb.org/3/movie/changes?start_date={0}&api_key={1}&page={2}"; /// /// The _HTTP client /// private readonly IHttpClient _httpClient; /// /// The _logger /// private readonly ILogger _logger; /// /// The _config /// private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _json; private readonly IFileSystem _fileSystem; /// /// Initializes a new instance of the class. /// /// The logger. /// The HTTP client. /// The config. /// The json. public MovieUpdatesPreScanTask(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IJsonSerializer json, IFileSystem fileSystem) { _logger = logger; _httpClient = httpClient; _config = config; _json = json; _fileSystem = fileSystem; } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// 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 innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => progress.Report(pct * .8)); await Run(innerProgress, false, cancellationToken).ConfigureAwait(false); progress.Report(80); //innerProgress = new ActionableProgress(); //innerProgress.RegisterAction(pct => progress.Report(80 + pct * .2)); //await Run(innerProgress, true, cancellationToken).ConfigureAwait(false); progress.Report(100); } /// /// Runs the specified progress. /// /// The progress. /// if set to true [run for box sets]. /// The cancellation token. /// Task. private async Task Run(IProgress progress, bool runForBoxSets, CancellationToken cancellationToken) { var path = runForBoxSets ? MovieDbProvider.GetBoxSetsDataPath(_config.CommonApplicationPaths) : MovieDbProvider.GetMoviesDataPath(_config.CommonApplicationPaths); Directory.CreateDirectory(path); var timestampFile = Path.Combine(path, "time.txt"); var timestampFileInfo = new FileInfo(timestampFile); // Don't check for updates every single time if (timestampFileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(timestampFileInfo)).TotalDays < 7) { return; } // Find out the last time we queried tvdb for updates var lastUpdateTime = timestampFileInfo.Exists ? File.ReadAllText(timestampFile, Encoding.UTF8) : string.Empty; var existingDirectories = Directory.EnumerateDirectories(path).Select(Path.GetFileName).ToList(); if (!string.IsNullOrEmpty(lastUpdateTime)) { long lastUpdateTicks; if (long.TryParse(lastUpdateTime, NumberStyles.Any, UsCulture, out lastUpdateTicks)) { var lastUpdateDate = new DateTime(lastUpdateTicks, DateTimeKind.Utc); // They only allow up to 14 days of updates if ((DateTime.UtcNow - lastUpdateDate).TotalDays > 13) { lastUpdateDate = DateTime.UtcNow.AddDays(-13); } var updatedIds = await GetIdsToUpdate(lastUpdateDate, 1, cancellationToken).ConfigureAwait(false); var existingDictionary = existingDirectories.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); var idsToUpdate = updatedIds.Where(i => !string.IsNullOrWhiteSpace(i) && existingDictionary.ContainsKey(i)); await UpdateMovies(idsToUpdate, runForBoxSets, path, progress, cancellationToken).ConfigureAwait(false); } } File.WriteAllText(timestampFile, DateTime.UtcNow.Ticks.ToString(UsCulture), Encoding.UTF8); progress.Report(100); } /// /// Gets the ids to update. /// /// The last update time. /// The page. /// The cancellation token. /// Task{IEnumerable{System.String}}. private async Task> GetIdsToUpdate(DateTime lastUpdateTime, int page, CancellationToken cancellationToken) { bool hasMorePages; var list = new List(); // First get last time using (var stream = await _httpClient.Get(new HttpRequestOptions { Url = string.Format(UpdatesUrl, lastUpdateTime.ToString("yyyy-MM-dd"), MovieDbProvider.ApiKey, page), CancellationToken = cancellationToken, EnableHttpCompression = true, ResourcePool = MovieDbProvider.Current.MovieDbResourcePool, AcceptHeader = MovieDbProvider.AcceptHeader }).ConfigureAwait(false)) { var obj = _json.DeserializeFromStream(stream); var data = obj.results.Select(i => i.id.ToString(UsCulture)); list.AddRange(data); hasMorePages = page < obj.total_pages; } if (hasMorePages) { var more = await GetIdsToUpdate(lastUpdateTime, page + 1, cancellationToken).ConfigureAwait(false); list.AddRange(more); } return list; } /// /// Updates the movies. /// /// The ids. /// if set to true [is box set]. /// The movies data path. /// The progress. /// The cancellation token. /// Task. private async Task UpdateMovies(IEnumerable ids, bool isBoxSet, string moviesDataPath, IProgress progress, CancellationToken cancellationToken) { var list = ids.ToList(); var numComplete = 0; foreach (var id in list) { try { await UpdateMovie(id, isBoxSet, moviesDataPath, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error updating tmdb movie id {0}", ex, id); } numComplete++; double percent = numComplete; percent /= list.Count; percent *= 100; progress.Report(percent); } } /// /// Updates the movie. /// /// The id. /// if set to true [is box set]. /// The data path. /// The cancellation token. /// Task. private Task UpdateMovie(string id, bool isBoxSet, string dataPath, CancellationToken cancellationToken) { _logger.Info("Updating movie from tmdb " + id); var itemDataPath = Path.Combine(dataPath, id); Directory.CreateDirectory(dataPath); return MovieDbProvider.Current.DownloadMovieInfo(id, isBoxSet, itemDataPath, cancellationToken); } class Result { public int id { get; set; } public bool? adult { get; set; } } class RootObject { public List results { get; set; } public int page { get; set; } public int total_pages { get; set; } public int total_results { get; set; } public RootObject() { results = new List(); } } } }