Vierundzwanzig Bilder, und der Weg dorthin fehlte sowieso

Zwei Dinge, die zusammengehören.

**Den Bilderdialog gab es nur an der Kachel.** Wer auf der Seite eines
Films stand und sah, dass das Plakat falsch ist, stand genau dort, wo er
nichts dagegen tun konnte — der Weg führte zurück in die Liste und über
das Dreipunktmenü. Jetzt steht „Bearbeiten" in der Knopfreihe der
Detailseite, bei Filmen wie bei Serien, und nur im Kopf: dieselbe Reihe
steht auch über jeder Fassung und jeder Folge.

**Und es waren vierundzwanzig, weil dort vierundzwanzig stand.** Bei
„365 Tage" hat TMDB neunundsechzig Plakate. Dazu ein zweiter, leiserer
Filter: gefragt wurden nur drei Sprachen. Fürs automatische Holen ist
das richtig — dort wird eines genommen, und es soll passen. Wer von Hand
wählt, will sehen, was es gibt; sortiert wird ohnehin nach Sprache, also
steht das Passende weiterhin oben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-12 20:49:37 +02:00
co-authored by Claude Opus 5
parent 78e347c58d
commit bfe263d495
3 changed files with 94 additions and 7 deletions
+19 -3
View File
@@ -420,6 +420,9 @@ public sealed class TmdbClient(
/// Schriftzug weggeworfen. Die Auswahl kostet also keine zusätzliche
/// Anfrage.
/// </remarks>
/// <summary>Wie viele Bilder je Art höchstens zur Wahl stehen.</summary>
private const int HöchstensBilderJeArt = 200;
public async Task<IReadOnlyList<Bildkandidat>> BilderAsync(
int providerId, bool serie, CancellationToken ct = default)
{
@@ -428,9 +431,16 @@ public sealed class TmdbClient(
var sprache = Sprachkürzel(einstellung.Language);
var weg = serie ? "tv" : "movie";
// <b>Ohne Sprachfilter.</b> Hier stand
// <c>include_image_language={sprache},en,null</c> — drei von
// vierzig Sprachen, und alles andere kam gar nicht erst an. Für das
// automatische Holen ist das richtig: dort wird eines genommen, und
// es soll das passende sein. Hier wählt ein Mensch, und der will
// sehen, was es gibt. Die Reihenfolge sortiert ohnehin nach
// Sprache, also steht das Passende weiterhin oben.
var antwort = await HolenAsync<TmdbBilderAntwort>(
$"{weg}/{providerId}/images?include_image_language={Uri.EscapeDataString(sprache)},en,null",
einstellung, ct);
$"{weg}/{providerId}/images", einstellung, ct);
if (antwort is null) return [];
@@ -448,7 +458,13 @@ public sealed class TmdbClient(
foreach (var b in (bilder ?? []).Where(b => !string.IsNullOrWhiteSpace(b.FilePath))
.OrderBy(b => Sprachrang(b.Language, sprache))
.ThenByDescending(b => b.VoteAverage ?? 0)
.Take(24))
// <b>Nicht vierundzwanzig.</b> Bei „365 Tage" hat
// TMDB neunundsechzig Plakate, und die Liste war
// nach vierundzwanzig zu Ende — ohne dass irgendwo
// stünde, dass da noch mehr ist. Die Grenze bleibt
// als Schutz vor einer Seite mit tausend Bildern,
// liegt aber dort, wo sie niemanden mehr behindert.
.Take(HöchstensBilderJeArt))
{
liste.Add(new Bildkandidat(
art, Bild(b.FilePath, groesse)!.Url,
@@ -28,6 +28,50 @@ public class TmdbClientTests
Assert.That(attrappe.Aufrufe, Is.Empty, "Ohne Schlüssel darf keine Anfrage hinausgehen");
}
/// <summary>Die Bilderliste für die Auswahl von Hand.</summary>
///
/// <remarks>
/// <b>Zwei Grenzen, die beide störten.</b> Die Anfrage holte nur drei
/// Sprachen, und gezählt wurde bei vierundzwanzig je Art zu Ende — bei
/// „365 Tage" hat TMDB neunundsechzig Plakate, und die fehlenden
/// fünfundvierzig sah niemand. Wer von Hand wählt, soll sehen, was es
/// gibt.
/// </remarks>
[Test]
public async Task Die_Bilderliste_ist_nicht_bei_vierundzwanzig_zu_Ende()
{
var plakate = string.Join(",", Enumerable.Range(0, 69)
.Select(i => $$"""{"file_path":"/p{{i}}.jpg","iso_639_1":"en","vote_average":1.0}"""));
var attrappe = Attrappe.MitJson($$"""{"posters":[{{plakate}}],"backdrops":[],"logos":[]}""");
var bilder = await Client(attrappe).BilderAsync(603, serie: false);
Assert.Multiple(() =>
{
Assert.That(bilder.Count(b => b.Kind == "poster"), Is.EqualTo(69));
Assert.That(attrappe.Aufrufe[0], Does.Not.Contain("include_image_language"),
"Die Auswahl von Hand soll alle Sprachen sehen");
});
}
/// <summary>Die passende Sprache steht trotzdem oben.</summary>
[Test]
public async Task Die_eigene_Sprache_steht_vorn()
{
var attrappe = Attrappe.MitJson("""
{"posters":[
{"file_path":"/en.jpg","iso_639_1":"en","vote_average":9.0},
{"file_path":"/de.jpg","iso_639_1":"de","vote_average":1.0},
{"file_path":"/ja.jpg","iso_639_1":"ja","vote_average":8.0}],
"backdrops":[],"logos":[]}
""");
var bilder = await Client(attrappe, FesteEinstellungen.MitSchluessel("de-DE"))
.BilderAsync(603, serie: false);
Assert.That(bilder[0].Url, Does.Contain("/de.jpg"));
}
[Test]
public async Task Ein_Film_wird_gelesen()
{
+31 -4
View File
@@ -19,6 +19,7 @@ import {
} from '../sprachwahl';
import { menueAnZiel } from '../menue';
import { meldedialog } from '../melden';
import { bearbeiten } from '../bearbeiten';
type Art = 'filme' | 'serien';
@@ -475,10 +476,36 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
}));
}
// **Nicht über `rundKnopf`.** Der schaltet sich beim Klick ab und erst im
// Fehlerfall wieder an — richtig für etwas, das danach die Seite neu lädt.
// Ein Dialog kehrt sofort zurück; der Knopf bliebe für den Rest der Seite
// tot.
// **Bearbeiten — und damit die Bilder — von hier aus erreichbar.**
//
// Den Dialog gab es längst, aber nur am Dreipunktmenü einer Kachel und
// in zwei Einstellungsseiten. Wer auf der Seite eines Films steht und
// sieht, dass das Plakat falsch ist, stand genau dort, wo er nichts
// dagegen tun konnte.
//
// **Nur im Kopf der Seite** (daran erkennbar, dass `melden` gesetzt ist):
// dieselbe Reihe steht über jeder Fassung und jeder Folge, und dort
// meinte der Knopf jedes Mal denselben Titel.
//
// Wie der Melden-Knopf nicht über `rundKnopf`: der schaltet sich beim
// Klick ab und erst im Fehlerfall wieder an — richtig für etwas, das
// danach neu lädt, falsch für einen Dialog, der sofort zurückkehrt.
if (melden && istAdmin()) {
const b = document.createElement('button');
b.className = 'rund';
b.innerHTML = icons.stift;
b.title = 'Bearbeiten';
b.setAttribute('aria-label', 'Bearbeiten');
b.onclick = () => bearbeiten(
art === 'serien' ? 'series' : 'movies', eintrag.id,
ergebnis => {
// Beim Zusammenlegen überlebt eine andere Kennung — dann führt
// diese Seite ins Leere, und zurück ist die ehrlichere Antwort.
if (ergebnis.verschmolzen) zurueck(); else void laden();
});
reihe.appendChild(b);
}
if (melden) {
const m = document.createElement('button');
m.className = 'rund';