using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; 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; private readonly ILibraryManager _libraryManager; /// /// 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, ILibraryManager libraryManager) { _logger = logger; _httpClient = httpClient; _config = config; _json = json; _fileSystem = fileSystem; _libraryManager = libraryManager; } 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.EnableTmdbUpdates) { progress.Report(100); return; } var path = 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, 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. /// The progress. /// The cancellation token. /// Task. private async Task UpdateMovies(IEnumerable ids, IProgress progress, CancellationToken cancellationToken) { var list = ids.ToList(); var numComplete = 0; // Gather all movies into a lookup by tmdb id var allMovies = _libraryManager.RootFolder.RecursiveChildren .Where(i => i is Movie || i is Trailer) .Where(i => !string.IsNullOrEmpty(i.GetProviderId(MetadataProviders.Tmdb))) .ToLookup(i => i.GetProviderId(MetadataProviders.Tmdb)); foreach (var id in list) { // Find the preferred language(s) for the movie in the library var languages = allMovies[id] .Select(i => i.GetPreferredMetadataLanguage()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); foreach (var language in languages) { try { await UpdateMovie(id, language, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error updating tmdb movie id {0}, language {1}", ex, id, language); } } numComplete++; double percent = numComplete; percent /= list.Count; percent *= 100; progress.Report(percent); } } /// /// Updates the movie. /// /// The id. /// The preferred metadata language. /// The cancellation token. /// Task. private Task UpdateMovie(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) { _logger.Info("Updating movie from tmdb " + id + ", language " + preferredMetadataLanguage); return MovieDbProvider.Current.DownloadMovieInfo(id, preferredMetadataLanguage, 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(); } } } }