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() {