Nebeneinander standen „7,2" und „12", beide ohne Beschriftung. Die 12 konnte alles heissen: zwölf Folgen, zwölf von hundert Punkten, ab zwölf Jahren. Die Auskunft steckte im Tooltip, und den sieht nur, wer schon vermutet, dass dort etwas steht. Jetzt trägt jeder Wert seine Quelle: FSK 12, TMDB 7,2, IMDb 7,4, MC 62. Bei Rotten Tomatoes ist „RT 87%" ebenfalls keine Auskunft, denn dort stehen zwei Zahlen — die der Kritik und die des Publikums, oft weit auseinander. Also „RT Kritik" und „RT Publikum", auch wenn nur eine der beiden da ist. Und das Publikum wird mitverlangt: `tomatoes=true` legt die Felder derselben Antwort bei, kostet keine zweite Anfrage und nichts vom Tageskontingent. Ob etwas darin steht, ist eine andere Frage — seit 2018 wird berichtet, dass dort `N/A` steht, und die Frage, ob die Felder je wieder Daten führen, ist bei OMDb seit 2017 unbeantwortet offen. Darum wird nichts erwartet und nichts erzwungen: steht eine Zahl da, erscheint sie; steht `N/A`, entsteht keine Wertung. Die Probe unter Einstellungen → Metadaten zeigt, was wirklich kam. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
249 lines
9.0 KiB
C#
249 lines
9.0 KiB
C#
using System.Net;
|
|
using Flaro.Core.Metadata;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace Flaro.Metadata.Tests;
|
|
|
|
/// <summary>Bewertungen von IMDb, Rotten Tomatoes und Metacritic.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Warum OMDb und nicht die Quellen selbst.</b> IMDbs eigene
|
|
/// Schnittstelle kostet Geld, Rotten Tomatoes vergibt Zugänge nur an
|
|
/// Partner, Letterboxd hat gar keine offene. OMDb liefert alle drei in
|
|
/// einer Antwort, über die IMDb-Kennung, die Flaro ohnehin schon führt.
|
|
/// </remarks>
|
|
public class OmdbTests
|
|
{
|
|
private static OmdbClient Client(Attrappe attrappe, string? schluessel = "ein-schluessel") =>
|
|
new(new HttpClient(attrappe) { BaseAddress = new Uri(OmdbClient.BaseAddress) },
|
|
new FesterOmdbSchluessel(schluessel),
|
|
NullLogger<OmdbClient>.Instance);
|
|
|
|
private const string Antwort = """
|
|
{"Title":"Probe","Response":"True",
|
|
"Ratings":[
|
|
{"Source":"Internet Movie Database","Value":"7.4/10"},
|
|
{"Source":"Rotten Tomatoes","Value":"87%"},
|
|
{"Source":"Metacritic","Value":"62/100"}]}
|
|
""";
|
|
|
|
[Test]
|
|
public async Task Ohne_Schluessel_geht_keine_Anfrage_hinaus()
|
|
{
|
|
var attrappe = Attrappe.MitJson(Antwort);
|
|
|
|
var wertungen = await Client(attrappe, schluessel: null).HolenAsync("tt0000001");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(wertungen, Is.Empty);
|
|
Assert.That(attrappe.Aufrufe, Is.Empty);
|
|
});
|
|
}
|
|
|
|
/// <summary>Jede Quelle kommt mit zwei Werten: einem zum Vergleichen, einem zum Anzeigen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Rotten Tomatoes in „8,7 von 10" umzurechnen wäre eine Lüge über
|
|
/// die Quelle.</b> Dort steht 87 %, und genau so kennt man es. Zum
|
|
/// Sortieren braucht es trotzdem eine gemeinsame Skala — deshalb beides.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Alle_drei_Quellen_kommen_normiert_und_lesbar()
|
|
{
|
|
var wertungen = await Client(Attrappe.MitJson(Antwort)).HolenAsync("tt0000001");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
var imdb = wertungen.Single(w => w.Quelle == "imdb");
|
|
Assert.That(imdb.Wert, Is.EqualTo(7.4).Within(0.01));
|
|
Assert.That(imdb.Anzeige, Is.EqualTo("7,4"));
|
|
|
|
var rt = wertungen.Single(w => w.Quelle == "rottenTomatoes");
|
|
Assert.That(rt.Wert, Is.EqualTo(8.7).Within(0.01));
|
|
Assert.That(rt.Anzeige, Is.EqualTo("87%"), "so steht es dort, und so kennt man es");
|
|
|
|
var mc = wertungen.Single(w => w.Quelle == "metacritic");
|
|
Assert.That(mc.Wert, Is.EqualTo(6.2).Within(0.01));
|
|
Assert.That(mc.Anzeige, Is.EqualTo("62"));
|
|
});
|
|
}
|
|
|
|
/// <summary>OMDb antwortet auf Unbekanntes mit 200 und „Response: False".</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Das als Treffer durchzulassen hiesse, eine leere Bewertung abzulegen
|
|
/// — und die stünde danach als „keine Wertung" da, obwohl es nur keine
|
|
/// Antwort gab.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Eine_abschlaegige_Antwort_ergibt_nichts()
|
|
{
|
|
var json = """{"Response":"False","Error":"Incorrect IMDb ID."}""";
|
|
|
|
Assert.That(await Client(Attrappe.MitJson(json)).HolenAsync("tt0000001"), Is.Empty);
|
|
}
|
|
|
|
/// <summary>Eine unbekannte vierte Quelle wird übergangen, nicht geraten.</summary>
|
|
[Test]
|
|
public async Task Eine_fremde_Quelle_wird_uebergangen()
|
|
{
|
|
var json = """
|
|
{"Response":"True","Ratings":[
|
|
{"Source":"Irgendwas Neues","Value":"5/5"},
|
|
{"Source":"Internet Movie Database","Value":"7.4/10"}]}
|
|
""";
|
|
|
|
var wertungen = await Client(Attrappe.MitJson(json)).HolenAsync("tt0000001");
|
|
|
|
Assert.That(wertungen.Select(w => w.Quelle), Is.EquivalentTo(new[] { "imdb" }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Kein_Netz_ergibt_nichts_statt_eines_Absturzes()
|
|
{
|
|
var wertungen = await Client(Attrappe.DieWirft(new HttpRequestException("weg")))
|
|
.HolenAsync("tt0000001");
|
|
|
|
Assert.That(wertungen, Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Ein_Fehlercode_ergibt_nichts()
|
|
{
|
|
Assert.That(
|
|
await Client(Attrappe.MitStatus(HttpStatusCode.Unauthorized)).HolenAsync("tt0000001"),
|
|
Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Kennung_und_Schluessel_stehen_in_der_Anfrage()
|
|
{
|
|
var attrappe = Attrappe.MitJson(Antwort);
|
|
await Client(attrappe).HolenAsync("tt0133093");
|
|
|
|
Assert.That(attrappe.Aufrufe[0],
|
|
Does.Contain("i=tt0133093").And.Contain("apikey=ein-schluessel"));
|
|
}
|
|
|
|
/// <summary>Der Publikumswert wird mitverlangt — er kostet nichts extra.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Eine Anfrage, nicht zwei.</b> OMDb legt die zusätzlichen Felder
|
|
/// derselben Antwort bei; das Tageskontingent von tausend Zugriffen
|
|
/// bleibt davon unberührt.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Der_Publikumswert_wird_mitverlangt()
|
|
{
|
|
var attrappe = Attrappe.MitJson(Antwort);
|
|
await Client(attrappe).HolenAsync("tt0133093");
|
|
|
|
Assert.That(attrappe.Aufrufe, Has.Count.EqualTo(1));
|
|
Assert.That(attrappe.Aufrufe[0], Does.Contain("tomatoes=true"));
|
|
}
|
|
|
|
/// <summary>Steht eine Zahl im Publikumsfeld, wird sie eine eigene Wertung.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Getrennt von der Kritik, nicht vermischt.</b> Bei Rotten Tomatoes
|
|
/// liegen die beiden Zahlen oft weit auseinander; sie unter einem Namen
|
|
/// zu führen hiesse, dem Betrachter zu verschweigen, welche er sieht.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Ein_Publikumswert_kommt_getrennt_von_der_Kritik()
|
|
{
|
|
var json = """
|
|
{"Response":"True",
|
|
"Ratings":[{"Source":"Rotten Tomatoes","Value":"83%"}],
|
|
"tomatoUserMeter":"88"}
|
|
""";
|
|
|
|
var wertungen = await Client(Attrappe.MitJson(json)).HolenAsync("tt0000001");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
var kritik = wertungen.Single(w => w.Quelle == "rottenTomatoes");
|
|
Assert.That(kritik.Anzeige, Is.EqualTo("83%"));
|
|
|
|
var publikum = wertungen.Single(w => w.Quelle == "rottenTomatoesAudience");
|
|
Assert.That(publikum.Anzeige, Is.EqualTo("88%"));
|
|
Assert.That(publikum.Wert, Is.EqualTo(8.8).Within(0.01));
|
|
});
|
|
}
|
|
|
|
/// <summary><c>N/A</c> ist keine Wertung, sondern deren Fehlen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Und das ist bei OMDb der Normalfall.</b> Seit 2018 wird berichtet,
|
|
/// dass die erweiterten Rotten-Tomatoes-Felder leer bleiben; die Frage,
|
|
/// ob sie je wieder Daten führen, ist seit 2017 offen. Eine „0 %"
|
|
/// daraus zu machen wäre eine Behauptung über einen Film.
|
|
/// </remarks>
|
|
[TestCase("\"N/A\"")]
|
|
[TestCase("\"\"")]
|
|
[TestCase("null")]
|
|
[TestCase("\"120\"")]
|
|
public async Task Ohne_brauchbaren_Publikumswert_entsteht_keine_Wertung(string feld)
|
|
{
|
|
var json = $$"""
|
|
{"Response":"True",
|
|
"Ratings":[{"Source":"Rotten Tomatoes","Value":"83%"}],
|
|
"tomatoUserMeter":{{feld}}}
|
|
""";
|
|
|
|
var wertungen = await Client(Attrappe.MitJson(json)).HolenAsync("tt0000001");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(wertungen.Select(w => w.Quelle), Is.EquivalentTo(new[] { "rottenTomatoes" }));
|
|
Assert.That(wertungen.Single().Anzeige, Is.EqualTo("83%"),
|
|
"die Kritik bleibt, auch wenn das Publikum fehlt");
|
|
});
|
|
}
|
|
|
|
/// <summary>Ein Zahlenfeld darf nicht die ganze Antwort verwerfen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Fest auf Text getippt wäre das ein Lesefehler</b> — und der
|
|
/// verwirft nicht bloss dieses Feld, sondern die drei Bewertungen
|
|
/// daneben. OMDb schreibt hier bisher Text; darauf zu wetten ist
|
|
/// unnötig.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Ein_Publikumswert_als_Zahl_wird_auch_gelesen()
|
|
{
|
|
var json = """
|
|
{"Response":"True",
|
|
"Ratings":[{"Source":"Internet Movie Database","Value":"7.4/10"}],
|
|
"tomatoUserMeter":88}
|
|
""";
|
|
|
|
var wertungen = await Client(Attrappe.MitJson(json)).HolenAsync("tt0000001");
|
|
|
|
Assert.That(wertungen.Select(w => w.Quelle),
|
|
Is.EquivalentTo(new[] { "imdb", "rottenTomatoesAudience" }));
|
|
}
|
|
|
|
/// <summary>Ohne IMDb-Kennung wird gar nicht erst gefragt.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// OMDb kennt nur diesen Weg; nach einem Titel zu suchen hiesse, denselben
|
|
/// Zuordnungsfehler ein zweites Mal zu riskieren.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Ohne_Kennung_geht_keine_Anfrage_hinaus()
|
|
{
|
|
var attrappe = Attrappe.MitJson(Antwort);
|
|
|
|
await Client(attrappe).HolenAsync("");
|
|
|
|
Assert.That(attrappe.Aufrufe, Is.Empty);
|
|
}
|
|
}
|
|
|
|
public sealed record FesterOmdbSchluessel(string? Wert) : IOmdbSettingsSource
|
|
{
|
|
public Task<string?> SchluesselAsync(CancellationToken ct = default) => Task.FromResult(Wert);
|
|
}
|