using System.Net; using Flaro.Core.Metadata; using Microsoft.Extensions.Logging.Abstractions; namespace Flaro.Metadata.Tests; /// Bewertungen von IMDb, Rotten Tomatoes und Metacritic. /// /// /// Warum OMDb und nicht die Quellen selbst. 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. /// public class OmdbTests { /// Wonach gefragt wird — bei OMDb zählt allein die Kennung. private static Wertungsfrage Frage(string imdbId) => new(imdbId, "Probe", 1999); private static OmdbClient Client(Attrappe attrappe, string? schluessel = "ein-schluessel") => new(new HttpClient(attrappe) { BaseAddress = new Uri(OmdbClient.BaseAddress) }, new FesterOmdbSchluessel(schluessel), NullLogger.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(Frage("tt0000001")); Assert.Multiple(() => { Assert.That(wertungen, Is.Empty); Assert.That(attrappe.Aufrufe, Is.Empty); }); } /// Jede Quelle kommt mit zwei Werten: einem zum Vergleichen, einem zum Anzeigen. /// /// /// 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. Zum /// Sortieren braucht es trotzdem eine gemeinsame Skala — deshalb beides. /// [Test] public async Task Alle_drei_Quellen_kommen_normiert_und_lesbar() { var wertungen = await Client(Attrappe.MitJson(Antwort)).HolenAsync(Frage("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")); }); } /// OMDb antwortet auf Unbekanntes mit 200 und „Response: False". /// /// /// Das als Treffer durchzulassen hiesse, eine leere Bewertung abzulegen /// — und die stünde danach als „keine Wertung" da, obwohl es nur keine /// Antwort gab. /// [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(Frage("tt0000001")), Is.Empty); } /// Eine unbekannte vierte Quelle wird übergangen, nicht geraten. [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(Frage("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(Frage("tt0000001")); Assert.That(wertungen, Is.Empty); } [Test] public async Task Ein_Fehlercode_ergibt_nichts() { Assert.That( await Client(Attrappe.MitStatus(HttpStatusCode.Unauthorized)).HolenAsync(Frage("tt0000001")), Is.Empty); } [Test] public async Task Kennung_und_Schluessel_stehen_in_der_Anfrage() { var attrappe = Attrappe.MitJson(Antwort); await Client(attrappe).HolenAsync(Frage("tt0133093")); Assert.That(attrappe.Aufrufe[0], Does.Contain("i=tt0133093").And.Contain("apikey=ein-schluessel")); } /// Der Publikumswert wird mitverlangt — er kostet nichts extra. /// /// /// Eine Anfrage, nicht zwei. OMDb legt die zusätzlichen Felder /// derselben Antwort bei; das Tageskontingent von tausend Zugriffen /// bleibt davon unberührt. /// [Test] public async Task Der_Publikumswert_wird_mitverlangt() { var attrappe = Attrappe.MitJson(Antwort); await Client(attrappe).HolenAsync(Frage("tt0133093")); Assert.That(attrappe.Aufrufe, Has.Count.EqualTo(1)); Assert.That(attrappe.Aufrufe[0], Does.Contain("tomatoes=true")); } /// Steht eine Zahl im Publikumsfeld, wird sie eine eigene Wertung. /// /// /// Getrennt von der Kritik, nicht vermischt. 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. /// [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(Frage("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)); }); } /// N/A ist keine Wertung, sondern deren Fehlen. /// /// /// Und das ist bei OMDb der Normalfall. 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. /// [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(Frage("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"); }); } /// Ein Zahlenfeld darf nicht die ganze Antwort verwerfen. /// /// /// Fest auf Text getippt wäre das ein Lesefehler — und der /// verwirft nicht bloss dieses Feld, sondern die drei Bewertungen /// daneben. OMDb schreibt hier bisher Text; darauf zu wetten ist /// unnötig. /// [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(Frage("tt0000001")); Assert.That(wertungen.Select(w => w.Quelle), Is.EquivalentTo(new[] { "imdb", "rottenTomatoesAudience" })); } /// Ohne IMDb-Kennung wird gar nicht erst gefragt. /// /// /// OMDb kennt nur diesen Weg; nach einem Titel zu suchen hiesse, denselben /// Zuordnungsfehler ein zweites Mal zu riskieren. /// [Test] public async Task Ohne_Kennung_geht_keine_Anfrage_hinaus() { var attrappe = Attrappe.MitJson(Antwort); await Client(attrappe).HolenAsync(Frage("")); Assert.That(attrappe.Aufrufe, Is.Empty); } } public sealed record FesterOmdbSchluessel(string? Wert) : IOmdbSettingsSource { public Task SchluesselAsync(CancellationToken ct = default) => Task.FromResult(Wert); }