From 1758a93011487d945c2131bcf7a1ea4111d79d4f Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 30 Jul 2026 01:54:16 +0300 Subject: [PATCH] Enhance TmdbMetadataProvider to support fallback for localized poster retrieval Updated the TmdbMetadataProvider class to implement a fallback mechanism for obtaining show posters when localized images are unavailable. Introduced a new method, NeutralPosterPathAsync, which retrieves a language-neutral poster if the localized one is missing. Additionally, updated related tests to verify the correct behavior of this fallback logic, ensuring that shows without localized posters still display appropriate imagery. --- .../Metadata/TmdbMetadataProvider.cs | 26 +++++++++++++++++- .../Metadata/TmdbMetadataProviderTests.cs | 27 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/backend/src/TeleWave.Infrastructure/Metadata/TmdbMetadataProvider.cs b/backend/src/TeleWave.Infrastructure/Metadata/TmdbMetadataProvider.cs index 96556ab..3d8b4b8 100644 --- a/backend/src/TeleWave.Infrastructure/Metadata/TmdbMetadataProvider.cs +++ b/backend/src/TeleWave.Infrastructure/Metadata/TmdbMetadataProvider.cs @@ -104,12 +104,20 @@ public sealed class TmdbMetadataProvider( if (doc is null) return null; var root = doc.RootElement; + + // Постер TMDb отдаёт под запрошенный язык, и у шоу без локализованной обложки поле пустое — + // хотя в поиске картинка была: там приходит язык-нейтральная. Без добора «Применить» молча + // оставляло бы шоу без постера, и это неотличимо от «постера нет вовсе». + var posterPath = + GetString(root, "poster_path") + ?? await NeutralPosterPathAsync(movie, externalId, cancellationToken); + return new ShowMetadata( externalId, GetString(root, TitleField(movie)) ?? "—", YearFrom(GetString(root, DateField(movie))), GetString(root, "overview"), - PosterUrl(GetString(root, "poster_path")), + PosterUrl(posterPath), GenresFrom(root), movie ? MovieCertification(root) : TvCertification(root), FranchiseFrom(root), @@ -117,6 +125,22 @@ public sealed class TmdbMetadataProvider( ); } + /// + /// Постер без привязки к языку: тот же запрос, но без language. Второе обращение к API + /// делается только когда локализованного постера нет, — то есть редко. + /// + private async Task NeutralPosterPathAsync( + bool movie, + string externalId, + CancellationToken cancellationToken + ) + { + var query = UsesBearer ? string.Empty : $"?api_key={Tmdb.ApiKey}"; + var url = $"{Tmdb.BaseUrl}/{Segment(movie)}/{externalId}{query}"; + using var doc = await TryGetAsync(httpFactory, url, cancellationToken, BearerToken); + return doc is null ? null : GetString(doc.RootElement, "poster_path"); + } + /// /// Франшиза фильма: TMDb отдаёт её прямо в ответе по фильму (belongs_to_collection), /// отдельного запроса не нужно. У сериалов этого поля нет. diff --git a/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs b/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs index bfe1852..1c66a11 100644 --- a/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs +++ b/backend/tests/TeleWave.Application.Tests/Metadata/TmdbMetadataProviderTests.cs @@ -105,9 +105,10 @@ public class TmdbMetadataProviderTests [Fact] public async Task GetShow_Movie_PrefersTheatricalCertification() { + // Постер в ответе есть — значит, добора язык-нейтрального не будет и запрос ровно один. var http = FakeHttpClientFactory.Json( """ - {"title":"Фильм","release_dates":{"results":[{"iso_3166_1":"US","release_dates":[ + {"title":"Фильм","poster_path":"/p.jpg","release_dates":{"results":[{"iso_3166_1":"US","release_dates":[ {"type":4,"certification":"R"}, {"type":3,"certification":"PG-13"} ]}]}} @@ -121,6 +122,30 @@ public class TmdbMetadataProviderTests Assert.Equal("PG-13", show?.ContentRating); } + /// + /// TMDb отдаёт постер под запрошенный язык, и у шоу без локализованной обложки поле пустое — + /// хотя в поиске картинка была. Без добора «Применить» молча оставляло бы шоу без постера. + /// + [Fact] + public async Task GetShow_WithoutLocalizedPoster_FallsBackToNeutralOne() + { + // Локализованный ответ (с language) — без постера, язык-нейтральный — с ним. + var http = new FakeHttpClientFactory(url => + FakeHttpClientFactory.Ok( + url.Contains("language=", StringComparison.Ordinal) + ? """{"name":"Сериал"}""" + : """{"name":"Сериал","poster_path":"/neutral.jpg"}""" + ) + ); + + var show = await Provider(http).GetShowAsync("9", ShowKind.Series, CancellationToken.None); + + Assert.Equal(2, http.Urls.Count); + Assert.DoesNotContain("language=", http.LastUrl); + Assert.Contains("/tv/9", http.LastUrl); + Assert.EndsWith("/neutral.jpg", show?.PosterUrl); + } + [Fact] public async Task GetShow_Movie_FallsBackToAnyReleaseCertification() {