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