Eine Reihe, die nur zu ihrer Zeit dasteht

Zu Halloween, zur Weihnachtszeit, zum Jahreswechsel, im Sommer — und zu
allem, was sonst jemand feiert.

**Nach Stichwörtern, nicht nach Genres.** „Stirb langsam" ist kein
Familienfilm und trägt trotzdem „christmas". Eine Weihnachtsreihe nach
Genres enthielte jeden Zeichentrickfilm und diesen Film nicht. TMDB führt
die Stichwörter ohnehin; sie kommen jetzt über `append_to_response` mit
und landen in einer eigenen Tabelle, nach demselben Muster wie Genres.
Kleingeschrieben abgelegt — TMDB liefert „Christmas" und „christmas" je
nach Eintrag, und zwei Schreibweisen wären zwei Stichwörter.

**Einstellbar und nicht eingebaut.** Halloween und Weihnachten kennt
jeder; der 1. August ist Schweizer Nationalfeiertag, der 4. Juli
amerikanischer, und der Geburtstag des Betreibers steht in keiner
Bibliothek. Eine feste Liste im Quelltext hiesse: für jeden Anlass eine
neue Fassung. Vier sind vorangelegt — als Beispiel, nicht als Meinung
darüber, was jemand feiert.

**Über den Jahreswechsel hinweg.** „28.12. bis 02.01." ist kein Fehler,
sondern der Normalfall für Silvester; ein Vergleich, der nur „von ≤ heute
≤ bis" kennt, fände diesen Zeitraum nie — und die Reihe stünde genau in
den Tagen nicht da, für die sie gedacht ist. Geprüft, Tag für Tag, samt
der Zusicherung, dass sich die vorangelegten Anlässe an keinem Tag im Jahr
überschneiden.

**Leer heisst leer.** Gilt heute kein Anlass oder findet er nichts, kommt
eine Reihe ohne Namen zurück und der Client zeigt gar keine — statt einer
mit Überschrift und nichts darunter. Scheitert der Abruf ganz, fehlt eine
Reihe und nicht die Startseite.

Bedient wird sie als Text, eine Zeile je Anlass: Name, von, bis,
Stichwörter, Genres. Ein Listeneditor mit Zeilen zum Hinzufügen,
Verschieben und Löschen wäre viel Oberfläche für etwas, das man dreimal im
Jahr anfasst — und es ist dieselbe Form, in der es gespeichert wird.

Ein Anlass ohne Stichwort und ohne Genre wird abgewiesen statt
stillschweigend geschluckt: er fände nie etwas, und wer ihn eingetragen
hat, suchte den Fehler woanders.

**Die Stichwörter kommen erst beim nächsten Auffrischen der Metadaten.**
Vorhandene Titel tragen noch keine — bis dahin bleibt die Reihe leer.

764 Tests grün, davon 22 neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-08 21:45:09 +02:00
co-authored by Claude Opus 5
parent c13d1e78d2
commit e066f5fe87
19 changed files with 2121 additions and 7 deletions
+17
View File
@@ -575,6 +575,23 @@ export const serie = (id: string) => anfrage<Serie>(`/series/${id}`);
/* ---------- Stand je Benutzer ---------- */
/** Die Reihe, die nur zu ihrer Zeit dasteht — `occasion` ist die Überschrift. */
export const saisonal = (take = 24) =>
anfrage<{ occasion: string | null; items: Seite<Film> }>(`/home/seasonal${abfrage({ take })}`);
/** Die saisonalen Anlässe — Name, Zeitraum, Stichwörter. */
export type Saisonanlass = {
name: string; von: string; bis: string;
stichworte: string[]; genres: string[];
};
export const saisonanlaesse = () => anfrage<Saisonanlass[]>('/settings/seasonal');
export const saisonanlaesseSetzen = (anlaesse: Saisonanlass[]) =>
anfrage<Saisonanlass[]>('/settings/seasonal', {
method: 'PUT', body: JSON.stringify(anlaesse),
});
export const weiterschauen = (take = 20) =>
anfrage<Seite<Angefangen>>(`/home/continue${abfrage({ take })}`);
+66 -1
View File
@@ -1,7 +1,8 @@
import {
doppelteZusammenlegen, metadatenEinstellung, metadatenSetzen, servernameSetzen, aufmacherhoeheSetzen,
systemauskunft, laufendeWiedergaben, serverzustand, bildAdresse, speicherorte,
bildspeicherLeeren, kratzplatzAufraeumen, type Wiedergabe,
bildspeicherLeeren, kratzplatzAufraeumen, saisonanlaesse, saisonanlaesseSetzen,
type Wiedergabe, type Saisonanlass,
} from '../../api';
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../../util';
import { dialog } from '../../dialog';
@@ -168,6 +169,46 @@ export function serverbereich(geaendert: () => void): HTMLElement {
tun: async wert => { await aufmacherhoeheSetzen(wert('stufe')); location.reload(); },
})));
/* ---------- Saisonale Reihen ---------- */
// **Eine Zeile je Anlass, als Text.** Ein Listeneditor mit Zeilen zum
// Hinzufügen, Verschieben und Löschen wäre viel Oberfläche für etwas,
// das man dreimal im Jahr anfasst. Vier Felder, durch Striche
// getrennt, kann man lesen und in zehn Sekunden ändern — und es ist
// dieselbe Form, in der es gespeichert wird.
const anlaesse = await saisonanlaesse().catch(() => []);
const heute = new Date();
const jetzt = `${String(heute.getMonth() + 1).padStart(2, '0')}-${String(heute.getDate()).padStart(2, '0')}`;
const laufend = anlaesse.find(a => gilt(a, jetzt));
liste.appendChild(zeile(
'Saisonale Reihen',
anlaesse.length === 0
? 'Keine'
: `${anlaesse.length} Anlässe` + (laufend ? ` · gerade: ${laufend.name}` : ' · gerade keiner'),
'Eine Reihe auf der Startseite, die nur zu ihrer Zeit dasteht. '
+ 'Gesucht wird nach Stichwörtern der Metadatenquelle — „Stirb langsam" '
+ 'ist kein Familienfilm und trägt trotzdem „christmas".',
() => dialog({
titel: 'Saisonale Reihen',
felder: [{
name: 'liste',
beschriftung: 'Ein Anlass je Zeile',
art: 'mehrzeilig',
zeilen: 8,
wert: anlaesse.map(alsZeile).join('\n'),
hinweis: 'Name | von | bis | Stichwörter | Genres — Datum als MM-TT, '
+ 'Stichwörter und Genres mit Komma getrennt. Ein Zeitraum darf über '
+ 'den Jahreswechsel gehen (12-28 bis 01-02). Genres sind freiwillig.',
}],
bestaetigen: 'Übernehmen',
tun: async wert => {
await saisonanlaesseSetzen(
wert('liste').split('\n').map(z => z.trim()).filter(Boolean).map(ausZeile));
location.reload();
},
})));
// **Wartung, nicht Alltag.** Nach jedem Scan passiert das von selbst;
// dieser Knopf ist für den Fall, dass doch wieder Dubletten auftauchen —
// etwa nachdem eine Zuordnung von Hand geändert wurde.
@@ -388,3 +429,27 @@ export function speicherbereich(): HTMLElement {
return bereich;
}
/* ---------- Saisonale Anlässe als Text ---------- */
/** „Zu Halloween | 10-01 | 11-01 | halloween, witch | Horror" */
function alsZeile(a: Saisonanlass): string {
return [a.name, a.von, a.bis, a.stichworte.join(', '), a.genres.join(', ')].join(' | ');
}
/** Und wieder zurück. Fehlende Felder bleiben leer — der Server weist ab,
* was damit unbrauchbar ist, statt es stillschweigend zu schlucken. */
function ausZeile(z: string): Saisonanlass {
const [name = '', von = '', bis = '', stichworte = '', genres = ''] = z.split('|').map(t => t.trim());
const teile = (t: string) => t.split(',').map(x => x.trim()).filter(Boolean);
return { name, von, bis, stichworte: teile(stichworte), genres: teile(genres) };
}
/** Ob ein Anlass heute gilt — dieselbe Regel wie auf dem Server.
*
* **Auch über den Jahreswechsel.** „28.12. bis 02.01." ist kein Fehler,
* sondern der Normalfall für Silvester. */
function gilt(a: Saisonanlass, heute: string): boolean {
const zahl = (t: string) => Number(t.replace('-', ''));
const [von, bis, nun] = [zahl(a.von), zahl(a.bis), zahl(heute)];
return von <= bis ? nun >= von && nun <= bis : nun >= von || nun <= bis;
}
+16 -2
View File
@@ -1,5 +1,5 @@
import {
inhalt, weiterschauen, naechsteFolgen, empfehlungen,
inhalt, weiterschauen, naechsteFolgen, empfehlungen, saisonal,
type Bibliothek,
} from '../api';
import { leer, fehlerzeile, meldung } from '../util';
@@ -39,10 +39,14 @@ export function startseite(
// **Alles auf einmal anfragen.** Bei fünf Mediatheken wären es sonst
// acht Anfragen nacheinander, und die Startseite baut sich vor den
// Augen des Benutzers Zeile für Zeile auf.
const [weiter, naechste, vorschlag, ...bestaende] = await Promise.all([
const [weiter, naechste, vorschlag, saison, ...bestaende] = await Promise.all([
weiterschauen(),
naechsteFolgen(),
empfehlungen(24).catch(() => ({ items: [], total: 0 })),
// **Scheitert sie, fehlt eine Reihe und nicht die Startseite.** Ein
// älterer Server kennt den Weg nicht; das darf nicht dazu führen,
// dass gar nichts erscheint.
saisonal(24).catch(() => ({ occasion: null, items: { items: [], total: 0 } })),
...bibliotheken.map(b => inhalt(b, { sort: 'added', take: 24 })),
]);
@@ -100,6 +104,16 @@ export function startseite(
naechste.items.map(e => kachel(angefangenAlsKachel(e), oeffnen))));
}
// **Weit oben, weil sie nur jetzt gilt.** „Zu Halloween" unter den
// Neuzugängen wäre im November verschwunden, ohne dass jemand sie
// gesehen hat. Ohne Anlass oder ohne Treffer kommt `occasion` leer
// zurück — dann steht hier gar keine Reihe statt einer mit
// Überschrift und nichts darunter.
if (saison.occasion && saison.items.items.length) {
wurzel.appendChild(reihe(saison.occasion,
saison.items.items.map(e => kachel(alsKachel(e), oeffnen))));
}
let etwasDa = false;
bibliotheken.forEach((b, i) => {
const seite = bestaende[i];