Der Publikumswert steht auf der Seite, also wird er von dort gelesen
OMDb führt im Feld `Ratings` nur eine Zahl je Dienst, und bei Rotten
Tomatoes ist das die der Kritik. Die erweiterten Felder gibt es, sie
stehen aber seit Jahren auf `N/A`; Jellyfin fragt sie an und liest sie
nicht einmal aus — deren Antwortklasse kennt `tomatoUserMeter` gar nicht.
Dort hat den Publikumswert also niemand.
Auf der Seite selbst steht er. Gemessen an `/m/the_matrix`: im Quelltext
liegt `"audienceScore":{… "score":"85" …}` neben `"criticsScore"`, dazu
ein `application/ld+json` mit Titel und Erscheinungsdatum; bei Serien
unter `/tv/…` dasselbe, und ein unbekannter Weg antwortet mit 404. Eine
Prüfung mit `[Explicit]` geht einmal wirklich hinaus und bestätigt es:
`rottenTomatoes 83%, rottenTomatoesAudience 85%`.
Das ist Auslesen einer fremden Webseite, und es hat seinen Preis. Eine
offene Suche gibt es nicht — `/napi/search` antwortet mit 401 —, also
wird der Weg aus dem Titel geraten und hinterher am Jahr geprüft: auf der
Seite steht keine IMDb-Kennung, und `/m/the_lion_king` ist der von 1994.
Weicht das Jahr um mehr als eines ab, wird verworfen; lieber keine Zahl
als die eines fremden Films. Der Originaltitel kommt zuerst dran, sonst
fände ein deutscher Bestand dort fast nichts. Ohne Jahr zuerst, weil der
Normalfall den blanken Namen trägt — mit Jahr zuerst kostete jeder Titel
eine Anfrage zu viel.
Deshalb ein Schalter, und der steht aus, bis jemand ihn umlegt. Dazu eine
Bremse: höchstens eine Anfrage je Sekunde, prozessweit. Achthundert Titel
in einem Schwall von einer Adresse wären eine Zumutung und würden
gesperrt. Der Scan dauert dadurch länger; das ist der Preis fürs
Mitbenutzen.
Die Probe unter Einstellungen → Metadaten fragt die Quelle mit, und dort
ist sie mehr als eine Höflichkeit: wird die Seite umgebaut, merkt man es
genau da und sonst nirgends.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e05bbfc2cd
commit
8397e042b8
@@ -571,7 +571,15 @@ public sealed record Saisonreihe(string? Occasion, Page<MovieResponse> Items);
|
||||
/// </param>
|
||||
public sealed record MetadataSettingsResponse(
|
||||
bool HasApiKey, string Language, bool UsingProjectKey,
|
||||
bool HasFanartKey = false, bool HasOmdbKey = false);
|
||||
bool HasFanartKey = false, bool HasOmdbKey = false,
|
||||
|
||||
/// <summary>Ob die Seite von Rotten Tomatoes gelesen werden darf.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Kein Schlüssel, sondern ein Schalter.</b> Dort gibt es keine
|
||||
/// Zugänge zu vergeben; eingeschaltet wird das Auslesen der Seite.
|
||||
/// </remarks>
|
||||
bool RottenTomatoes = false);
|
||||
|
||||
/// <summary>Was ein Client über der Mediathek anzeigen muss.</summary>
|
||||
///
|
||||
@@ -584,7 +592,8 @@ public sealed record MetadataSettingsResponse(
|
||||
public sealed record AttributionResponse(string Text, string LogoUrl, string Url);
|
||||
|
||||
public sealed record MetadataSettingsRequest(
|
||||
string? ApiKey, string? Language, string? FanartApiKey = null, string? OmdbApiKey = null);
|
||||
string? ApiKey, string? Language, string? FanartApiKey = null, string? OmdbApiKey = null,
|
||||
bool? RottenTomatoes = null);
|
||||
|
||||
/// <summary>Was der Server über die Wiedergabe entscheidet.</summary>
|
||||
///
|
||||
|
||||
@@ -19,7 +19,8 @@ public static class SettingsEndpoints
|
||||
.Where(s => s.Key == DatabaseTmdbSettings.ApiKeyName
|
||||
|| s.Key == DatabaseTmdbSettings.LanguageName
|
||||
|| s.Key == DatabaseFanartSettings.ApiKeyName
|
||||
|| s.Key == DatabaseOmdbSettings.ApiKeyName)
|
||||
|| s.Key == DatabaseOmdbSettings.ApiKeyName
|
||||
|| s.Key == DatabaseRottenTomatoesSettings.SchalterName)
|
||||
.ToDictionaryAsync(s => s.Key, s => s.Value);
|
||||
|
||||
// **Der Schlüssel selbst kommt nicht zurück.** Er gäbe jedem
|
||||
@@ -38,7 +39,10 @@ public static class SettingsEndpoints
|
||||
HasFanartKey: !string.IsNullOrWhiteSpace(
|
||||
zeilen.GetValueOrDefault(DatabaseFanartSettings.ApiKeyName)),
|
||||
HasOmdbKey: !string.IsNullOrWhiteSpace(
|
||||
zeilen.GetValueOrDefault(DatabaseOmdbSettings.ApiKeyName))));
|
||||
zeilen.GetValueOrDefault(DatabaseOmdbSettings.ApiKeyName)),
|
||||
RottenTomatoes: string.Equals(
|
||||
zeilen.GetValueOrDefault(DatabaseRottenTomatoesSettings.SchalterName),
|
||||
"true", StringComparison.OrdinalIgnoreCase)));
|
||||
}).RequireAuthorization(FlaroPolicies.Administrator);
|
||||
|
||||
// **In welcher Sprache der Bestand vorliegt.**
|
||||
@@ -100,6 +104,11 @@ public static class SettingsEndpoints
|
||||
await Setzen(db, DatabaseFanartSettings.ApiKeyName, anfrage.FanartApiKey.Trim());
|
||||
if (anfrage.OmdbApiKey is not null)
|
||||
await Setzen(db, DatabaseOmdbSettings.ApiKeyName, anfrage.OmdbApiKey.Trim());
|
||||
|
||||
// **Ein Schalter, kein Schlüssel.** Rotten Tomatoes vergibt keine
|
||||
// Zugänge; eingeschaltet wird hier das Lesen ihrer Seite.
|
||||
if (anfrage.RottenTomatoes is { } an)
|
||||
await Setzen(db, DatabaseRottenTomatoesSettings.SchalterName, an ? "true" : "false");
|
||||
if (anfrage.Language is not null)
|
||||
{
|
||||
var sprache = anfrage.Language.Trim();
|
||||
|
||||
@@ -88,3 +88,31 @@ public sealed class DatabaseOmdbSettings(FlaroContext db) : IOmdbSettingsSource
|
||||
return _gemerkt = string.IsNullOrWhiteSpace(zeile?.Value) ? null : zeile.Value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ob die Seite von Rotten Tomatoes gelesen werden darf.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Kein Schlüssel, sondern ein Schalter — und der steht auf aus.</b>
|
||||
/// Rotten Tomatoes vergibt keine Zugänge; die beiden Werte stehen nur auf
|
||||
/// der Seite selbst. Das ist Auslesen einer fremden Webseite: es bricht,
|
||||
/// wenn dort umgebaut wird, und es geschieht ohne Einverständnis des
|
||||
/// Betreibers. Eine solche Quelle gehört nicht stillschweigend
|
||||
/// eingeschaltet, sondern bewusst.
|
||||
/// </remarks>
|
||||
public sealed class DatabaseRottenTomatoesSettings(FlaroContext db) : IRottenTomatoesSettingsSource
|
||||
{
|
||||
public const string SchalterName = "metadata.rottenTomatoes.enabled";
|
||||
|
||||
private bool? _gemerkt;
|
||||
|
||||
public async Task<bool> EingeschaltetAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_gemerkt is { } schon) return schon;
|
||||
|
||||
var zeile = await db.Settings.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Key == SchalterName, ct);
|
||||
|
||||
return (_gemerkt = string.Equals(zeile?.Value, "true", StringComparison.OrdinalIgnoreCase))
|
||||
.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,36 @@ builder.Services.AddHttpClient<OmdbClient>(c =>
|
||||
c.BaseAddress = new Uri(OmdbClient.BaseAddress);
|
||||
c.Timeout = TimeSpan.FromSeconds(20);
|
||||
});
|
||||
builder.Services.AddScoped<IBewertungsquelle>(sp => sp.GetRequiredService<OmdbClient>());
|
||||
|
||||
// **Rotten Tomatoes dazu, aber nur auf Wunsch.** Dort gibt es keine
|
||||
// Schnittstelle — die beiden Werte stehen auf der Seite selbst, und OMDb
|
||||
// gibt davon nur den der Kritik weiter. Das Auslesen einer fremden Webseite
|
||||
// bricht, wenn dort umgebaut wird; deshalb ein Schalter, und der steht aus.
|
||||
builder.Services.AddScoped<IRottenTomatoesSettingsSource, DatabaseRottenTomatoesSettings>();
|
||||
|
||||
// **Eine Anfrage je Sekunde, prozessweit.** Achthundert Titel in einem
|
||||
// Schwall von einer Adresse wären eine Zumutung und würden gesperrt. Der
|
||||
// Scan dauert dadurch länger; das ist der Preis fürs Mitbenutzen.
|
||||
builder.Services.AddSingleton(new Anklopfbremse(TimeSpan.FromSeconds(1)));
|
||||
builder.Services.AddHttpClient<RottenTomatoesClient>(c =>
|
||||
{
|
||||
c.BaseAddress = new Uri(RottenTomatoesClient.BaseAddress);
|
||||
c.Timeout = TimeSpan.FromSeconds(20);
|
||||
// Ohne Browserkennung antwortet die Seite mit einer Sperrseite statt
|
||||
// mit dem Film.
|
||||
c.DefaultRequestHeaders.UserAgent.ParseAdd(
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
+ "Chrome/131.0.0.0 Safari/537.36");
|
||||
});
|
||||
|
||||
// **Die Reihenfolge ist die Rangfolge.** Beide führen den Wert der Kritik;
|
||||
// OMDb aus zweiter Hand, Rotten Tomatoes als Quelle selbst. Wer hinten
|
||||
// steht, überschreibt.
|
||||
builder.Services.AddScoped<IBewertungsquelle>(sp => new Bewertungsquellen(
|
||||
[
|
||||
sp.GetRequiredService<OmdbClient>(),
|
||||
sp.GetRequiredService<RottenTomatoesClient>(),
|
||||
]));
|
||||
|
||||
// **Damit man sieht, ob ein Schlüssel wirkt.** „Hinterlegt" ist keine
|
||||
// Auskunft darüber, ob der Dienst antwortet.
|
||||
|
||||
@@ -32,17 +32,21 @@ public sealed class Quellenprobe(
|
||||
TmdbClient tmdb,
|
||||
FanartClient fanart,
|
||||
OmdbClient omdb,
|
||||
RottenTomatoesClient tomaten,
|
||||
ITmdbSettingsSource spracheVon)
|
||||
{
|
||||
/// <summary>„The Matrix" — bei allen drei Quellen vorhanden.</summary>
|
||||
/// <summary>„The Matrix" — bei allen Quellen vorhanden.</summary>
|
||||
private const int ProbeTmdbId = 603;
|
||||
private const string ProbeImdbId = "tt0133093";
|
||||
private const string ProbeTitel = "The Matrix";
|
||||
private const int ProbeJahr = 1999;
|
||||
|
||||
public async Task<IReadOnlyList<Quellenbefund>> LaufenAsync(CancellationToken ct = default) =>
|
||||
[
|
||||
await TmdbProbe(ct),
|
||||
await FanartProbe(ct),
|
||||
await OmdbProbe(ct),
|
||||
await TomatenProbe(ct),
|
||||
];
|
||||
|
||||
private async Task<Quellenbefund> TmdbProbe(CancellationToken ct)
|
||||
@@ -89,7 +93,8 @@ public sealed class Quellenprobe(
|
||||
if (!await omdb.EingerichtetAsync(ct))
|
||||
return new("omdb", false, false, "Kein Schlüssel hinterlegt — die Quelle ist aus.");
|
||||
|
||||
var wertungen = await omdb.HolenAsync(ProbeImdbId, ct);
|
||||
var wertungen = await omdb.HolenAsync(
|
||||
new Wertungsfrage(ProbeImdbId, ProbeTitel, ProbeJahr), ct);
|
||||
|
||||
if (wertungen.Count == 0)
|
||||
{
|
||||
@@ -98,10 +103,40 @@ public sealed class Quellenprobe(
|
||||
+ "oder ist das Tageskontingent aufgebraucht?");
|
||||
}
|
||||
|
||||
var namen = string.Join(", ", wertungen.Select(w => $"{Kurz(w.Quelle)} {w.Anzeige}"));
|
||||
return new("omdb", true, true, $"Erreichbar — zu The Matrix: {namen}.");
|
||||
return new("omdb", true, true, $"Erreichbar — zu The Matrix: {Aufzaehlung(wertungen)}.");
|
||||
}
|
||||
|
||||
/// <summary>Die Seite von Rotten Tomatoes, falls das Lesen erlaubt wurde.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Hier ist die Probe mehr als eine Höflichkeit.</b> Bei den anderen
|
||||
/// beantwortet sie die Frage, ob ein Schlüssel stimmt; hier beantwortet
|
||||
/// sie die Frage, ob das Auslesen überhaupt noch geht. Wird die Seite
|
||||
/// umgebaut oder der Zugriff gesperrt, merkt man es genau hier — und
|
||||
/// sonst nirgends.
|
||||
/// </remarks>
|
||||
private async Task<Quellenbefund> TomatenProbe(CancellationToken ct)
|
||||
{
|
||||
if (!await tomaten.EingerichtetAsync(ct))
|
||||
return new("rottenTomatoes", false, false, "Ausgeschaltet.");
|
||||
|
||||
var wertungen = await tomaten.HolenAsync(
|
||||
new Wertungsfrage(ProbeImdbId, ProbeTitel, ProbeJahr), ct);
|
||||
|
||||
if (wertungen.Count == 0)
|
||||
{
|
||||
return new("rottenTomatoes", true, false,
|
||||
"Nichts bekommen. Entweder ist der Zugriff gesperrt, "
|
||||
+ "oder die Seite ist umgebaut worden.");
|
||||
}
|
||||
|
||||
return new("rottenTomatoes", true, true,
|
||||
$"Erreichbar — zu The Matrix: {Aufzaehlung(wertungen)}.");
|
||||
}
|
||||
|
||||
private static string Aufzaehlung(IEnumerable<Fremdwertung> wertungen) =>
|
||||
string.Join(", ", wertungen.Select(w => $"{Kurz(w.Quelle)} {w.Anzeige}"));
|
||||
|
||||
private static string Kurz(string quelle) => quelle switch
|
||||
{
|
||||
"imdb" => "IMDb",
|
||||
|
||||
Reference in New Issue
Block a user