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,137 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flaro.Core.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Flaro.Metadata;
|
||||
|
||||
/// <summary>Woher der Schlüssel für OMDb kommt.</summary>
|
||||
public interface IOmdbSettingsSource
|
||||
{
|
||||
Task<string?> SchluesselAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>Bewertungen von IMDb, Rotten Tomatoes und Metacritic.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Warum OMDb und nicht die Quellen selbst.</b> 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Zwei Werte je Bewertung, und beide gebraucht.</b> 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Ohne Schlüssel geschieht nichts.</b> OMDb verlangt einen
|
||||
/// persönlichen, kostenlosen; einen fremden mitzuliefern hiesse, dessen
|
||||
/// Tageskontingent zu verbrauchen.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class OmdbClient(
|
||||
HttpClient http,
|
||||
IOmdbSettingsSource einstellungen,
|
||||
ILogger<OmdbClient> log) : IBewertungsquelle
|
||||
{
|
||||
public const string BaseAddress = "https://www.omdbapi.com/";
|
||||
|
||||
public async Task<bool> EingerichtetAsync(CancellationToken ct = default) =>
|
||||
!string.IsNullOrWhiteSpace(await einstellungen.SchluesselAsync(ct));
|
||||
|
||||
public async Task<IReadOnlyList<Fremdwertung>> 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<OmdbAntwort>(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<Fremdwertung>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Die Namen, die OMDb benutzt, auf unsere Kennungen.</summary>
|
||||
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,
|
||||
};
|
||||
|
||||
/// <summary>Bringt „8.7/10", „87%" und „62/100" auf dieselbe Skala.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Nur zum Vergleichen. Angezeigt wird, was die Quelle sagt — siehe
|
||||
/// <see cref="Anzeige"/>.
|
||||
/// </remarks>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Was dastehen soll — in der Form, die man von der Quelle kennt.</summary>
|
||||
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<OmdbWertung>? Ratings);
|
||||
|
||||
private sealed record OmdbWertung(
|
||||
[property: JsonPropertyName("Source")] string? Source,
|
||||
[property: JsonPropertyName("Value")] string? Value);
|
||||
}
|
||||
Reference in New Issue
Block a user