diff --git a/server/src/Flaro.Metadata/TmdbClient.cs b/server/src/Flaro.Metadata/TmdbClient.cs
index a599888..1f63f77 100644
--- a/server/src/Flaro.Metadata/TmdbClient.cs
+++ b/server/src/Flaro.Metadata/TmdbClient.cs
@@ -420,6 +420,9 @@ public sealed class TmdbClient(
/// Schriftzug weggeworfen. Die Auswahl kostet also keine zusätzliche
/// Anfrage.
///
+ /// Wie viele Bilder je Art höchstens zur Wahl stehen.
+ private const int HöchstensBilderJeArt = 200;
+
public async Task> 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";
+
+ // Ohne Sprachfilter. Hier stand
+ // include_image_language={sprache},en,null — 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(
- $"{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))
+ // Nicht vierundzwanzig. 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,
diff --git a/server/tests/Flaro.Metadata.Tests/TmdbClientTests.cs b/server/tests/Flaro.Metadata.Tests/TmdbClientTests.cs
index ad6de17..37498a4 100644
--- a/server/tests/Flaro.Metadata.Tests/TmdbClientTests.cs
+++ b/server/tests/Flaro.Metadata.Tests/TmdbClientTests.cs
@@ -28,6 +28,50 @@ public class TmdbClientTests
Assert.That(attrappe.Aufrufe, Is.Empty, "Ohne Schlüssel darf keine Anfrage hinausgehen");
}
+ /// Die Bilderliste für die Auswahl von Hand.
+ ///
+ ///
+ /// Zwei Grenzen, die beide störten. 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.
+ ///
+ [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");
+ });
+ }
+
+ /// Die passende Sprache steht trotzdem oben.
+ [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()
{
diff --git a/web/app/src/views/detail.ts b/web/app/src/views/detail.ts
index 0907c49..715e99d 100644
--- a/web/app/src/views/detail.ts
+++ b/web/app/src/views/detail.ts
@@ -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';