using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; 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.Net; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Providers.Movies { /// /// Class TmdbPersonProvider /// public class TmdbPersonProvider : BaseMetadataProvider { protected readonly IProviderManager ProviderManager; internal static TmdbPersonProvider Current { get; private set; } const string DataFileName = "info.json"; private readonly IFileSystem _fileSystem; public TmdbPersonProvider(IJsonSerializer jsonSerializer, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem) : base(logManager, configurationManager) { if (jsonSerializer == null) { throw new ArgumentNullException("jsonSerializer"); } JsonSerializer = jsonSerializer; ProviderManager = providerManager; _fileSystem = fileSystem; Current = this; } /// /// Gets the json serializer. /// /// The json serializer. protected IJsonSerializer JsonSerializer { get; private set; } /// /// Supportses the specified item. /// /// The item. /// true if XXXX, false otherwise public override bool Supports(BaseItem item) { return item is Person; } protected override bool RefreshOnVersionChange { get { return true; } } protected override string ProviderVersion { get { return "3"; } } public override ItemUpdateType ItemUpdateType { get { return ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataDownload; } } protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) { if (HasAltMeta(item) && !ConfigurationManager.Configuration.EnableTmdbUpdates) return false; return base.NeedsRefreshInternal(item, providerInfo); } protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) { var provderId = item.GetProviderId(MetadataProviders.Tmdb); if (!string.IsNullOrEmpty(provderId)) { // Process images var path = GetPersonDataPath(ConfigurationManager.ApplicationPaths, provderId); var file = Path.Combine(path, DataFileName); var fileInfo = new FileInfo(file); if (fileInfo.Exists) { return _fileSystem.GetLastWriteTimeUtc(fileInfo) > providerInfo.LastRefreshed; } return true; } return base.NeedsRefreshBasedOnCompareDate(item, providerInfo); } internal static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId) { var letter = tmdbId.GetMD5().ToString().Substring(0, 1); var seriesDataPath = Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId); return seriesDataPath; } internal static string GetPersonsDataPath(IApplicationPaths appPaths) { var dataPath = Path.Combine(appPaths.DataPath, "tmdb-people"); return dataPath; } private bool HasAltMeta(BaseItem item) { return item.LocationType == LocationType.FileSystem && item.ResolveArgs.ContainsMetaFileByName("person.xml"); } /// /// 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 person = (Person)item; var id = person.GetProviderId(MetadataProviders.Tmdb); // We don't already have an Id, need to fetch it if (string.IsNullOrEmpty(id)) { id = await GetTmdbId(item, cancellationToken).ConfigureAwait(false); } cancellationToken.ThrowIfCancellationRequested(); if (!string.IsNullOrEmpty(id)) { await FetchInfo(person, id, force, cancellationToken).ConfigureAwait(false); } else { Logger.Debug("TmdbPersonProvider Unable to obtain id for " + item.Name); } SetLastRefreshed(item, DateTime.UtcNow); return true; } /// /// 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; } } private readonly CultureInfo _usCulture = new CultureInfo("en-US"); /// /// Gets the TMDB id. /// /// The person. /// The cancellation token. /// Task{System.String}. private async Task GetTmdbId(BaseItem person, CancellationToken cancellationToken) { string url = string.Format(@"http://api.themoviedb.org/3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(person.Name), MovieDbProvider.ApiKey); PersonSearchResults searchResult = null; using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, AcceptHeader = MovieDbProvider.AcceptHeader }).ConfigureAwait(false)) { searchResult = JsonSerializer.DeserializeFromStream(json); } return searchResult != null && searchResult.Total_Results > 0 ? searchResult.Results[0].Id.ToString(_usCulture) : null; } /// /// Fetches the info. /// /// The person. /// The id. /// if set to true [is forced refresh]. /// The cancellation token. /// Task. private async Task FetchInfo(Person person, string id, bool isForcedRefresh, CancellationToken cancellationToken) { var personDataPath = GetPersonDataPath(ConfigurationManager.ApplicationPaths, id); var file = Path.Combine(personDataPath, DataFileName); // Only download if not already there // The prescan task will take care of updates so we don't need to re-download here if (!File.Exists(file)) { await DownloadPersonInfo(id, cancellationToken).ConfigureAwait(false); } if (isForcedRefresh || ConfigurationManager.Configuration.EnableTmdbUpdates || !HasAltMeta(person)) { var info = JsonSerializer.DeserializeFromFile(Path.Combine(personDataPath, DataFileName)); cancellationToken.ThrowIfCancellationRequested(); ProcessInfo(person, info); Logger.Debug("TmdbPersonProvider downloaded and saved information for {0}", person.Name); await FetchImages(person, info.images, cancellationToken).ConfigureAwait(false); } } internal async Task DownloadPersonInfo(string id, CancellationToken cancellationToken) { var personDataPath = GetPersonDataPath(ConfigurationManager.ApplicationPaths, id); var url = string.Format(@"http://api.themoviedb.org/3/person/{1}?api_key={0}&append_to_response=credits,images", MovieDbProvider.ApiKey, id); using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, AcceptHeader = MovieDbProvider.AcceptHeader }).ConfigureAwait(false)) { Directory.CreateDirectory(personDataPath); using (var fs = _fileSystem.GetFileStream(Path.Combine(personDataPath, DataFileName), FileMode.Create, FileAccess.Write, FileShare.Read, true)) { await json.CopyToAsync(fs).ConfigureAwait(false); } } } /// /// Processes the info. /// /// The person. /// The search result. protected void ProcessInfo(Person person, PersonResult searchResult) { if (!person.LockedFields.Contains(MetadataFields.Overview)) { person.Overview = searchResult.biography; } DateTime date; if (DateTime.TryParseExact(searchResult.birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date)) { person.PremiereDate = date.ToUniversalTime(); } if (DateTime.TryParseExact(searchResult.deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date)) { person.EndDate = date.ToUniversalTime(); } if (!string.IsNullOrEmpty(searchResult.homepage)) { person.HomePageUrl = searchResult.homepage; } if (!person.LockedFields.Contains(MetadataFields.ProductionLocations)) { if (!string.IsNullOrEmpty(searchResult.place_of_birth)) { person.ProductionLocations = new List { searchResult.place_of_birth }; } } person.SetProviderId(MetadataProviders.Tmdb, searchResult.id.ToString(_usCulture)); } /// /// Fetches the images. /// /// The person. /// The search result. /// The cancellation token. /// Task. private async Task FetchImages(Person person, Images searchResult, CancellationToken cancellationToken) { if (searchResult != null && searchResult.profiles.Count > 0) { //get our language var profile = searchResult.profiles.FirstOrDefault( p => !string.IsNullOrEmpty(GetIso639(p)) && GetIso639(p).Equals(ConfigurationManager.Configuration.PreferredMetadataLanguage, StringComparison.OrdinalIgnoreCase)); if (profile == null) { //didn't find our language - try first null one profile = searchResult.profiles.FirstOrDefault( p => !string.IsNullOrEmpty(GetIso639(p)) && GetIso639(p).Equals(ConfigurationManager.Configuration.PreferredMetadataLanguage, StringComparison.OrdinalIgnoreCase)); } if (profile == null) { //still nothing - just get first one profile = searchResult.profiles[0]; } if (profile != null && !person.HasImage(ImageType.Primary)) { var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); await DownloadAndSaveImage(person, tmdbSettings.images.base_url + "original" + profile.file_path, MimeTypes.GetMimeType(profile.file_path), cancellationToken).ConfigureAwait(false); } } } private string GetIso639(Profile p) { return p.iso_639_1 == null ? string.Empty : p.iso_639_1.ToString(); } /// /// Downloads the and save image. /// /// The item. /// The source. /// Type of the MIME. /// The cancellation token. /// Task{System.String}. private async Task DownloadAndSaveImage(BaseItem item, string source, string mimeType, CancellationToken cancellationToken) { if (source == null) return; using (var sourceStream = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = source, CancellationToken = cancellationToken }).ConfigureAwait(false)) { await ProviderManager.SaveImage(item, sourceStream, mimeType, ImageType.Primary, null, source, cancellationToken) .ConfigureAwait(false); Logger.Debug("TmdbPersonProvider downloaded and saved image for {0}", item.Name); } } #region Result Objects /// /// Class PersonSearchResult /// protected class PersonSearchResult { /// /// Gets or sets a value indicating whether this is adult. /// /// true if adult; otherwise, false. public bool Adult { get; set; } /// /// Gets or sets the id. /// /// The id. public int Id { get; set; } /// /// Gets or sets the name. /// /// The name. public string Name { get; set; } /// /// Gets or sets the profile_ path. /// /// The profile_ path. public string Profile_Path { get; set; } } /// /// Class PersonSearchResults /// protected class PersonSearchResults { /// /// Gets or sets the page. /// /// The page. public int Page { get; set; } /// /// Gets or sets the results. /// /// The results. public List Results { get; set; } /// /// Gets or sets the total_ pages. /// /// The total_ pages. public int Total_Pages { get; set; } /// /// Gets or sets the total_ results. /// /// The total_ results. public int Total_Results { get; set; } } protected class Cast { public int id { get; set; } public string title { get; set; } public string character { get; set; } public string original_title { get; set; } public string poster_path { get; set; } public string release_date { get; set; } public bool adult { get; set; } } protected class Crew { public int id { get; set; } public string title { get; set; } public string original_title { get; set; } public string department { get; set; } public string job { get; set; } public string poster_path { get; set; } public string release_date { get; set; } public bool adult { get; set; } } protected class Credits { public List cast { get; set; } public List crew { get; set; } } protected class Profile { public string file_path { get; set; } public int width { get; set; } public int height { get; set; } public object iso_639_1 { get; set; } public double aspect_ratio { get; set; } } protected class Images { public List profiles { get; set; } } protected class PersonResult { public bool adult { get; set; } public List also_known_as { get; set; } public string biography { get; set; } public string birthday { get; set; } public string deathday { get; set; } public string homepage { get; set; } public int id { get; set; } public string imdb_id { get; set; } public string name { get; set; } public string place_of_birth { get; set; } public double popularity { get; set; } public string profile_path { get; set; } public Credits credits { get; set; } public Images images { get; set; } } #endregion } }