TmdbSeriesProvider.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. #pragma warning disable CS1591
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net.Http;
  8. using System.Net.Http.Headers;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using MediaBrowser.Common.Configuration;
  12. using MediaBrowser.Controller.Configuration;
  13. using MediaBrowser.Controller.Entities;
  14. using MediaBrowser.Controller.Entities.TV;
  15. using MediaBrowser.Controller.Library;
  16. using MediaBrowser.Controller.Providers;
  17. using MediaBrowser.Model.Entities;
  18. using MediaBrowser.Model.Globalization;
  19. using MediaBrowser.Model.IO;
  20. using MediaBrowser.Model.Providers;
  21. using MediaBrowser.Model.Serialization;
  22. using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
  23. using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
  24. using MediaBrowser.Providers.Plugins.Tmdb.Movies;
  25. using Microsoft.Extensions.Logging;
  26. namespace MediaBrowser.Providers.Plugins.Tmdb.TV
  27. {
  28. public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
  29. {
  30. private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
  31. private readonly IJsonSerializer _jsonSerializer;
  32. private readonly IFileSystem _fileSystem;
  33. private readonly IServerConfigurationManager _configurationManager;
  34. private readonly ILogger<TmdbSeriesProvider> _logger;
  35. private readonly ILocalizationManager _localization;
  36. private readonly IHttpClientFactory _httpClientFactory;
  37. private readonly ILibraryManager _libraryManager;
  38. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  39. internal static TmdbSeriesProvider Current { get; private set; }
  40. public TmdbSeriesProvider(
  41. IJsonSerializer jsonSerializer,
  42. IFileSystem fileSystem,
  43. IServerConfigurationManager configurationManager,
  44. ILogger<TmdbSeriesProvider> logger,
  45. ILocalizationManager localization,
  46. IHttpClientFactory httpClientFactory,
  47. ILibraryManager libraryManager)
  48. {
  49. _jsonSerializer = jsonSerializer;
  50. _fileSystem = fileSystem;
  51. _configurationManager = configurationManager;
  52. _logger = logger;
  53. _localization = localization;
  54. _httpClientFactory = httpClientFactory;
  55. _libraryManager = libraryManager;
  56. Current = this;
  57. }
  58. public string Name => TmdbUtils.ProviderName;
  59. public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
  60. {
  61. var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
  62. if (!string.IsNullOrEmpty(tmdbId))
  63. {
  64. cancellationToken.ThrowIfCancellationRequested();
  65. await EnsureSeriesInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
  66. var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage);
  67. var obj = _jsonSerializer.DeserializeFromFile<SeriesResult>(dataFilePath);
  68. var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
  69. var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
  70. var remoteResult = new RemoteSearchResult
  71. {
  72. Name = obj.Name,
  73. SearchProviderName = Name,
  74. ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path
  75. };
  76. remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
  77. remoteResult.SetProviderId(MetadataProvider.Imdb, obj.External_Ids.Imdb_Id);
  78. if (obj.External_Ids.Tvdb_Id > 0)
  79. {
  80. remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture));
  81. }
  82. return new[] { remoteResult };
  83. }
  84. var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb);
  85. if (!string.IsNullOrEmpty(imdbId))
  86. {
  87. var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
  88. if (searchResult != null)
  89. {
  90. return new[] { searchResult };
  91. }
  92. }
  93. var tvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb);
  94. if (!string.IsNullOrEmpty(tvdbId))
  95. {
  96. var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
  97. if (searchResult != null)
  98. {
  99. return new[] { searchResult };
  100. }
  101. }
  102. return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
  103. }
  104. public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
  105. {
  106. var result = new MetadataResult<Series>();
  107. result.QueriedById = true;
  108. var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
  109. if (string.IsNullOrEmpty(tmdbId))
  110. {
  111. var imdbId = info.GetProviderId(MetadataProvider.Imdb);
  112. if (!string.IsNullOrEmpty(imdbId))
  113. {
  114. var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
  115. if (searchResult != null)
  116. {
  117. tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
  118. }
  119. }
  120. }
  121. if (string.IsNullOrEmpty(tmdbId))
  122. {
  123. var tvdbId = info.GetProviderId(MetadataProvider.Tvdb);
  124. if (!string.IsNullOrEmpty(tvdbId))
  125. {
  126. var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
  127. if (searchResult != null)
  128. {
  129. tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
  130. }
  131. }
  132. }
  133. if (string.IsNullOrEmpty(tmdbId))
  134. {
  135. result.QueriedById = false;
  136. var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false);
  137. var searchResult = searchResults.FirstOrDefault();
  138. if (searchResult != null)
  139. {
  140. tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
  141. }
  142. }
  143. if (!string.IsNullOrEmpty(tmdbId))
  144. {
  145. cancellationToken.ThrowIfCancellationRequested();
  146. result = await FetchMovieData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
  147. result.HasMetadata = result.Item != null;
  148. }
  149. return result;
  150. }
  151. private async Task<MetadataResult<Series>> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
  152. {
  153. SeriesResult seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false);
  154. if (seriesInfo == null)
  155. {
  156. return null;
  157. }
  158. tmdbId = seriesInfo.Id.ToString(_usCulture);
  159. string dataFilePath = GetDataFilePath(tmdbId, language);
  160. Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
  161. _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath);
  162. await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
  163. var result = new MetadataResult<Series>();
  164. result.Item = new Series();
  165. result.ResultLanguage = seriesInfo.ResultLanguage;
  166. var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
  167. ProcessMainInfo(result, seriesInfo, preferredCountryCode, settings);
  168. return result;
  169. }
  170. private void ProcessMainInfo(MetadataResult<Series> seriesResult, SeriesResult seriesInfo, string preferredCountryCode, TmdbSettingsResult settings)
  171. {
  172. var series = seriesResult.Item;
  173. series.Name = seriesInfo.Name;
  174. series.OriginalTitle = seriesInfo.Original_Name;
  175. series.SetProviderId(MetadataProvider.Tmdb, seriesInfo.Id.ToString(_usCulture));
  176. string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
  177. if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
  178. {
  179. series.CommunityRating = rating;
  180. }
  181. series.Overview = seriesInfo.Overview;
  182. if (seriesInfo.Networks != null)
  183. {
  184. series.Studios = seriesInfo.Networks.Select(i => i.Name).ToArray();
  185. }
  186. if (seriesInfo.Genres != null)
  187. {
  188. series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
  189. }
  190. series.HomePageUrl = seriesInfo.Homepage;
  191. series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
  192. if (string.Equals(seriesInfo.Status, "Ended", StringComparison.OrdinalIgnoreCase))
  193. {
  194. series.Status = SeriesStatus.Ended;
  195. series.EndDate = seriesInfo.Last_Air_Date;
  196. }
  197. else
  198. {
  199. series.Status = SeriesStatus.Continuing;
  200. }
  201. series.PremiereDate = seriesInfo.First_Air_Date;
  202. var ids = seriesInfo.External_Ids;
  203. if (ids != null)
  204. {
  205. if (!string.IsNullOrWhiteSpace(ids.Imdb_Id))
  206. {
  207. series.SetProviderId(MetadataProvider.Imdb, ids.Imdb_Id);
  208. }
  209. if (ids.Tvrage_Id > 0)
  210. {
  211. series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.Value.ToString(_usCulture));
  212. }
  213. if (ids.Tvdb_Id > 0)
  214. {
  215. series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.Value.ToString(_usCulture));
  216. }
  217. }
  218. var contentRatings = (seriesInfo.Content_Ratings ?? new ContentRatings()).Results ?? new List<ContentRating>();
  219. var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
  220. var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
  221. var minimumRelease = contentRatings.FirstOrDefault();
  222. if (ourRelease != null)
  223. {
  224. series.OfficialRating = ourRelease.Rating;
  225. }
  226. else if (usRelease != null)
  227. {
  228. series.OfficialRating = usRelease.Rating;
  229. }
  230. else if (minimumRelease != null)
  231. {
  232. series.OfficialRating = minimumRelease.Rating;
  233. }
  234. if (seriesInfo.Videos != null && seriesInfo.Videos.Results != null)
  235. {
  236. foreach (var video in seriesInfo.Videos.Results)
  237. {
  238. if ((video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
  239. || video.Type.Equals("clip", StringComparison.OrdinalIgnoreCase))
  240. && video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase))
  241. {
  242. series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.Key}");
  243. }
  244. }
  245. }
  246. seriesResult.ResetPeople();
  247. var tmdbImageUrl = settings.images.GetImageUrl("original");
  248. if (seriesInfo.Credits != null)
  249. {
  250. if (seriesInfo.Credits.Cast != null)
  251. {
  252. foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
  253. {
  254. var personInfo = new PersonInfo
  255. {
  256. Name = actor.Name.Trim(),
  257. Role = actor.Character,
  258. Type = PersonType.Actor,
  259. SortOrder = actor.Order
  260. };
  261. if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
  262. {
  263. personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
  264. }
  265. if (actor.Id > 0)
  266. {
  267. personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
  268. }
  269. seriesResult.AddPerson(personInfo);
  270. }
  271. }
  272. if (seriesInfo.Credits.Crew != null)
  273. {
  274. var keepTypes = new[]
  275. {
  276. PersonType.Director,
  277. PersonType.Writer,
  278. PersonType.Producer
  279. };
  280. foreach (var person in seriesInfo.Credits.Crew)
  281. {
  282. // Normalize this
  283. var type = TmdbUtils.MapCrewToPersonType(person);
  284. if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
  285. && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
  286. {
  287. continue;
  288. }
  289. seriesResult.AddPerson(new PersonInfo
  290. {
  291. Name = person.Name.Trim(),
  292. Role = person.Job,
  293. Type = type
  294. });
  295. }
  296. }
  297. }
  298. }
  299. internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId)
  300. {
  301. var dataPath = GetSeriesDataPath(appPaths);
  302. return Path.Combine(dataPath, tmdbId);
  303. }
  304. internal static string GetSeriesDataPath(IApplicationPaths appPaths)
  305. {
  306. var dataPath = Path.Combine(appPaths.CachePath, "tmdb-tv");
  307. return dataPath;
  308. }
  309. internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken)
  310. {
  311. SeriesResult mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
  312. if (mainResult == null)
  313. {
  314. return;
  315. }
  316. var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage);
  317. Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
  318. _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
  319. }
  320. internal async Task<SeriesResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
  321. {
  322. var url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey);
  323. if (!string.IsNullOrEmpty(language))
  324. {
  325. url += "&language=" + TmdbMovieProvider.NormalizeLanguage(language)
  326. + "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language
  327. }
  328. cancellationToken.ThrowIfCancellationRequested();
  329. using var mainRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
  330. foreach (var header in TmdbUtils.AcceptHeaders)
  331. {
  332. mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
  333. }
  334. using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage);
  335. await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
  336. var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(mainStream).ConfigureAwait(false);
  337. if (!string.IsNullOrEmpty(language))
  338. {
  339. mainResult.ResultLanguage = language;
  340. }
  341. cancellationToken.ThrowIfCancellationRequested();
  342. // If the language preference isn't english, then have the overview fallback to english if it's blank
  343. if (mainResult != null &&
  344. string.IsNullOrEmpty(mainResult.Overview) &&
  345. !string.IsNullOrEmpty(language) &&
  346. !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
  347. {
  348. _logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language);
  349. url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en";
  350. if (!string.IsNullOrEmpty(language))
  351. {
  352. // Get images in english and with no language
  353. url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
  354. }
  355. using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
  356. foreach (var header in TmdbUtils.AcceptHeaders)
  357. {
  358. mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
  359. }
  360. using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
  361. await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
  362. var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(stream).ConfigureAwait(false);
  363. mainResult.Overview = englishResult.Overview;
  364. mainResult.ResultLanguage = "en";
  365. }
  366. return mainResult;
  367. }
  368. internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken)
  369. {
  370. if (string.IsNullOrEmpty(tmdbId))
  371. {
  372. throw new ArgumentNullException(nameof(tmdbId));
  373. }
  374. var path = GetDataFilePath(tmdbId, language);
  375. var fileInfo = _fileSystem.GetFileSystemInfo(path);
  376. if (fileInfo.Exists)
  377. {
  378. // If it's recent or automatic updates are enabled, don't re-download
  379. if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
  380. {
  381. return Task.CompletedTask;
  382. }
  383. }
  384. return DownloadSeriesInfo(tmdbId, language, cancellationToken);
  385. }
  386. internal string GetDataFilePath(string tmdbId, string preferredLanguage)
  387. {
  388. if (string.IsNullOrEmpty(tmdbId))
  389. {
  390. throw new ArgumentNullException(nameof(tmdbId));
  391. }
  392. var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
  393. var filename = string.Format("series-{0}.json", preferredLanguage ?? string.Empty);
  394. return Path.Combine(path, filename);
  395. }
  396. private async Task<RemoteSearchResult> FindByExternalId(string id, string externalSource, CancellationToken cancellationToken)
  397. {
  398. var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}",
  399. id,
  400. TmdbUtils.ApiKey,
  401. externalSource);
  402. using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
  403. foreach (var header in TmdbUtils.AcceptHeaders)
  404. {
  405. requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
  406. }
  407. using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage);
  408. await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
  409. var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(stream).ConfigureAwait(false);
  410. if (result != null && result.Tv_Results != null)
  411. {
  412. var tv = result.Tv_Results.FirstOrDefault();
  413. if (tv != null)
  414. {
  415. var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
  416. var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
  417. var remoteResult = new RemoteSearchResult
  418. {
  419. Name = tv.Name,
  420. SearchProviderName = Name,
  421. ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path)
  422. ? null
  423. : tmdbImageUrl + tv.Poster_Path
  424. };
  425. remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture));
  426. return remoteResult;
  427. }
  428. }
  429. return null;
  430. }
  431. // After TheTVDB
  432. public int Order => 1;
  433. public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
  434. {
  435. return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken);
  436. }
  437. }
  438. }