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:
co-authored by
Claude Opus 5
parent
fa18c0d7fa
commit
98565e14b5
+100
-17
@@ -4,6 +4,7 @@ import {
|
||||
vermisste, vermisstesEntfernen, type VermissteDatei,
|
||||
protokolltage, protokollAdresse, sprachen, spracheSetzen, uebersetzungenHolen,
|
||||
meineSprache, zuPruefen, veroeffentlichen, zurueckziehen, pruefungSetzen,
|
||||
mediathekFreigeben, reihenfolgeSetzen,
|
||||
type Pruefeintrag,
|
||||
benutzer, benutzerAnlegen, metadatenEinstellung, metadatenSetzen,
|
||||
servernameSetzen, systemauskunft,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
} from '../api';
|
||||
import { esc, dauer, knopf, fehlerzeile, leer } from '../util';
|
||||
import { dialog, nachfragen } from '../dialog';
|
||||
import { icon } from '../icons';
|
||||
import { icon, icons } from '../icons';
|
||||
|
||||
/** Die Verwaltung.
|
||||
*
|
||||
@@ -519,6 +520,22 @@ function vermisstenbereich(geaendert: () => void): HTMLElement {
|
||||
/* ---------- Bibliotheken ---------- */
|
||||
|
||||
function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLElement {
|
||||
/** Die Liste, wie sie gerade steht — für das Umsortieren. */
|
||||
let alleBibliotheken: Bibliothek[] = [];
|
||||
|
||||
/** Schiebt einen Eintrag und schickt die ganze neue Reihenfolge. */
|
||||
async function schieben(von: number, nach: number): Promise<void> {
|
||||
if (nach < 0 || nach >= alleBibliotheken.length) return;
|
||||
|
||||
const neu = [...alleBibliotheken];
|
||||
const [eintrag] = neu.splice(von, 1);
|
||||
neu.splice(nach, 0, eintrag!);
|
||||
|
||||
await reihenfolgeSetzen(neu.map(b => b.id));
|
||||
await laden();
|
||||
geaendert();
|
||||
}
|
||||
|
||||
const bereich = document.createElement('section');
|
||||
|
||||
const anlegen = knopf('Mediathek hinzufügen', 'btn btn-primary');
|
||||
@@ -571,14 +588,15 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
liste.appendChild(leer('Noch keine Mediathek. Lege oben eine an.'));
|
||||
return;
|
||||
}
|
||||
alle.forEach(b => liste.appendChild(block(b)));
|
||||
alleBibliotheken = alle;
|
||||
alle.forEach((b, i) => liste.appendChild(block(b, i)));
|
||||
} catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile((fehler as Error).message));
|
||||
}
|
||||
}
|
||||
|
||||
function block(b: Bibliothek): HTMLElement {
|
||||
function block(b: Bibliothek, platz: number): HTMLElement {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'einstellung';
|
||||
|
||||
@@ -589,10 +607,16 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
? (serie ? 'Serie' : 'Film')
|
||||
: (serie ? 'Serien' : 'Filme'));
|
||||
|
||||
// Was wartet, gehört in dieselbe Zeile — sonst erfährt man es nur, wenn
|
||||
// man bis zur Prüfliste weiterscrollt.
|
||||
const wartet = b.pendingCount > 0
|
||||
? ` · ${b.pendingCount} wartet${b.pendingCount === 1 ? '' : 'en'} auf Freigabe`
|
||||
: '';
|
||||
|
||||
const kopf = document.createElement('div');
|
||||
kopf.className = 'zeile';
|
||||
kopf.innerHTML = `<strong>${esc(b.name)}</strong>`
|
||||
+ `<span class="winzig">${esc(b.path)}${esc(anzahl)}</span>`;
|
||||
+ `<span class="winzig">${esc(b.path)}${esc(anzahl)}${esc(wartet)}</span>`;
|
||||
d.appendChild(kopf);
|
||||
|
||||
const stand = document.createElement('div');
|
||||
@@ -685,9 +709,51 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
}
|
||||
};
|
||||
|
||||
// **Umsortieren über zwei Pfeile, nicht über Ziehen.**
|
||||
//
|
||||
// Ziehen und Ablegen ist schöner, aber mit der Tastatur unbedienbar und
|
||||
// auf dem Telefon ein Glücksspiel. Zwei Knöpfe kann jeder bedienen, und
|
||||
// der Server bekommt die ganze Liste statt eines „eins hoch" — zwei
|
||||
// Benutzer, die gleichzeitig schieben, kämen sonst in einen Zustand, den
|
||||
// keiner von beiden wollte.
|
||||
const hoch = knopf('', 'btn rund');
|
||||
hoch.innerHTML = icons.hoch;
|
||||
hoch.setAttribute('aria-label', `„${b.name}“ nach oben`);
|
||||
hoch.disabled = platz === 0;
|
||||
hoch.onclick = () => void schieben(platz, platz - 1);
|
||||
|
||||
const runter = knopf('', 'btn rund');
|
||||
runter.innerHTML = icons.hoch;
|
||||
runter.style.transform = 'rotate(180deg)';
|
||||
runter.setAttribute('aria-label', `„${b.name}“ nach unten`);
|
||||
runter.disabled = platz === alleBibliotheken.length - 1;
|
||||
runter.onclick = () => void schieben(platz, platz + 1);
|
||||
|
||||
const knoepfe = document.createElement('div');
|
||||
knoepfe.className = 'knopfreihe';
|
||||
knoepfe.append(los, auffrischen, pruefung, umbenennen, entfernen);
|
||||
knoepfe.append(hoch, runter, los, auffrischen, pruefung, umbenennen, entfernen);
|
||||
|
||||
// **Nur wenn hier wirklich etwas liegt.** Ein Knopf, der nichts zu tun
|
||||
// hat, ist schlechter als keiner — und die Zahl daneben sagt, worum es
|
||||
// geht, ohne dass man erst weiterscrollen muss.
|
||||
if (b.pendingCount > 0) {
|
||||
const freigeben = knopf(
|
||||
`${b.pendingCount} ${b.pendingCount === 1 ? 'Titel' : 'Titel'} freigeben`,
|
||||
'btn btn-primary');
|
||||
freigeben.onclick = async () => {
|
||||
freigeben.disabled = true;
|
||||
try {
|
||||
const bericht = await mediathekFreigeben(b.id);
|
||||
stand.textContent = `${bericht.published} freigegeben.`;
|
||||
await laden();
|
||||
geaendert();
|
||||
} catch (fehler) {
|
||||
d.appendChild(fehlerzeile((fehler as Error).message));
|
||||
freigeben.disabled = false;
|
||||
}
|
||||
};
|
||||
knoepfe.appendChild(freigeben);
|
||||
}
|
||||
d.append(knoepfe, stand);
|
||||
void verfolgen(false);
|
||||
return d;
|
||||
@@ -821,21 +887,38 @@ function serverbereich(geaendert: () => void): HTMLElement {
|
||||
platzhalter: 'Read Access Token oder API Key',
|
||||
hinweis: 'Leer lassen, um den mitgelieferten zu behalten.',
|
||||
},
|
||||
{
|
||||
name: 'sprache', beschriftung: 'Sprache der Metadaten',
|
||||
auswahl: sprachen, wert: stand.language,
|
||||
hinweis: 'In dieser Sprache werden Titel und Beschreibungen geholt. '
|
||||
+ 'Sie bestimmt auch, welche Altersfreigabe gilt. Vorhandene Titel '
|
||||
+ 'behalten ihre alte Sprache — dafür gibt es bei jeder Mediathek '
|
||||
+ 'den Knopf „Metadaten auffrischen".',
|
||||
},
|
||||
],
|
||||
bestaetigen: 'Speichern',
|
||||
tun: async wert => {
|
||||
await metadatenSetzen({
|
||||
apiKey: wert('schluessel') || undefined,
|
||||
language: wert('sprache') || undefined,
|
||||
});
|
||||
await metadatenSetzen({ apiKey: wert('schluessel') || undefined });
|
||||
location.reload();
|
||||
},
|
||||
})));
|
||||
|
||||
// **Die Sprache des Servers als eigene Zeile.**
|
||||
//
|
||||
// Sie 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.
|
||||
liste.appendChild(zeile(
|
||||
'Sprache des Servers',
|
||||
sprachen.find(x => x.wert === stand.language)?.text ?? stand.language,
|
||||
'In dieser Sprache werden Titel und Beschreibungen geholt, und sie '
|
||||
+ 'bestimmt die Altersfreigabe. Gilt für alle. Vorhandene Titel behalten '
|
||||
+ 'ihre alte Sprache — dafür gibt es bei jeder Mediathek „Metadaten '
|
||||
+ 'auffrischen".',
|
||||
() => dialog({
|
||||
titel: 'Sprache des Servers',
|
||||
text: 'Die Umstellung wirkt auf neue Funde sofort. Für den vorhandenen '
|
||||
+ 'Bestand drückst du danach bei jeder Mediathek „Metadaten auffrischen" '
|
||||
+ '— sonst behalten die Titel ihre alte Sprache.',
|
||||
felder: [{
|
||||
name: 'sprache', beschriftung: 'Sprache', auswahl: sprachen, wert: stand.language,
|
||||
}],
|
||||
bestaetigen: 'Übernehmen',
|
||||
tun: async wert => {
|
||||
await metadatenSetzen({ language: wert('sprache') });
|
||||
location.reload();
|
||||
},
|
||||
})));
|
||||
|
||||
Reference in New Issue
Block a user