Jede Zahl sagt jetzt, was sie ist — und woher sie kommt

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>
This commit is contained in:
Scarriffle
2026-10-08 22:41:41 +02:00
co-authored by Claude Opus 5
parent a609b2d087
commit ac64b20a5b
4 changed files with 229 additions and 30 deletions
+2 -1
View File
@@ -105,7 +105,8 @@ public sealed class Quellenprobe(
private static string Kurz(string quelle) => quelle switch
{
"imdb" => "IMDb",
"rottenTomatoes" => "RT",
"rottenTomatoes" => "RT Kritik",
"rottenTomatoesAudience" => "RT Publikum",
"metacritic" => "MC",
_ => quelle,
};
+55 -2
View File
@@ -28,6 +28,17 @@ public interface IOmdbSettingsSource
/// und genau das kennt man von dort.
/// </para>
/// <para>
/// <b>Der Publikumswert von Rotten Tomatoes kommt mit, falls OMDb ihn
/// hat.</b> Mit <c>tomatoes=true</c> legt OMDb weitere Felder derselben
/// Antwort bei — keine zweite Anfrage, kein zweiter Abzug vom
/// Tageskontingent. Darin steht <c>tomatoUserMeter</c>: der Wert des
/// Publikums neben dem der Kritik. Seit 2018 wird berichtet, dass dort
/// meist <c>N/A</c> steht (omdbapi/OMDb-API#102), und die Frage, ob die
/// Felder je wieder Daten führen, ist seit 2017 unbeantwortet offen (#5).
/// Deshalb wird nichts erwartet und nichts erzwungen: steht eine Zahl da,
/// erscheint sie; steht <c>N/A</c>, entsteht keine Wertung.
/// </para>
/// <para>
/// <b>Ohne Schlüssel geschieht nichts.</b> OMDb verlangt einen
/// persönlichen, kostenlosen; einen fremden mitzuliefern hiesse, dessen
/// Tageskontingent zu verbrauchen.
@@ -53,7 +64,8 @@ public sealed class OmdbClient(
try
{
using var ergebnis = await http.GetAsync(
$"?i={Uri.EscapeDataString(imdbId)}&apikey={Uri.EscapeDataString(schluessel)}", ct);
$"?i={Uri.EscapeDataString(imdbId)}&tomatoes=true"
+ $"&apikey={Uri.EscapeDataString(schluessel)}", ct);
if (!ergebnis.IsSuccessStatusCode)
{
@@ -85,9 +97,49 @@ public sealed class OmdbClient(
liste.Add(new Fremdwertung(quelle, wert, Anzeige(quelle, r.Value)));
}
// **Der Publikumswert steht nicht in `Ratings`, sondern einzeln.**
// Die Liste führt nur je eine Zahl pro Dienst, und das ist bei
// Rotten Tomatoes die der Kritik.
if (Prozent(Rohwert(antwort.TomatoUserMeter)) is { } publikum)
{
liste.Add(new Fremdwertung(
"rottenTomatoesAudience",
publikum / 10,
publikum.ToString("0", CultureInfo.InvariantCulture) + "%"));
}
return liste;
}
/// <summary>Holt den Text aus einem Feld, das auch eine Zahl sein kann.</summary>
///
/// <remarks>
/// <b>Fest auf <c>string</c> getippt wäre ein Zahlenfeld ein
/// Lesefehler</b> — und der verwirft nicht nur dieses Feld, sondern die
/// ganze Antwort samt der drei Bewertungen, die darin stehen. OMDb
/// schreibt hier bisher Text; darauf zu wetten ist unnötig.
/// </remarks>
private static string? Rohwert(JsonElement? feld) => feld switch
{
{ ValueKind: JsonValueKind.String } e => e.GetString(),
{ ValueKind: JsonValueKind.Number } e => e.GetRawText(),
_ => null,
};
/// <summary>Liest „88" oder „88%" als Zahl — und <c>N/A</c> als nichts.</summary>
private static double? Prozent(string? roh)
{
if (string.IsNullOrWhiteSpace(roh)) return null;
var zahl = roh.Trim().TrimEnd('%');
if (!double.TryParse(zahl, NumberStyles.Float, CultureInfo.InvariantCulture, out var wert))
return null;
// Ausserhalb von 0 bis 100 ist es kein Prozentwert, sondern ein
// Missverständnis über das Feld.
return wert is >= 0 and <= 100 ? wert : null;
}
/// <summary>Die Namen, die OMDb benutzt, auf unsere Kennungen.</summary>
private static string? Quelle(string? name) => name switch
{
@@ -129,7 +181,8 @@ public sealed class OmdbClient(
private sealed record OmdbAntwort(
[property: JsonPropertyName("Response")] string? Response,
[property: JsonPropertyName("Ratings")] List<OmdbWertung>? Ratings);
[property: JsonPropertyName("Ratings")] List<OmdbWertung>? Ratings,
[property: JsonPropertyName("tomatoUserMeter")] JsonElement? TomatoUserMeter = null);
private sealed record OmdbWertung(
[property: JsonPropertyName("Source")] string? Source,
@@ -126,6 +126,105 @@ public class OmdbTests
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>