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; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Movies; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.People { public class MovieDbPersonProvider : IRemoteMetadataProvider { const string DataFileName = "info.json"; internal static MovieDbPersonProvider Current { get; private set; } private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly IHttpClient _httpClient; private readonly ILogger _logger; public MovieDbPersonProvider(IFileSystem fileSystem, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger) { _fileSystem = fileSystem; _configurationManager = configurationManager; _jsonSerializer = jsonSerializer; _httpClient = httpClient; _logger = logger; Current = this; } public string Name => "TheMovieDb"; public async Task> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) { var tmdbId = searchInfo.GetProviderId(MetadataProviders.Tmdb); var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); if (!string.IsNullOrEmpty(tmdbId)) { await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false); var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId); var info = _jsonSerializer.DeserializeFromFile(dataFilePath); var images = (info.images ?? new Images()).profiles ?? new List(); var result = new RemoteSearchResult { Name = info.name, SearchProviderName = Name, ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].file_path) }; result.SetProviderId(MetadataProviders.Tmdb, info.id.ToString(_usCulture)); result.SetProviderId(MetadataProviders.Imdb, info.imdb_id); return new[] { result }; } if (searchInfo.IsAutomated) { // Don't hammer moviedb searching by name return new List(); } var url = string.Format(MovieDbProvider.BaseMovieDbUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), MovieDbProvider.ApiKey); using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, AcceptHeader = MovieDbProvider.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { var result = await _jsonSerializer.DeserializeFromStreamAsync(json).ConfigureAwait(false) ?? new PersonSearchResults(); return result.Results.Select(i => GetSearchResult(i, tmdbImageUrl)); } } } private RemoteSearchResult GetSearchResult(PersonSearchResult i, string baseImageUrl) { var result = new RemoteSearchResult { SearchProviderName = Name, Name = i.Name, ImageUrl = string.IsNullOrEmpty(i.Profile_Path) ? null : (baseImageUrl + i.Profile_Path) }; result.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture)); return result; } public async Task> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken) { var tmdbId = id.GetProviderId(MetadataProviders.Tmdb); // We don't already have an Id, need to fetch it if (string.IsNullOrEmpty(tmdbId)) { tmdbId = await GetTmdbId(id, cancellationToken).ConfigureAwait(false); } var result = new MetadataResult(); if (!string.IsNullOrEmpty(tmdbId)) { try { await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) { return result; } throw; } var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId); var info = _jsonSerializer.DeserializeFromFile(dataFilePath); var item = new Person(); result.HasMetadata = true; // Take name from incoming info, don't rename the person // TODO: This should go in PersonMetadataService, not each person provider item.Name = id.Name; //item.HomePageUrl = info.homepage; if (!string.IsNullOrWhiteSpace(info.place_of_birth)) { item.ProductionLocations = new string[] { info.place_of_birth }; } item.Overview = info.biography; if (DateTime.TryParseExact(info.birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date)) { item.PremiereDate = date.ToUniversalTime(); } if (DateTime.TryParseExact(info.deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date)) { item.EndDate = date.ToUniversalTime(); } item.SetProviderId(MetadataProviders.Tmdb, info.id.ToString(_usCulture)); if (!string.IsNullOrEmpty(info.imdb_id)) { item.SetProviderId(MetadataProviders.Imdb, info.imdb_id); } result.HasMetadata = true; result.Item = item; } return result; } private readonly CultureInfo _usCulture = new CultureInfo("en-US"); /// /// Gets the TMDB id. /// /// The information. /// The cancellation token. /// Task{System.String}. private async Task GetTmdbId(PersonLookupInfo info, CancellationToken cancellationToken) { var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); return results.Select(i => i.GetProviderId(MetadataProviders.Tmdb)).FirstOrDefault(); } internal async Task EnsurePersonInfo(string id, CancellationToken cancellationToken) { var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, id); var fileInfo = _fileSystem.GetFileSystemInfo(dataFilePath); if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) { return; } var url = string.Format(MovieDbProvider.BaseMovieDbUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", MovieDbProvider.ApiKey, id); using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, AcceptHeader = MovieDbProvider.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); using (var fs = _fileSystem.GetFileStream(dataFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { await json.CopyToAsync(fs).ConfigureAwait(false); } } } } private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId) { var letter = tmdbId.GetMD5().ToString().Substring(0, 1); return Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId); } internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId) { return Path.Combine(GetPersonDataPath(appPaths, tmdbId), DataFileName); } private static string GetPersonsDataPath(IApplicationPaths appPaths) { return Path.Combine(appPaths.CachePath, "tmdb-people"); } #region Result Objects /// /// Class PersonSearchResult /// public 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 /// public 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; } } public 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; } } public 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; } } public class Credits { public List cast { get; set; } public List crew { get; set; } } public 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; } } public class Images { public List profiles { get; set; } } public class ExternalIds { public string imdb_id { get; set; } public string freebase_mid { get; set; } public string freebase_id { get; set; } public int tvrage_id { get; set; } } public 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; } public ExternalIds external_ids { get; set; } } #endregion public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClient.GetResponse(new HttpRequestOptions { CancellationToken = cancellationToken, Url = url }); } } }