Der Ordner bestimmt die Serie, nicht der Dateiname

Aus einer Serie wurden siebenhundert. Dateien wie "S01E001_Aang.mkv"
tragen vor der Folgennummer nichts, also wurde der ganze Dateiname zum
Serientitel — jede Folge eine eigene Serie. Und umgekehrt ergab
"Doctor Who (2005) - S12E04.mkv" im Ordner "Doctor Who" zwei Serien, weil
die Jahresangabe nur im Dateinamen stand.

Jetzt gilt: was unter Serien/Pingu/ liegt, gehört zu Pingu. Egal wie tief,
egal wie die Datei heisst. Der Ordner ist die verlässlichere Angabe, weil
ihn ein Mensch angelegt hat; so macht es Plex seit je und aus demselben
Grund. Staffel und Folge liest weiterhin der Dateiname.

Ohne bekannte Wurzel bleibt es beim Dateinamen — dort könnte über dem
Staffelordner irgendetwas stehen. Diesen Weg gehen nur die Tests.

Dazu vier Dinge aus der Benutzung:

Die Mediatheken standen scheinbar in zufälliger Reihenfolge. SQLite
vergleicht Text zeichengenau, und dort stehen alle Grossbuchstaben vor
allen kleinen: "Serien" landete über "film". Jetzt ohne Rücksicht auf
Gross- und Kleinschreibung — und umsortierbar über zwei Pfeile. Nicht über
Ziehen: das ist schöner, aber mit der Tastatur unbedienbar und auf dem
Telefon ein Glücksspiel.

Die Sprache des Servers steckte im Dialog "Metadaten", zusammen mit dem
TMDB-Schlüssel — also dort, wo niemand nach einer Sprache sucht. Wer die
Mediathek auf Englisch vorfand, sah nur "Meine Sprache" und hielt das für
die einzige Möglichkeit. Jetzt eine eigene Zeile.

Das Kontextmenü gab es nur per Rechtsklick, und den findet nur, wer ihn
vermutet — auf dem Telefon gibt es ihn gar nicht. Jetzt drei Punkte auf
jeder Kachel, beim Überfahren und bei Tastaturfokus, auf Geräten ohne
Zeiger dauerhaft. Und "Zuordnung korrigieren" steht endlich darin: wer
zwanzig falsch erkannte Titel vor sich hat, soll sie dort geradeziehen
können statt über vierzig Seitenwechsel.

Folgen ohne eigenes Bild nehmen das der Serie. TMDB liefert Folgenbilder
nur für einen Teil der Folgen und bei kleineren Serien für gar keine; eine
graue Fläche sagt nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 14:20:00 +02:00
co-authored by Claude Opus 5
parent fa18c0d7fa
commit 98565e14b5
19 changed files with 1509 additions and 50 deletions
+104
View File
@@ -0,0 +1,104 @@
import { vorschlaege, zuordnen } from './api';
import { esc, knopf, feld, fehlerzeile, leer } from './util';
import { icons } from './icons';
/** Einen Titel einem anderen Eintrag der Metadatenquelle zuordnen.
*
* **Als eigener Dialog und nicht nur auf der Detailseite.** Wer in der
* Mediathek zwanzig falsch erkannte Titel vor sich hat, soll sie dort
* geradeziehen können — vierzig Seitenwechsel sind keine Bedienung.
*
* `fertig` wird gerufen, wenn etwas zugeordnet wurde; der Aufrufer frischt
* damit auf, was er zeigt.
*/
export function zuordnungsdialog(
art: 'movies' | 'series',
id: string,
titel: string,
jahr: number | null,
fertig?: () => void,
): void {
const d = document.createElement('dialog');
d.className = 'dlg dlg-breit';
const kopf = document.createElement('div');
kopf.className = 'dlg-kopf';
const h = document.createElement('h2');
h.textContent = 'Zuordnung korrigieren';
const zu = document.createElement('button');
zu.className = 'dlg-zu';
zu.innerHTML = icons.schliessen;
zu.setAttribute('aria-label', 'Schliessen');
zu.onclick = () => d.close();
kopf.append(h, zu);
const koerper = document.createElement('div');
koerper.className = 'dlg-koerper';
const hinweis = document.createElement('p');
hinweis.className = 'hinweis';
hinweis.textContent = 'Der gewählte Eintrag ersetzt Titel, Beschreibung, Bilder '
+ 'und Besetzung. Von Hand gewählte Bilder bleiben.';
const suche = feld('Anderer Suchbegriff');
suche.value = titel;
const jahrfeld = feld('Jahr', 'number');
if (jahr) jahrfeld.value = String(jahr);
const suchen = knopf('Suchen', 'btn');
const reihe = document.createElement('div');
reihe.className = 'knopfreihe';
reihe.append(suche, jahrfeld, suchen);
const treffer = document.createElement('div');
treffer.className = 'einstellungen';
koerper.append(hinweis, reihe, treffer);
d.append(kopf, koerper);
d.addEventListener('close', () => d.remove());
d.addEventListener('click', e => { if (e.target === d) d.close(); });
// Enter im Suchfeld sucht, statt den Dialog zu schliessen.
suche.onkeydown = e => { if (e.key === 'Enter') { e.preventDefault(); void fragen(); } };
document.body.appendChild(d);
d.showModal();
suchen.onclick = () => void fragen();
void fragen();
async function fragen(): Promise<void> {
treffer.innerHTML = '<div class="spinner"></div>';
try {
const liste = await vorschlaege(art, id,
suche.value.trim(), jahrfeld.value ? Number(jahrfeld.value) : undefined);
treffer.innerHTML = '';
if (liste.length === 0) { treffer.appendChild(leer('Nichts gefunden.')); return; }
liste.forEach(v => {
const zeile = document.createElement('button');
zeile.className = 'einstellung waehlbar';
zeile.innerHTML =
`<div class="zeile"><strong>${esc(v.title)}</strong>`
+ `<span class="winzig">${esc(v.year ?? '')} · TMDB ${v.tmdbId}</span></div>`
+ `<div class="hinweis">${esc((v.overview ?? '').slice(0, 160))}</div>`;
zeile.onclick = async () => {
zeile.disabled = true;
try {
await zuordnen(art, id, v.tmdbId);
d.close();
fertig?.();
} catch (fehler) {
treffer.appendChild(fehlerzeile((fehler as Error).message));
zeile.disabled = false;
}
};
treffer.appendChild(zeile);
});
} catch (fehler) {
treffer.innerHTML = '';
treffer.appendChild(fehlerzeile((fehler as Error).message));
}
}
}