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);
}