IMDb, Rotten Tomatoes und Metacritic neben der TMDB-Wertung
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 ohnehin schon an jedem Film steht. **Eine Tabelle und kein Feld je Quelle.** `Movie.Rating` ist ein Skalar ohne Quellenangabe, und das war richtig, solange es eine Quelle gab. Für drei weitere wären es drei Spalten — und bei der vierten wieder eine Migration. Jetzt ist jede Quelle eine Zeile, je Titel und Quelle eine, mit eindeutigem Index darauf: ein zweiter Lauf schreibt um statt anzuhäufen. **Zwei Werte je Bewertung, und beide gebraucht.** Der normierte Wert zum Vergleichen und Sortieren, die Anzeige zum Hinschreiben. Rotten Tomatoes in „8,7 von 10" umzurechnen wäre eine Lüge über die Quelle — dort steht 87 %, und genau so kennt man es. **Wieder keine zweite Metadatenquelle.** `IBewertungsquelle` liefert Bewertungen, sonst nichts. Bei Titel und Beschreibung wären zwei Anbieter zwei Wahrheiten; bei einer Bewertung ist jede Quelle eine eigene Zahl, und da sind mehrere kein Widerspruch, sondern der Sinn der Sache. **Ohne Kennung wird nicht gefragt.** OMDb kennt nur den Weg über die IMDb-Kennung; nach einem Titel zu suchen hiesse, denselben Zuordnungsfehler ein zweites Mal zu riskieren. Und auf Unbekanntes antwortet OMDb mit 200 und `Response: "False"` — das als Treffer durchzulassen legte eine leere Bewertung ab, die danach als „keine Wertung" dastünde. Angezeigt wird mit Kürzel statt Stern: vier gleich aussehende Zahlen nebeneinander sagen nicht, woher sie kommen. Kein Logo — die Marken von IMDb und Rotten Tomatoes mitzuliefern hiesse, fremde Zeichen auszuliefern. Kein mitgelieferter Schlüssel, aus demselben Grund wie bei fanart.tv: OMDb begrenzt auf tausend Abrufe am Tag je Schlüssel. Einzutragen unter Einstellungen → Übersicht → Metadaten. 740 Tests grün, davon acht neue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
97edb49564
commit
af2b844d65
@@ -0,0 +1,149 @@
|
||||
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>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);
|
||||
}
|
||||
Reference in New Issue
Block a user