using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using Flaro.Core.Metadata; using Microsoft.Extensions.Logging; namespace Flaro.Metadata; /// Woher der Schlüssel für OMDb kommt. public interface IOmdbSettingsSource { Task SchluesselAsync(CancellationToken ct = default); } /// 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, und Letterboxd hat gar keine offene. OMDb liefert alle drei in /// einer Antwort, über die IMDb-Kennung, die Flaro ohnehin schon führt. /// /// /// Zwei Werte je Bewertung, und beide gebraucht. Der normierte Wert /// zum Vergleichen, 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 das kennt man von dort. /// /// /// Ohne Schlüssel geschieht nichts. OMDb verlangt einen /// persönlichen, kostenlosen; einen fremden mitzuliefern hiesse, dessen /// Tageskontingent zu verbrauchen. /// /// public sealed class OmdbClient( HttpClient http, IOmdbSettingsSource einstellungen, ILogger log) : IBewertungsquelle { public const string BaseAddress = "https://www.omdbapi.com/"; public async Task EingerichtetAsync(CancellationToken ct = default) => !string.IsNullOrWhiteSpace(await einstellungen.SchluesselAsync(ct)); public async Task> HolenAsync( string imdbId, CancellationToken ct = default) { var schluessel = await einstellungen.SchluesselAsync(ct); if (string.IsNullOrWhiteSpace(schluessel) || string.IsNullOrWhiteSpace(imdbId)) return []; OmdbAntwort? antwort; try { using var ergebnis = await http.GetAsync( $"?i={Uri.EscapeDataString(imdbId)}&apikey={Uri.EscapeDataString(schluessel)}", ct); if (!ergebnis.IsSuccessStatusCode) { log.LogDebug("OMDb antwortete mit {Code} für {Kennung}.", ergebnis.StatusCode, imdbId); return []; } antwort = await ergebnis.Content.ReadFromJsonOderNull(ct); } catch (Exception fehler) when (fehler is HttpRequestException or TaskCanceledException or JsonException) { // Eine fremde Quelle, die hakt, darf keinen Scan anhalten. log.LogDebug(fehler, "OMDb war für {Kennung} nicht zu erreichen.", imdbId); return []; } // OMDb antwortet auf Unbekanntes mit 200 und `Response: "False"`. if (antwort is null || !string.Equals(antwort.Response, "True", StringComparison.OrdinalIgnoreCase)) return []; var liste = new List(); foreach (var r in antwort.Ratings ?? []) { var quelle = Quelle(r.Source); if (quelle is null || string.IsNullOrWhiteSpace(r.Value)) continue; if (Lesen(quelle, r.Value) is not { } wert) continue; liste.Add(new Fremdwertung(quelle, wert, Anzeige(quelle, r.Value))); } return liste; } /// Die Namen, die OMDb benutzt, auf unsere Kennungen. private static string? Quelle(string? name) => name switch { "Internet Movie Database" => "imdb", "Rotten Tomatoes" => "rottenTomatoes", "Metacritic" => "metacritic", // Käme eine vierte dazu, stünde sie hier — bis dahin wird sie // übergangen statt mit falschem Namen abgelegt. _ => null, }; /// Bringt „8.7/10", „87%" und „62/100" auf dieselbe Skala. /// /// /// Nur zum Vergleichen. Angezeigt wird, was die Quelle sagt — siehe /// . /// private static double? Lesen(string quelle, string roh) { var zahl = roh.Split('/')[0].TrimEnd('%'); if (!double.TryParse(zahl, NumberStyles.Float, CultureInfo.InvariantCulture, out var wert)) return null; return quelle switch { "imdb" => wert, "rottenTomatoes" => wert / 10, "metacritic" => wert / 10, _ => null, }; } /// Was dastehen soll — in der Form, die man von der Quelle kennt. private static string Anzeige(string quelle, string roh) => quelle switch { "imdb" => roh.Split('/')[0].Replace('.', ','), "rottenTomatoes" => roh, _ => roh.Split('/')[0], }; private sealed record OmdbAntwort( [property: JsonPropertyName("Response")] string? Response, [property: JsonPropertyName("Ratings")] List? Ratings); private sealed record OmdbWertung( [property: JsonPropertyName("Source")] string? Source, [property: JsonPropertyName("Value")] string? Value); }