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.Plugins.Tmdb.Models.General; using MediaBrowser.Providers.Plugins.Tmdb.Models.People; using MediaBrowser.Providers.Plugins.Tmdb.Models.Search; using MediaBrowser.Providers.Plugins.Tmdb.Movies; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Plugins.Tmdb.People { public class TmdbPersonProvider : IRemoteMetadataProvider { const string DataFileName = "info.json"; internal static TmdbPersonProvider 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 TmdbPersonProvider( 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 => TmdbUtils.ProviderName; public async Task> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) { var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb); var tmdbSettings = await TmdbMovieProvider.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 PersonImages()).Profiles ?? new List(); var result = new RemoteSearchResult { Name = info.Name, SearchProviderName = Name, ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path) }; result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture)); result.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id); return new[] { result }; } if (searchInfo.IsAutomated) { // Don't hammer moviedb searching by name return new List(); } var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey); using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { var result = await _jsonSerializer.DeserializeFromStreamAsync>(json).ConfigureAwait(false) ?? new TmdbSearchResult(); 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(MetadataProvider.Tmdb, i.Id.ToString(_usCulture)); return result; } public async Task> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken) { var tmdbId = id.GetProviderId(MetadataProvider.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(MetadataProvider.Tmdb, info.Id.ToString(_usCulture)); if (!string.IsNullOrEmpty(info.Imdb_Id)) { item.SetProviderId(MetadataProvider.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(MetadataProvider.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(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id); using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); using (var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, 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"); } public Task GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClient.GetResponse(new HttpRequestOptions { CancellationToken = cancellationToken, Url = url }); } } }