Eine zweite Bildquelle, weil TMDB nicht alles hat

„365 Tage" stand mit polnischem Plakat da. Nachgesehen: TMDB hat zu diesem
Film 26 Plakate und kein einziges deutsches. Die Auswahl von gestern nimmt
seither das sprachlose statt des polnischen — erfinden kann sie keines.

fanart.tv ist die Quelle, bei der jedes Plakat seine Sprache mitführt, und
die Gemeinschaft dort pflegt landessprachliche Fassungen. OMDb wäre keine
Hilfe gewesen: es liefert das Plakat von IMDb, und das ist in aller Regel
das amerikanische.

**Ausdrücklich keine zweite Metadatenquelle.** `IBildquelle` liefert
Bilder, sonst nichts. Titel, Beschreibung und Besetzung kommen weiterhin
von einer Stelle — zwei Anbieter für dieselbe Angabe wären zwei Wahrheiten.

**Gefragt wird nur, wenn es lohnt.** `MovieMetadata` trägt jetzt die
Sprache des gewählten Plakats mit. Hat TMDB schon eines in der
eingestellten Sprache, geht keine Anfrage hinaus — bei achthundert Titeln
ist das der Unterschied zwischen „läuft nebenher" und „hängt am fremden
Dienst". Und nur ein landessprachliches sticht: ein sprachloses von
fanart.tv ist nicht besser als ein sprachloses von TMDB.

**Die Rangfolge liegt jetzt im Kern**, nicht im TMDB-Client. Sie gilt für
alle Quellen, sonst kämen die Bilder zweier Anbieter in zwei Reihenfolgen
zusammen. Im Bilderdialog stehen beide Quellen gemeinsam geordnet: ein
deutsches Plakat von fanart.tv vor einem sprachlosen von TMDB.

**Nur Filme.** Der Serienweg von fanart.tv verlangt eine TVDB-Kennung, und
die führt Flaro nicht. Eine halbe Lösung, die bei Serien stillschweigend
nichts tut, wäre schlechter als eine, die sagt, worauf sie sich beschränkt.

**Kein mitgelieferter Schlüssel.** Bei TMDB gibt es einen als Rückfall;
fanart.tv verlangt einen persönlichen, und einen fremden mitzuliefern
hiesse, ihn fremd zu benutzen. Ohne eigenen Schlüssel geht keine Anfrage
hinaus — geprüft. Einzutragen unter Einstellungen → Übersicht → Metadaten.

Dass „00" dort „ohne Sprache" heisst, wird beim Lesen übersetzt: sonst
führte die gemeinsame Rangfolge ein sprachloses Plakat als Sprache „00"
und setzte es hinter Englisch statt davor.

732 Tests grün, davon sieben neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-08 21:18:27 +02:00
co-authored by Claude Opus 5
parent 2bd13f0f19
commit 97edb49564
13 changed files with 545 additions and 32 deletions
@@ -0,0 +1,137 @@
using System.Net;
using Flaro.Core.Metadata;
using Microsoft.Extensions.Logging.Abstractions;
namespace Flaro.Metadata.Tests;
/// <summary>Plakate von fanart.tv — der Quelle, bei der die Sprache mitsteht.</summary>
///
/// <remarks>
/// <b>Der Anlass.</b> „365 Tage" stand mit polnischem Plakat da. Nachgesehen:
/// TMDB hat zu diesem Film 26 Plakate und kein einziges deutsches. Die
/// Auswahl im TMDB-Client nimmt seither das sprachlose statt des polnischen
/// — erfinden kann sie ein deutsches nicht. Genau dafür gibt es diese zweite
/// Quelle.
/// </remarks>
public class FanartTests
{
private static FanartClient Client(Attrappe attrappe, string? schluessel = "ein-schluessel") =>
new(new HttpClient(attrappe) { BaseAddress = new Uri(FanartClient.BaseAddress) },
new FesterFanartSchluessel(schluessel),
NullLogger<FanartClient>.Instance);
private const string Antwort = """
{"name":"Probe","tmdb_id":"500",
"movieposter":[
{"id":"1","url":"https://assets.fanart.tv/de.jpg","lang":"de","likes":"3"},
{"id":"2","url":"https://assets.fanart.tv/en.jpg","lang":"en","likes":"9"},
{"id":"3","url":"https://assets.fanart.tv/ohne.jpg","lang":"00","likes":"5"}],
"moviebackground":[
{"id":"4","url":"https://assets.fanart.tv/hg.jpg","lang":"","likes":"2"}]}
""";
[Test]
public async Task Ohne_Schluessel_geht_keine_Anfrage_hinaus()
{
var attrappe = Attrappe.MitJson(Antwort);
var bilder = await Client(attrappe, schluessel: null).BilderAsync("500");
Assert.Multiple(() =>
{
Assert.That(bilder, Is.Empty);
Assert.That(attrappe.Aufrufe, Is.Empty,
"fanart.tv verlangt einen persönlichen Schlüssel — ohne ihn wird nicht gefragt");
});
}
[Test]
public async Task Die_Plakate_kommen_mit_ihrer_Sprache()
{
var bilder = await Client(Attrappe.MitJson(Antwort)).BilderAsync("500");
var plakate = bilder.Where(b => b.Kind == "poster").ToList();
Assert.Multiple(() =>
{
Assert.That(plakate, Has.Count.EqualTo(3));
Assert.That(plakate.Select(p => p.Language), Is.EquivalentTo(new[] { "de", "en", null }));
Assert.That(plakate.All(p => p.Quelle == "fanart"), Is.True);
});
}
/// <summary>„00" ist bei fanart.tv die Schreibweise für „ohne Sprache".</summary>
///
/// <remarks>
/// Sie durchzureichen hiesse, ein sprachloses Plakat als Sprache „00" zu
/// führen — und die gemeinsame Rangfolge setzte es damit hinter Englisch
/// statt davor.
/// </remarks>
[Test]
public async Task Die_Null_Null_wird_zu_sprachlos()
{
var bilder = await Client(Attrappe.MitJson(Antwort)).BilderAsync("500");
Assert.That(bilder.Any(b => b.Language == "00"), Is.False);
}
/// <summary>Zu den meisten Filmen hat fanart.tv nichts — das ist kein Fehler.</summary>
[Test]
public async Task Ein_unbekannter_Film_ergibt_eine_leere_Liste()
{
var bilder = await Client(Attrappe.MitStatus(HttpStatusCode.NotFound)).BilderAsync("500");
Assert.That(bilder, Is.Empty);
}
/// <summary>Eine hakende fremde Quelle darf keinen Scan anhalten.</summary>
[Test]
public async Task Kein_Netz_ergibt_nichts_statt_eines_Absturzes()
{
var bilder = await Client(Attrappe.DieWirft(new HttpRequestException("weg"))).BilderAsync("500");
Assert.That(bilder, Is.Empty);
}
[Test]
public async Task Der_Schluessel_steht_in_der_Anfrage()
{
var attrappe = Attrappe.MitJson(Antwort);
await Client(attrappe).BilderAsync("500");
Assert.That(attrappe.Aufrufe[0], Does.Contain("movies/500").And.Contain("api_key=ein-schluessel"));
}
/// <summary>Ein deutsches Plakat von fanart.tv sticht ein sprachloses von TMDB.</summary>
///
/// <remarks>
/// Das ist der ganze Zweck der zweiten Quelle, und die Rangfolge dafür
/// liegt im Kern — sie muss für beide Quellen dieselbe sein, sonst kämen
/// die Bilder zweier Anbieter in zwei Reihenfolgen zusammen.
/// </remarks>
[Test]
public async Task Die_gemeinsame_Rangfolge_stellt_das_deutsche_nach_vorn()
{
var vonFanart = await Client(Attrappe.MitJson(Antwort)).BilderAsync("500");
var vonTmdb = new[]
{
new Bildkandidat("poster", "tmdb-ohne.jpg", "t", null, 9.5),
new Bildkandidat("poster", "tmdb-en.jpg", "t", "en", 9.9),
};
var geordnet = Bildrang
.Geordnet(vonTmdb.Concat(vonFanart.Where(b => b.Kind == "poster")), "de")
.ToList();
Assert.Multiple(() =>
{
Assert.That(geordnet[0].Url, Does.EndWith("de.jpg"));
Assert.That(geordnet[0].Quelle, Is.EqualTo("fanart"));
Assert.That(geordnet[1].Url, Does.Contain("ohne"), "dann das sprachlose, egal von wem");
});
}
}
public sealed record FesterFanartSchluessel(string? Wert) : IFanartSettingsSource
{
public Task<string?> SchluesselAsync(CancellationToken ct = default) => Task.FromResult(Wert);
}