Eine Sekunde ist kein Anfang
Drei Zahlen standen als Konstante im Quelltext und bestimmten, wie sich der Server anfühlt. Jetzt stehen sie in der Verwaltung, unter „Wiedergabe". **Weiterschauen ab.** Gemessen: `BelongsInContinueWatching` verlangte nur `Position.Ticks > 0` — ein Tick genügte. Wer im Aufmacher eine Sekunde hineinschaute, fand den Titel danach in „Fortsetzen". Jetzt gibt es eine Untergrenze, voreingestellt auf 1½ Minuten. Was sich dabei **nicht** ändert: die Stelle wird weiter gemerkt. Das ist der Unterschied zu Jellyfin, um dessentwillen es diesen Server gibt — die Schwelle entscheidet, ob der Titel in der Reihe steht, nicht ob man ihn wiederfindet. Nachgemessen: nach dem Umstellen ist „Heat" aus der Reihe verschwunden und steht weiterhin bei Sekunde 1. **Gesehen ab.** War 90 % als `const`, ist jetzt 85/90/95. **Umkodieren.** `-preset veryfast -crf 21` und VAAPI `-qp 23` waren fest verdrahtet, ohne Einstellung und ohne Umgebungsvariable. Drei Stufen: Qualität (medium/19, qp 21), Ausgewogen (wie bisher), Tempo (ultrafast/23, qp 26). Mit dem Warnsatz daneben, und der ist ernst gemeint: was in Echtzeit laufen muss, darf nicht langsamer rechnen als es abspielt. **Und das Vorhandene wird neu bewertet.** `InProgress` ist eine gespeicherte Spalte — die Bruchrechnung lässt sich nicht nach SQL übersetzen. Eine neue Schwelle wirkte sonst nur auf künftige Meldungen. Der Endpunkt läuft deshalb einmal in Stapeln durch und meldet zurück, wie viele Zeilen das umgeworfen hat: „Übernommen — 2 Einträge wurden neu bewertet." Ohne diesen Satz änderte sich die Startseite scheinbar von selbst. `ProgressRule` bleibt ein reiner Typ: die Regeln kommen als Wert herein. Nebenbei zieht der doppelte `zeile()`-Bauer nach `teile.ts` — er stand zweimal wortgleich da. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1e4a231d1b
commit
9622da0284
@@ -4,6 +4,7 @@ import { dashboard, serverbereich, speicherbereich } from './uebersicht';
|
||||
import { bibliotheksbereich, vermisstenbereich } from './mediatheken';
|
||||
import { pruefbereich } from './pruefung';
|
||||
import { ohneZuordnungBereich, unzugeordnetbereich } from './zuordnung';
|
||||
import { wiedergabebereich } from './wiedergabe';
|
||||
import { sprachbereich } from './metadaten';
|
||||
import { benutzerbereich } from './benutzer';
|
||||
import { protokollbereich } from './protokolle';
|
||||
@@ -11,13 +12,14 @@ import { protokollbereich } from './protokolle';
|
||||
/** Welcher Teil der Verwaltung offen ist. */
|
||||
export type Bereich =
|
||||
| 'uebersicht' | 'mediatheken' | 'pruefung' | 'zuordnung'
|
||||
| 'metadaten' | 'benutzer' | 'protokolle';
|
||||
| 'wiedergabe' | 'metadaten' | 'benutzer' | 'protokolle';
|
||||
|
||||
const BEREICHE: { wert: Bereich; text: string; bild: IconName }[] = [
|
||||
{ wert: 'uebersicht', text: 'Übersicht', bild: 'verwaltung' },
|
||||
{ wert: 'mediatheken', text: 'Mediatheken', bild: 'filme' },
|
||||
{ wert: 'pruefung', text: 'Zu prüfen', bild: 'hakenKreis' },
|
||||
{ wert: 'zuordnung', text: 'Zuordnung', bild: 'suche' },
|
||||
{ wert: 'wiedergabe', text: 'Wiedergabe', bild: 'stufen' },
|
||||
{ wert: 'metadaten', text: 'Metadaten', bild: 'stift' },
|
||||
{ wert: 'benutzer', text: 'Benutzer', bild: 'person' },
|
||||
{ wert: 'protokolle', text: 'Protokolle', bild: 'protokoll' },
|
||||
@@ -89,6 +91,8 @@ export function verwaltung(
|
||||
}
|
||||
case 'zuordnung':
|
||||
return [ohneZuordnungBereich(), unzugeordnetbereich()];
|
||||
case 'wiedergabe':
|
||||
return [wiedergabebereich()];
|
||||
case 'metadaten':
|
||||
return [sprachbereich()];
|
||||
case 'benutzer':
|
||||
|
||||
@@ -2,11 +2,10 @@ import {
|
||||
sprachen, spracheSetzen, uebersetzungenHolen, meineSprache, metadatenEinstellung,
|
||||
metadatenSetzen, sprachverteilung, abweichendeAuffrischen,
|
||||
} from '../../api';
|
||||
import { esc, knopf, fehlerzeile, meldung } from '../../util';
|
||||
import { knopf, fehlerzeile, meldung } from '../../util';
|
||||
import { dialog } from '../../dialog';
|
||||
import { icon } from '../../icons';
|
||||
|
||||
import { abschnitt } from './teile';
|
||||
import { abschnitt, zeile } from './teile';
|
||||
|
||||
/** Sprache und Herkunft der Metadaten. */
|
||||
|
||||
@@ -184,35 +183,5 @@ export function sprachbereich(): HTMLElement {
|
||||
|
||||
return bereich;
|
||||
|
||||
function zeile(
|
||||
name: string, wert: string, hinweis: string | null, bearbeiten?: () => void,
|
||||
): HTMLElement {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'einstellung';
|
||||
|
||||
const oben = document.createElement('div');
|
||||
oben.className = 'knopfreihe';
|
||||
const text = document.createElement('div');
|
||||
text.className = 'zeile';
|
||||
text.style.flex = '1';
|
||||
text.innerHTML = `<strong>${esc(name)}</strong><span class="winzig">${esc(wert)}</span>`;
|
||||
oben.appendChild(text);
|
||||
|
||||
if (bearbeiten) {
|
||||
const b = knopf('Ändern', 'btn');
|
||||
b.prepend(icon('stift'));
|
||||
b.onclick = bearbeiten;
|
||||
oben.appendChild(b);
|
||||
}
|
||||
d.appendChild(oben);
|
||||
|
||||
if (hinweis) {
|
||||
const h = document.createElement('div');
|
||||
h.className = 'hinweis';
|
||||
h.textContent = hinweis;
|
||||
d.appendChild(h);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { esc, knopf } from '../../util';
|
||||
import { icon } from '../../icons';
|
||||
|
||||
|
||||
/** Gemeinsame Bausteine der Verwaltung. */
|
||||
|
||||
@@ -18,3 +21,40 @@ export function abschnitt(titel: string, knoepfe: HTMLElement[] = []): HTMLEleme
|
||||
return kopf;
|
||||
}
|
||||
|
||||
|
||||
/** Eine Zeile „Name — Wert" mit einem Ändern-Knopf.
|
||||
*
|
||||
* **Eine Stelle, nicht zwei.** Diesen Bauer gab es wortgleich zweimal — in
|
||||
* der Sprach- und in der Serverauskunft. Zwei Kopien derselben Zeile sind
|
||||
* genau dann ein Problem, wenn eine davon gepflegt wird.
|
||||
*/
|
||||
export function zeile(
|
||||
name: string, wert: string, hinweis: string | null, bearbeiten?: () => void,
|
||||
): HTMLElement {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'einstellung';
|
||||
|
||||
const oben = document.createElement('div');
|
||||
oben.className = 'knopfreihe';
|
||||
const text = document.createElement('div');
|
||||
text.className = 'zeile';
|
||||
text.style.flex = '1';
|
||||
text.innerHTML = `<strong>${esc(name)}</strong><span class="winzig">${esc(wert)}</span>`;
|
||||
oben.appendChild(text);
|
||||
|
||||
if (bearbeiten) {
|
||||
const b = knopf('Ändern', 'btn');
|
||||
b.prepend(icon('stift'));
|
||||
b.onclick = bearbeiten;
|
||||
oben.appendChild(b);
|
||||
}
|
||||
d.appendChild(oben);
|
||||
|
||||
if (hinweis) {
|
||||
const h = document.createElement('div');
|
||||
h.className = 'hinweis';
|
||||
h.textContent = hinweis;
|
||||
d.appendChild(h);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ import {
|
||||
} from '../../api';
|
||||
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../../util';
|
||||
import { dialog } from '../../dialog';
|
||||
import { icon } from '../../icons';
|
||||
|
||||
import { abschnitt } from './teile';
|
||||
import { abschnitt, zeile } from './teile';
|
||||
|
||||
/** Übersicht: was läuft, was der Server meldet, was er belegt. */
|
||||
|
||||
@@ -218,36 +217,6 @@ export function serverbereich(geaendert: () => void): HTMLElement {
|
||||
}
|
||||
})();
|
||||
|
||||
function zeile(
|
||||
name: string, wert: string, hinweis: string | null, bearbeiten?: () => void,
|
||||
): HTMLElement {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'einstellung';
|
||||
|
||||
const oben = document.createElement('div');
|
||||
oben.className = 'knopfreihe';
|
||||
const text = document.createElement('div');
|
||||
text.className = 'zeile';
|
||||
text.style.flex = '1';
|
||||
text.innerHTML = `<strong>${esc(name)}</strong><span class="winzig">${esc(wert)}</span>`;
|
||||
oben.appendChild(text);
|
||||
|
||||
if (bearbeiten) {
|
||||
const b = knopf('Ändern', 'btn');
|
||||
b.prepend(icon('stift'));
|
||||
b.onclick = bearbeiten;
|
||||
oben.appendChild(b);
|
||||
}
|
||||
d.appendChild(oben);
|
||||
|
||||
if (hinweis) {
|
||||
const h = document.createElement('div');
|
||||
h.className = 'hinweis';
|
||||
h.textContent = hinweis;
|
||||
d.appendChild(h);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
liste.innerHTML = '<div class="spinner"></div>';
|
||||
return bereich;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { wiedergabeEinstellung, wiedergabeSetzen } from '../../api';
|
||||
import { fehlerzeile, meldung, hinweisZeigen } from '../../util';
|
||||
import { dialog } from '../../dialog';
|
||||
|
||||
import { abschnitt, zeile } from './teile';
|
||||
|
||||
/** Wie der Server mit Fortschritt umgeht und wie er umkodiert. */
|
||||
|
||||
/** Die Stufen fürs Weiterschauen — lieber eine Auswahl als ein Zahlenfeld.
|
||||
*
|
||||
* **„87 Sekunden" will niemand eintippen.** Eine freie Zahl sieht nach
|
||||
* Genauigkeit aus, die es hier nicht gibt: ob 90 oder 100 Sekunden, merkt
|
||||
* kein Mensch. Fünf Stufen beantworten die Frage, um die es geht — zählt
|
||||
* Hineinschauen oder nicht.
|
||||
*/
|
||||
const SCHWELLEN: { wert: string; text: string }[] = [
|
||||
{ wert: '0', text: 'Sofort — jede angefangene Sekunde' },
|
||||
{ wert: '30', text: 'Nach 30 Sekunden' },
|
||||
{ wert: '90', text: 'Nach 1½ Minuten' },
|
||||
{ wert: '180', text: 'Nach 3 Minuten' },
|
||||
{ wert: '300', text: 'Nach 5 Minuten' },
|
||||
];
|
||||
|
||||
const ANTEILE: { wert: string; text: string }[] = [
|
||||
{ wert: '0.85', text: '85 % — früher abgehakt' },
|
||||
{ wert: '0.9', text: '90 %' },
|
||||
{ wert: '0.95', text: '95 % — erst im Abspann' },
|
||||
];
|
||||
|
||||
const STUFEN: { wert: string; text: string }[] = [
|
||||
{ wert: 'qualitaet', text: 'Qualität — schöneres Bild, rechnet länger' },
|
||||
{ wert: 'ausgewogen', text: 'Ausgewogen' },
|
||||
{ wert: 'tempo', text: 'Tempo — startet schneller, kostet Bild' },
|
||||
];
|
||||
|
||||
export function wiedergabebereich(): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.appendChild(abschnitt('Wiedergabe'));
|
||||
|
||||
const liste = document.createElement('div');
|
||||
liste.className = 'einstellungen';
|
||||
liste.innerHTML = '<div class="spinner"></div>';
|
||||
bereich.appendChild(liste);
|
||||
|
||||
void laden();
|
||||
return bereich;
|
||||
|
||||
async function laden(): Promise<void> {
|
||||
let stand;
|
||||
try {
|
||||
stand = await wiedergabeEinstellung();
|
||||
} catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile(meldung(fehler)));
|
||||
return;
|
||||
}
|
||||
|
||||
liste.innerHTML = '';
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Weiterschauen ab',
|
||||
textVon(SCHWELLEN, String(stand.resumeAfterSeconds)),
|
||||
'Wer kürzer hineinschaut, findet den Titel nicht in „Fortsetzen" wieder. '
|
||||
+ 'Die Stelle wird trotzdem gemerkt — wer ihn später öffnet, macht dort weiter.',
|
||||
() => fragen({
|
||||
titel: 'Weiterschauen ab',
|
||||
text: 'Ohne Untergrenze genügt eine Sekunde: ein Blick in den Aufmacher, '
|
||||
+ 'und der Titel steht in „Fortsetzen". Die Umstellung wirkt rückwirkend — '
|
||||
+ 'was schon in der Reihe steht, wird neu bewertet.',
|
||||
name: 'schwelle',
|
||||
beschriftung: 'Erst ab',
|
||||
auswahl: SCHWELLEN,
|
||||
wert: naechsteStufe(SCHWELLEN, stand.resumeAfterSeconds),
|
||||
setzen: v => wiedergabeSetzen({ resumeAfterSeconds: Number(v) }),
|
||||
})));
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Gesehen ab',
|
||||
`${Math.round(stand.watchedFraction * 100)} %`,
|
||||
'Niemand schaut bis zur letzten Sekunde des Abspanns. Ab diesem Anteil '
|
||||
+ 'bekommt ein Titel sein Häkchen und verlässt „Fortsetzen".',
|
||||
() => fragen({
|
||||
titel: 'Gesehen ab',
|
||||
text: 'Höher heisst: der Titel bleibt länger in „Fortsetzen" stehen. '
|
||||
+ 'Auch das wirkt rückwirkend.',
|
||||
name: 'anteil',
|
||||
beschriftung: 'Anteil der Laufzeit',
|
||||
auswahl: ANTEILE,
|
||||
wert: naechsteStufe(ANTEILE, stand.watchedFraction),
|
||||
setzen: v => wiedergabeSetzen({ watchedFraction: Number(v) }),
|
||||
})));
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Umkodieren',
|
||||
textVon(STUFEN, stand.encoder),
|
||||
'Gilt nur, wenn wirklich gerechnet werden muss — wird eine Datei bloss '
|
||||
+ 'umgepackt, bleibt das Bild unberührt. Vorsicht bei „Qualität": was in '
|
||||
+ 'Echtzeit laufen muss, darf nicht langsamer rechnen als es abspielt. '
|
||||
+ 'Auf einer schwachen Maschine kann das Bild dadurch stocken.',
|
||||
() => fragen({
|
||||
titel: 'Umkodieren',
|
||||
text: 'Die Stufe wirkt auf die nächste Wiedergabe, nicht auf eine laufende.',
|
||||
name: 'stufe',
|
||||
beschriftung: 'Abwägung',
|
||||
auswahl: STUFEN,
|
||||
wert: stand.encoder,
|
||||
setzen: v => wiedergabeSetzen({ encoder: v }),
|
||||
})));
|
||||
}
|
||||
|
||||
/** Ein Auswahldialog, der danach meldet, was er umgeworfen hat. */
|
||||
function fragen(o: {
|
||||
titel: string; text: string; name: string; beschriftung: string;
|
||||
auswahl: { wert: string; text: string }[]; wert: string;
|
||||
setzen: (wert: string) => Promise<{ reevaluated: number }>;
|
||||
}): void {
|
||||
dialog({
|
||||
titel: o.titel,
|
||||
text: o.text,
|
||||
felder: [{ name: o.name, beschriftung: o.beschriftung, auswahl: o.auswahl, wert: o.wert }],
|
||||
bestaetigen: 'Übernehmen',
|
||||
tun: async wert => {
|
||||
const ergebnis = await o.setzen(wert(o.name));
|
||||
// **Die Zahl gehört in die Rückmeldung.** Eine neue Schwelle räumt
|
||||
// „Fortsetzen" auf; ohne diesen Satz änderte sich die Startseite
|
||||
// scheinbar von selbst.
|
||||
hinweisZeigen(ergebnis.reevaluated === 0
|
||||
? 'Übernommen.'
|
||||
: ergebnis.reevaluated === 1
|
||||
? 'Übernommen — ein Eintrag wurde neu bewertet.'
|
||||
: `Übernommen — ${ergebnis.reevaluated} Einträge wurden neu bewertet.`);
|
||||
await laden();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Der Text zur gespeicherten Zahl — oder die Zahl selbst, wenn sie nicht passt. */
|
||||
function textVon(stufen: { wert: string; text: string }[], wert: string): string {
|
||||
return stufen.find(s => s.wert === wert)?.text ?? wert;
|
||||
}
|
||||
|
||||
/** Welche Stufe ein gespeicherter Wert am ehesten meint.
|
||||
*
|
||||
* In der Datenbank steht eine Zahl, keine Stufe — sie kann aus einer älteren
|
||||
* Fassung stammen oder von Hand gesetzt sein. Dann soll der Dialog nicht leer
|
||||
* aufgehen, sondern bei der nächstgelegenen stehen.
|
||||
*/
|
||||
function naechsteStufe(stufen: { wert: string; text: string }[], wert: number): string {
|
||||
let beste = stufen[0]!.wert;
|
||||
let abstand = Infinity;
|
||||
for (const s of stufen) {
|
||||
const d = Math.abs(Number(s.wert) - wert);
|
||||
if (d < abstand) { abstand = d; beste = s.wert; }
|
||||
}
|
||||
return beste;
|
||||
}
|
||||
Reference in New Issue
Block a user