Enhance TmdbMetadataProvider to support fallback for localized poster retrieval
ci / build-backend (push) Successful in 1m40s
ci / build-frontend (push) Successful in 54s
ci / tests (push) Successful in 1m44s
ci / sonar (push) Successful in 4m57s

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.
This commit is contained in:
Leonid Pershin
2026-07-30 01:54:16 +03:00
parent 10e05b8689
commit 1758a93011
2 changed files with 51 additions and 2 deletions
@@ -104,12 +104,20 @@ public sealed class TmdbMetadataProvider(
if (doc is null) if (doc is null)
return null; return null;
var root = doc.RootElement; var root = doc.RootElement;
// Постер TMDb отдаёт под запрошенный язык, и у шоу без локализованной обложки поле пустое —
// хотя в поиске картинка была: там приходит язык-нейтральная. Без добора «Применить» молча
// оставляло бы шоу без постера, и это неотличимо от «постера нет вовсе».
var posterPath =
GetString(root, "poster_path")
?? await NeutralPosterPathAsync(movie, externalId, cancellationToken);
return new ShowMetadata( return new ShowMetadata(
externalId, externalId,
GetString(root, TitleField(movie)) ?? "—", GetString(root, TitleField(movie)) ?? "—",
YearFrom(GetString(root, DateField(movie))), YearFrom(GetString(root, DateField(movie))),
GetString(root, "overview"), GetString(root, "overview"),
PosterUrl(GetString(root, "poster_path")), PosterUrl(posterPath),
GenresFrom(root), GenresFrom(root),
movie ? MovieCertification(root) : TvCertification(root), movie ? MovieCertification(root) : TvCertification(root),
FranchiseFrom(root), FranchiseFrom(root),
@@ -117,6 +125,22 @@ public sealed class TmdbMetadataProvider(
); );
} }
/// <summary>
/// Постер без привязки к языку: тот же запрос, но без <c>language</c>. Второе обращение к API
/// делается только когда локализованного постера нет, — то есть редко.
/// </summary>
private async Task<string?> 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");
}
/// <summary> /// <summary>
/// Франшиза фильма: TMDb отдаёт её прямо в ответе по фильму (<c>belongs_to_collection</c>), /// Франшиза фильма: TMDb отдаёт её прямо в ответе по фильму (<c>belongs_to_collection</c>),
/// отдельного запроса не нужно. У сериалов этого поля нет. /// отдельного запроса не нужно. У сериалов этого поля нет.
@@ -105,9 +105,10 @@ public class TmdbMetadataProviderTests
[Fact] [Fact]
public async Task GetShow_Movie_PrefersTheatricalCertification() public async Task GetShow_Movie_PrefersTheatricalCertification()
{ {
// Постер в ответе есть — значит, добора язык-нейтрального не будет и запрос ровно один.
var http = FakeHttpClientFactory.Json( 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":4,"certification":"R"},
{"type":3,"certification":"PG-13"} {"type":3,"certification":"PG-13"}
]}]}} ]}]}}
@@ -121,6 +122,30 @@ public class TmdbMetadataProviderTests
Assert.Equal("PG-13", show?.ContentRating); Assert.Equal("PG-13", show?.ContentRating);
} }
/// <summary>
/// TMDb отдаёт постер под запрошенный язык, и у шоу без локализованной обложки поле пустое —
/// хотя в поиске картинка была. Без добора «Применить» молча оставляло бы шоу без постера.
/// </summary>
[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] [Fact]
public async Task GetShow_Movie_FallsBackToAnyReleaseCertification() public async Task GetShow_Movie_FallsBackToAnyReleaseCertification()
{ {