Kratzplatz, Markenerkennung, Tonabdruck — erfundene Wörter für Fachbegriffe

„Scratch disk" wörtlich zu übersetzen ist derselbe Fehler wie „Wachhund"
für Watchdog und „Keks" für Cookie. Und „Marke" ist kein Fachbegriff für
das, was gemeint ist: Jellyfin nennt diese Abschnitte Media Segments, und
das ist auch das Wort, in dem sich darüber reden lässt.

    Kratzplatz*       → Transcode*          (was es ist, steht in FLARO_TRANSCODE)
    Marke, Marken     → Segment, Segmente
    Markenerkennung   → Segmenterkennung
    Markenart/-suche  → Segmentart/-suche
    Tonabdruck        → Fingerprint         (Chromaprint nennt es so)
    Tonabgleich       → Fingerprintabgleich
    Schlüsselbild     → Keyframe
    Bildabtaster      → Frameabtaster

Gewöhnliches Deutsch bleibt gewöhnliches Deutsch: Wartung, Freigabe,
Meldung, Abspann, Vorspann. Das war nie das Problem.

**Was sich nicht ändert, und zwar mit Absicht.** Die drei
Einstellungsschlüssel `wartung.kratzplatz.paket.*` bleiben, wie sie sind —
sie stehen in der Datenbank, und ein neuer Name hiesse, dass die Werte
des Nutzers stillschweigend auf die Vorgabe zurückfallen. Dasselbe für
die Endpunktpfade `/marken`: sie sind nicht sichtbar, und ein Umbenennen
wäre Diff ohne Gewinn.

**Die Migration ist von Hand geschrieben.** `dotnet ef` hat ein Gerüst
erzeugt, das beide Tabellen löscht und neu anlegt — es sieht eine
verschwundene und eine neue Tabelle, nicht eine umbenannte. Damit wäre
jedes erkannte Segment und jeder Vermerk „hier wurde schon gesucht" weg
gewesen. `RenameTable` und `RenameIndex` tun dasselbe, ohne eine Zeile
anzufassen; nachgemessen an einer echten SQLite-Datei: Zeilen vorher 1
und 1, nachher 1 und 1, Inhalt unverändert.

Nachgezogen: in `bilder.ts` und `main.ts` heisst „marke" ein Häkchen und
das Logo — die blieben unberührt. Und „Segment" ist sächlich, was eine
Blindersetzung nicht weiss: „eine Segment" stand danach an vierzehn
Stellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-16 16:26:57 +02:00
co-authored by Claude Opus 5
parent 32f592b83e
commit ed13d5fa43
73 changed files with 2137 additions and 694 deletions
+22 -22
View File
@@ -88,7 +88,7 @@ export type Film = {
/** Das Bild, das jetzt im Aufmacher gilt — vom Server gewählt. */
heroImage?: string | null;
/** Der Abspann, sofern er im Bild gefunden wurde. */
markers?: Marke[] | null;
markers?: Segment[] | null;
};
export type Serie = {
id: string; title: string; year: number | null; overview: string | null;
@@ -120,7 +120,7 @@ export type Serie = {
heroImage?: string | null;
};
/** Ein überspringbarer Abschnitt einer Folge. */
export type Marke = { kind: 'intro' | 'credits'; startSeconds: number; endSeconds: number };
export type Segment = { kind: 'intro' | 'credits'; startSeconds: number; endSeconds: number };
export type Folge = {
id: string; season: number; episode: number;
@@ -129,7 +129,7 @@ export type Folge = {
/** Zu welcher Serie sie gehört — für die Sprachwahl der Serie. */
seriesId?: string | null;
/** Vorspann und Abspann, sofern der Server sie gefunden hat. */
markers?: Marke[] | null;
markers?: Segment[] | null;
/** Wann sie freigegeben ist. Liegt das in der Zukunft, gibt es sie noch nicht. */
publishAt?: string | null;
};
@@ -196,11 +196,11 @@ export type Scanstand = {
missing: number | null;
/** Woran der Lauf gerade ist, in einem Satz. */
step: string | null;
/** Woran die Markensuche gerade ist — `null`, wenn keine läuft.
/** Woran die Segmentesuche gerade ist — `null`, wenn keine läuft.
*
* **Ein eigenes Feld, nicht `step`.** Sie hängt sich hinter den Scan und
* läuft weiter, wenn der längst „fertig" meldet. */
marken?: string | null;
segmente?: string | null;
problems: { path: string; reason: string }[] | null; error: string | null;
metadata: { moviesMatched: number; seriesMatched: number; episodesMatched: number;
skipped: boolean; error: string | null } | null;
@@ -653,19 +653,19 @@ export const naechsteFolgen = (take = 20) =>
/** Eine Folge für sich — tolerant, wie die Nachbarn.
*
* Für den Spieler: er bekommt nur eine Kennung und möchte wissen, ob es
* dazu Marken gibt. Eine 404 heisst „kein Folgentitel", und mehr hängt
* dazu Segmente gibt. Eine 404 heisst „kein Folgentitel", und mehr hängt
* nicht daran. */
/** Stösst die Suche nach Vorspann und Abspann einer Serie an.
*
* Antwortet mit 202: der Lauf dauert Minuten — er liest von jeder Folge
* einer Staffel ein Stück Ton —, und darauf wartet niemand. */
/** Wie weit ein Markenlauf greift.
/** Wie weit ein Segmentelauf greift.
*
* **„Schon durchsucht" und „hat eine Marke" sind zwei verschiedene
* Dinge.** Ein Titel, bei dem die Suche einmal nichts fand, trägt einen
* Vermerk und keine Marke — und war damit für jeden weiteren Lauf
* erledigt, obwohl er der interessanteste Fall ist. */
export type Markenumfang =
export type Segmentumfang =
/** Nur, wo noch nie gesucht wurde. */
| 'Neue'
/** Auch, wo gesucht und nichts gefunden wurde — nicht, wo schon eine Marke steht. */
@@ -673,21 +673,21 @@ export type Markenumfang =
/** Alles, auch was längst eine Marke hat. */
| 'Alles';
export const markenSuchen = (serieId: string, umfang: Markenumfang = 'Alles') =>
export const segmenteSuchen = (serieId: string, umfang: Segmentumfang = 'Alles') =>
anfrage<void>(
`/series/${serieId}/marken?umfang=${umfang}`, { method: 'POST' });
/** Die Marken eines Titels — Folge oder Film.
/** Die Segmente eines Titels — Folge oder Film.
*
* **Eine Kennung, zwei mögliche Wege.** Der Spieler bekommt nur eine
* Kennung und weiss nicht, was dahintersteckt; erst die Antwort sagt es.
* Eine 404 auf beiden Wegen heisst: keine Marken, und mehr hängt nicht
* Eine 404 auf beiden Wegen heisst: keine Segmente, und mehr hängt nicht
* daran. */
export async function markenHolen(titelId: string): Promise<Marke[]> {
export async function segmenteHolen(titelId: string): Promise<Segment[]> {
const folge = await folgeLesen(titelId);
if (folge) return folge.markers ?? [];
try {
const f = await anfrage<{ markers?: Marke[] | null }>(`/movies/${titelId}`);
const f = await anfrage<{ markers?: Segment[] | null }>(`/movies/${titelId}`);
return f.markers ?? [];
} catch {
return [];
@@ -698,7 +698,7 @@ export async function markenHolen(titelId: string): Promise<Marke[]> {
*
* **Antwortet sofort, arbeitet weiter.** Der Lauf liest je Folge ein Stück
* Ton; über fünfundneunzig Serien sind das Stunden. */
export const mediathekMarken = (id: string, umfang: Markenumfang) =>
export const mediathekSegmente = (id: string, umfang: Segmentumfang) =>
anfrage<void>(`/libraries/${id}/marken?umfang=${umfang}`, { method: 'POST' });
/** Die Adresse der Messkurve eines Films — zum Herunterladen.
@@ -706,15 +706,15 @@ export const mediathekMarken = (id: string, umfang: Markenumfang) =>
* **Keine Erkennung, sondern Zahlen zum Ansehen.** Welche Schwelle bei
* einem Abspann über laufendem Bild trägt, lässt sich nur an der Kurve
* entscheiden, nicht am Ergebnis der Erkennung. */
export const markenkurveAdresse = (filmId: string) =>
export const segmentkurveAdresse = (filmId: string) =>
`/api/v1/movies/${filmId}/markenkurve`;
/** Nimmt die Marken eines Films wieder weg — wenn die Erkennung danebenlag. */
export const filmmarkenVerwerfen = (filmId: string) =>
/** Nimmt die Segmente eines Films wieder weg — wenn die Erkennung danebenlag. */
export const filmsegmenteVerwerfen = (filmId: string) =>
anfrage<void>(`/movies/${filmId}/marken`, { method: 'DELETE' });
/** Stösst die Abspannsuche für einen Film an. */
export const filmmarkenSuchen = (filmId: string, umfang: Markenumfang = 'Alles') =>
export const filmsegmenteSuchen = (filmId: string, umfang: Segmentumfang = 'Alles') =>
anfrage<void>(`/movies/${filmId}/marken?umfang=${umfang}`, { method: 'POST' });
export async function folgeLesen(id: string): Promise<Folge | null> {
@@ -819,7 +819,7 @@ export const stromStarten = (datei: string, wunsch: Stromwunsch) =>
method: 'POST', body: JSON.stringify(wunsch),
});
/** Beendet einen Strom und räumt seinen Kratzplatz weg. */
/** Beendet einen Strom und räumt seinen Transcode-Cache weg. */
export const stromBeenden = (sitzung: string) =>
anfrage<void>(`/stream/${sitzung}`, { method: 'DELETE' });
@@ -1190,7 +1190,7 @@ export const statistik = (wochen = 12) =>
export type Speicherort = {
name: string; variable: string; pfad: string;
belegt: number; frei: number | null; bilder: number | null;
/** Nur beim Kratzplatz: die Unterordner einzeln. */
/** Nur beim Transcode-Cache: die Unterordner einzeln. */
teile: { name: string; frei: number; dateien: number }[] | null;
};
@@ -1202,11 +1202,11 @@ export type Aufraeumbericht = {
teile: { name: string; frei: number; dateien: number }[];
};
/** Räumt den Kratzplatz auf — Verwaistes immer, der Rest nach der Obergrenze.
/** Räumt den Transcode-Cache auf — Verwaistes immer, der Rest nach der Obergrenze.
*
* POST und nicht DELETE wie beim Bildspeicher: dort wird ein Ordner geleert,
* hier eine Regel angewandt. */
export const kratzplatzAufraeumen = () =>
export const transcodeAufraeumen = () =>
anfrage<Aufraeumbericht>('/system/storage/transcode/cleanup', { method: 'POST' });
export type Wartungseinstellung = {
+28 -28
View File
@@ -1,7 +1,7 @@
import type HlsTyp from 'hls.js';
import {
stromStarten, stromBeenden, untertitelHolen, fortschritt, nachbarfolge, markenHolen,
type Stromauskunft, type Spurauskunft, type Folge, type Marke,
stromStarten, stromBeenden, untertitelHolen, fortschritt, nachbarfolge, segmenteHolen,
type Stromauskunft, type Spurauskunft, type Folge, type Segment,
} from './api';
import { dauer, knopf, meldung, hinweisZeigen, folgenname } from './util';
import { serienton, serientext, spurFuer } from './sprachwahl';
@@ -386,10 +386,10 @@ export function spieler(anfrage: Spieleranfrage): void {
// **Zwei Knöpfe, nicht einer.** Im Abspann einer Serie gibt es zwei
// vernünftige Antworten — weiterschauen oder den Abspann überspringen —
// und welche gemeint ist, weiss nur der Zuschauer.
const markenreihe = document.createElement('div');
markenreihe.className = 'pl-marken hidden';
const segmentreihe = document.createElement('div');
segmentreihe.className = 'pl-segmente hidden';
schicht.append(video, dreher, mitte, kopf, bedienung, markenreihe, meldungsfeld, kleinleiste);
schicht.append(video, dreher, mitte, kopf, bedienung, segmentreihe, meldungsfeld, kleinleiste);
document.body.appendChild(schicht);
document.body.classList.add('pl-offen');
@@ -626,7 +626,7 @@ export function spieler(anfrage: Spieleranfrage): void {
let vorige: Folge | null = null;
let naechste: Folge | null = null;
/** Vorspann und Abspann dieser Folge — leer heisst: keine erkannt. */
let marken: Marke[] = [];
let segmente: Segment[] = [];
// **Eine Folge weiter, eine zurück.**
//
@@ -670,11 +670,11 @@ export function spieler(anfrage: Spieleranfrage): void {
const [davor, danach, gefunden] = await Promise.all([
nachbarfolge(anfrage.titelId, 'previous'),
nachbarfolge(anfrage.titelId, 'next'),
markenHolen(anfrage.titelId),
segmenteHolen(anfrage.titelId),
]);
vorige = davor;
naechste = danach;
marken = gefunden;
segmente = gefunden;
if (geschlossen) return;
vorigeKnopf.classList.toggle('hidden', vorige?.files[0] == null);
naechsteKnopf.classList.toggle('hidden', naechste?.files[0] == null);
@@ -827,7 +827,7 @@ export function spieler(anfrage: Spieleranfrage): void {
kleinStand.textContent = laenge > 0 ? `${dauer(jetzt)} / ${dauer(laenge)}` : dauer(jetzt);
kleinGefuellt.style.width = `${Math.min(100, anteil * 100)}%`;
markenstand(jetzt);
segmentstand(jetzt);
}
/** Zeigt den Überspringen-Knopf, solange man in einer Marke steht.
@@ -841,8 +841,8 @@ export function spieler(anfrage: Spieleranfrage): void {
* mehrmals je Sekunde; die Knöpfe jedesmal neu zu bauen hiesse, einen
* Klick mitten im Drücken zu verlieren.
*/
let markenstandJetzt = '';
/** Ob die Marken-Knöpfe inzwischen an der Bedienung hängen.
let segmentstandJetzt = '';
/** Ob die Segmente-Knöpfe inzwischen an der Bedienung hängen.
*
* **Erst für sich, dann mit der Bedienung.** Eine neue Marke zeigt ihre
* Knöpfe von selbst — sonst müsste man erst das Bild anfassen, um das
@@ -850,27 +850,27 @@ export function spieler(anfrage: Spieleranfrage): void {
* Sobald man sie aber einmal eingeblendet hat, gehören die Knöpfe dazu
* und gehen mit ihr. Keine Frist, kein Zeitgeber: der Zuschauer sagt
* selbst, wann er genug gesehen hat. */
let markenGebunden = false;
let markeLaeuft = false;
let segmentGebunden = false;
let segmentLaeuft = false;
function markenstand(jetzt: number): void {
const drin = marken.find(m => jetzt >= m.startSeconds && jetzt < m.endSeconds);
function segmentstand(jetzt: number): void {
const drin = segmente.find(m => jetzt >= m.startSeconds && jetzt < m.endSeconds);
const abspann = drin?.kind === 'credits';
const weiter = abspann && naechste?.files[0] != null;
const kennung = `${drin?.startSeconds ?? ''}|${weiter}`;
// Eine neue Marke ist ein neues Angebot: sie steht wieder für sich,
// bis die Bedienung das nächste Mal auftaucht.
if (kennung !== markenstandJetzt) markenGebunden = false;
if (kennung !== segmentstandJetzt) segmentGebunden = false;
markeLaeuft = drin != null;
markenreihe.classList.toggle(
segmentLaeuft = drin != null;
segmentreihe.classList.toggle(
'hidden',
!drin || (markenGebunden && schicht.classList.contains('pl-still')));
!drin || (segmentGebunden && schicht.classList.contains('pl-still')));
if (!drin || kennung === markenstandJetzt) return;
markenstandJetzt = kennung;
markenreihe.replaceChildren();
if (!drin || kennung === segmentstandJetzt) return;
segmentstandJetzt = kennung;
segmentreihe.replaceChildren();
// **Im Abspann einer Serie zuerst die nächste Folge.** Das ist, was man
// dort will; „überspringen" steht daneben für den, der den Abspann
@@ -878,7 +878,7 @@ export function spieler(anfrage: Spieleranfrage): void {
if (weiter) {
const w = knopf('Nächste Folge', 'btn btn-primary');
w.onclick = e => { e.stopPropagation(); folgeWechseln(naechste); };
markenreihe.appendChild(w);
segmentreihe.appendChild(w);
}
const u = knopf(
@@ -894,11 +894,11 @@ export function spieler(anfrage: Spieleranfrage): void {
// heisst überspringen Schluss" — und damit bekam niemand die Szene
// je zu sehen. Geschlossen wird nur, wenn nach dieser Marke nichts
// mehr kommt.
const danach = marken.some(m => m.startSeconds >= drin.endSeconds - 1);
const danach = segmente.some(m => m.startSeconds >= drin.endSeconds - 1);
if (abspann && !weiter && !danach) { void beenden(); return; }
void springen(drin.endSeconds);
};
markenreihe.appendChild(u);
segmentreihe.appendChild(u);
}
/** Ans Ende springen und zumachen — für den Abspann ohne Nachfolger.
@@ -1365,11 +1365,11 @@ export function spieler(anfrage: Spieleranfrage): void {
// Mausbewegung, auch wenn die Bedienung längst da ist. Das jedes Mal
// als „einmal gezeigt" zu werten hiesse, dem Intro seinen Knopf zu
// nehmen, bevor es ihn je hatte.
if (schicht.classList.contains('pl-still')) markenGebunden = true;
if (schicht.classList.contains('pl-still')) segmentGebunden = true;
schicht.classList.remove('pl-still');
// Im Stehen rechnet `markenstand` nicht weiter: wer pausiert im Intro
// Im Stehen rechnet `segmentstand` nicht weiter: wer pausiert im Intro
// die Maus bewegt, bekäme die Bedienung zurück und den Knopf nicht.
if (markeLaeuft) markenreihe.classList.remove('hidden');
if (segmentLaeuft) segmentreihe.classList.remove('hidden');
verbergenPlanen();
}
function verbergenPlanen(): void {
+4 -4
View File
@@ -659,7 +659,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
* wiederholen hiesse, vier gleich aussehende Zahlen nebeneinanderzustellen,
* von denen keine sagt, woher sie kommt. Das Kürzel sagt es in zwei Zeichen.
*
* **Und kein Logo.** Die Marken von IMDb und Rotten Tomatoes mitzuliefern
* **Und kein Logo.** Die Segmente von IMDb und Rotten Tomatoes mitzuliefern
* hiesse, fremde Zeichen auszuliefern — mit allem, was daran hängt. */
.bewertung.fremd { font-weight: 500; }
.bewertungsquelle {
@@ -685,7 +685,7 @@ svg { width: 20px; height: 20px; display: block; flex: none; }
/* **Der Primärknopf ist hell, nicht farbig.**
*
* Genau das macht Netflix: der Abspielknopf ist weiss mit schwarzer Schrift,
* die Markenfarbe erscheint nur im Schriftzug. Der Akzent sass hier auf neun
* die Segmentefarbe erscheint nur im Schriftzug. Der Akzent sass hier auf neun
* Dingen gleichzeitig — Marke, Navigation, Knopf, Fortschritt, Plakette,
* Stern, Fokus, Filter, Kontokreis. Wenn alles Akzent ist, markiert er
* nichts. */
@@ -928,7 +928,7 @@ textarea.feld {
* Leiste gar nicht. Hier liegt er über dem Bild, rechts über der Stelle,
* an der die Leiste erscheinen würde, und bleibt stehen, solange die Marke
* läuft. */
.pl-marken {
.pl-segmente {
position: absolute;
right: var(--s-section);
bottom: calc(var(--s-section) * 3.5);
@@ -936,7 +936,7 @@ textarea.feld {
display: flex;
gap: var(--s-snug);
}
.pl-marken .btn { box-shadow: 0 2px 12px rgba(0, 0, 0, .6); }
.pl-segmente .btn { box-shadow: 0 2px 12px rgba(0, 0, 0, .6); }
.pl-unten {
display: grid; grid-template-columns: 1fr auto 1fr;
+13 -13
View File
@@ -2,8 +2,8 @@ import { ApiFehler,
film as filmLaden, serie as serieLaden, folgen,
vorschlaege, zuordnen, bildAdresse, istAdmin, gesehen, favorit, weglegen, serienstimmen,
type Film, type Serie, type Folge, type Datei, type Stand,
type Mitwirkender, type Spur, aehnliche, metadatenAuffrischen, scanstand, markenSuchen,
filmmarkenSuchen, filmmarkenVerwerfen, markenkurveAdresse,
type Mitwirkender, type Spur, aehnliche, metadatenAuffrischen, scanstand, segmenteSuchen,
filmsegmenteSuchen, filmsegmenteVerwerfen, segmentkurveAdresse,
} from '../api';
import {
esc, dauer, groesse, knopf, feld, fehlerzeile, leer, anteil, meldung, hinweisZeigen,
@@ -33,7 +33,7 @@ type Art = 'filme' | 'serien';
* die Besetzung mit runden Porträts. */
/** Wie die fremden Quellen heissen — kurz an der Kachel, lang im Tooltip.
*
* **Kein Logo.** Die Marken von IMDb und Rotten Tomatoes mitzuliefern
* **Kein Logo.** Die Segmente von IMDb und Rotten Tomatoes mitzuliefern
* hiesse, fremde Zeichen auszuliefern; ein Kürzel sagt dasselbe und gehört
* uns.
*
@@ -513,7 +513,7 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
if (melden && istAdmin() && art === 'filme') {
reihe.appendChild(rundKnopf('uhr', false, 'Abspann neu suchen',
async () => {
await filmmarkenSuchen(eintrag.id, 'Alles');
await filmsegmenteSuchen(eintrag.id, 'Alles');
hinweisZeigen('Der Abspann wird gesucht. Der Server sieht sich dafür '
+ 'die letzten zwanzig Minuten an — das dauert einen Augenblick.');
// Kurz warten und nachsehen: anders als bei einer Serie ist das
@@ -526,7 +526,7 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
// begründen. Der Knopf lädt sie als Tabelle herunter.
reihe.appendChild(rundKnopf('protokoll', false, 'Messwerte des Abspanns holen',
async () => {
window.location.href = markenkurveAdresse(eintrag.id);
window.location.href = segmentkurveAdresse(eintrag.id);
hinweisZeigen('Die Messung läuft — der Server sieht sich dafür die '
+ 'letzten zwanzig Minuten dreimal an.');
}));
@@ -534,9 +534,9 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
// **Nur, wenn es etwas zu verwerfen gibt.** Ein Knopf, der nichts
// wegnehmen kann, ist eine Behauptung über einen Zustand.
if ('markers' in eintrag && (eintrag.markers?.length ?? 0) > 0) {
reihe.appendChild(rundKnopf('schliessen', false, 'Abspannmarke verwerfen',
reihe.appendChild(rundKnopf('schliessen', false, 'Abspannsegment verwerfen',
async () => {
await filmmarkenVerwerfen(eintrag.id);
await filmsegmenteVerwerfen(eintrag.id);
hinweisZeigen('Marke entfernt. Der nächste Scan legt sie nicht wieder an.');
await laden();
}));
@@ -546,7 +546,7 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
if (melden && istAdmin() && art === 'serien') {
reihe.appendChild(rundKnopf('uhr', false, 'Vorspann und Abspann neu suchen',
async () => {
await markenSuchen(eintrag.id, 'Alles');
await segmenteSuchen(eintrag.id, 'Alles');
hinweisZeigen('Vorspann und Abspann werden gesucht. '
+ 'Der Server liest dafür jede Folge einmal durch — das dauert ein paar Minuten.');
}));
@@ -1112,14 +1112,14 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
* gar nicht gefunden wurde oder bloss nicht angezeigt wird, ist von
* aussen nicht zu unterscheiden — und beides hat eine andere Ursache.
* Einmal nachsehen zu können erspart eine Runde Raten. */
function markenwort(marken: Folge['markers']): string {
function segmentwort(segmente: Folge['markers']): string {
if (!istAdmin()) return '';
if (!marken || marken.length === 0) return 'keine Marken';
if (!segmente || segmente.length === 0) return 'keine Segmente';
// **Mit Zeiten, nicht bloss mit Namen.** Dass eine Marke da ist, sagt
// noch nicht, dass sie an der richtigen Stelle steht — und genau das
// ist der Unterschied zwischen „die Erkennung hat nichts gefunden"
// und „der Knopf kommt zur falschen Minute".
return marken
return segmente
.map(m => `${m.kind === 'intro' ? 'Vorspann' : 'Abspann'} `
+ `${dauer(m.startSeconds)}–${dauer(m.endSeconds)}`)
.join(' · ');
@@ -1135,8 +1135,8 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
: folge.state && folge.state.positionSeconds > 0
? `noch ${Math.round(((folge.state.runtimeSeconds ?? 0) - folge.state.positionSeconds) / 60)} Min`
: '';
const marken = markenwort(folge.markers);
const daneben = [nebenbei, marken].filter(Boolean).join(' · ');
const segmente = segmentwort(folge.markers);
const daneben = [nebenbei, segmente].filter(Boolean).join(' · ');
zeile.innerHTML =
`<strong>${String(folge.episode).padStart(2, '0')} · ${esc(folge.title ?? 'Ohne Titel')}</strong>`
+ (daneben ? `<span class="winzig">${esc(daneben)}</span>` : '');
+20 -20
View File
@@ -1,6 +1,6 @@
import {
bibliotheken, bibliothekAnlegen, bibliothekUmbenennen, bibliothekLoeschen, scanStarten,
mediathekMarken, type Markenumfang,
mediathekSegmente, type Segmentumfang,
scanstand, metadatenAuffrischen, vermisste, vermisstesEntfernen, type VermissteDatei,
pruefungSetzen, mediathekZuruecksetzen, type Bibliothek,
} from '../../api';
@@ -227,10 +227,10 @@ export function bibliotheksbereich(
// Angebot.** Den gibt es auf der Serienseite; hier gilt er für alles,
// was in dieser Mediathek liegt — Serien über den Tonabgleich, Filme
// über das Bild.
/** Startet einen Markenlauf und hängt die Standzeile daran. */
function markenlauf(umfang: Markenumfang): void {
/** Startet einen Segmentelauf und hängt die Standzeile daran. */
function segmentelauf(umfang: Segmentumfang): void {
laufendSetzen(true);
void mediathekMarken(aktuell.id, umfang).then(() => {
void mediathekSegmente(aktuell.id, umfang).then(() => {
hinweisZeigen('Die Suche läuft. Der Stand steht hier und im Protokoll von heute.');
void verfolgen(false);
});
@@ -243,29 +243,29 @@ export function bibliotheksbereich(
// herauskam — und eben nicht die vierhundert, bei denen längst etwas
// steht. „Alles neu" ist die seltene und teure; sie gehört nicht
// hinter denselben Knopf.
const marken = knopf('Fehlende Marken suchen', 'btn');
marken.title = 'Nimmt sich die Titel vor, bei denen keine Marke steht.';
marken.onclick = () => nachfragen({
titel: `Fehlende Marken in „${aktuell.name}" suchen?`,
const segmente = knopf('Fehlende Segmente suchen', 'btn');
segmente.title = 'Nimmt sich die Titel vor, bei denen keine Marke steht.';
segmente.onclick = () => nachfragen({
titel: `Fehlende Segmente in „${aktuell.name}" suchen?`,
text: 'Nur, wo noch kein Vorspann oder Abspann steht. Bei einer Serie '
+ 'wird eine Staffel ganz gelesen, sobald einer ihrer Folgen etwas '
+ 'fehlt — der Tonabgleich vergleicht die Folgen miteinander und '
+ 'geht nicht einzeln. Es läuft im Hintergrund weiter.',
bestaetigen: 'Suchen',
tun: async () => markenlauf('Luecken'),
tun: async () => segmentelauf('Luecken'),
});
const markenAlle = knopf('Alles neu durchsuchen', 'btn');
markenAlle.title = 'Bestimmt auch vorhandene Marken noch einmal.';
markenAlle.onclick = () => nachfragen({
titel: `Alle Marken in „${aktuell.name}" neu bestimmen?`,
const segmenteAlle = knopf('Alles neu durchsuchen', 'btn');
segmenteAlle.title = 'Bestimmt auch vorhandene Segmente noch einmal.';
segmenteAlle.onclick = () => nachfragen({
titel: `Alle Segmente in „${aktuell.name}" neu bestimmen?`,
text: `Jeder Titel wird noch einmal durchsucht — auch die, bei denen `
+ `längst eine Marke steht. Der Server liest dafür von jeder Folge die `
+ `Tonspur und sieht sich bei jedem Film die letzten zwanzig Minuten an. `
+ `Bei ${aktuell.itemCount ?? 'vielen'} Titeln dauert das Stunden. `
+ `Für „was fehlt noch?" ist der Knopf daneben der richtige.`,
bestaetigen: 'Alles neu',
tun: async () => markenlauf('Alles'),
tun: async () => segmentelauf('Alles'),
});
const umbenennen = knopf('Umbenennen', 'btn');
@@ -335,13 +335,13 @@ export function bibliotheksbereich(
laufendSetzen(false);
stand.textContent = s.error ?? 'Gescheitert.';
return;
} else if (s.marken) {
// **Die Markensuche hängt sich hinten an und läuft weiter.**
} else if (s.segmente) {
// **Die Segmentesuche hängt sich hinten an und läuft weiter.**
// Sie dauert Stunden, wo der Scan Minuten dauert; ohne diesen
// Zweig stünde hier längst das Ergebnis des Scans, während der
// Server noch jede Folge durchhört.
laufendSetzen(true);
stand.textContent = s.marken;
stand.textContent = s.segmente;
}
// **Alle Zähler auf demselben Takt.**
@@ -357,9 +357,9 @@ export function bibliotheksbereich(
// Mediathek, ohne je Sekunde zwei Abrufe zu kosten.
if (melden && i > 0 && i % 3 === 0) geaendert(await laden());
// Ein fertiger Scan ist nicht das Ende, solange die Markensuche
// Ein fertiger Scan ist nicht das Ende, solange die Segmentesuche
// dahinter noch läuft — sonst fiele die Zeile mitten im Lauf weg.
if (s.state === 'finished' && !s.marken) {
if (s.state === 'finished' && !s.segmente) {
const teile = [`${s.filesFound} Dateien`, `${s.added} neu`, `${s.removed} entfernt`];
// „ohne Folgennummer" stimmte nicht mehr: gezählt wird jetzt auch,
// was eine Nummer trägt, aber zu keiner Serie gehört.
@@ -402,7 +402,7 @@ export function bibliotheksbereich(
// und danach nachzusehen. Der Knopf heisst „Seitenleiste bearbeiten".
const knoepfe = document.createElement('div');
knoepfe.className = 'knopfreihe';
knoepfe.append(los, auffrischen, marken, markenAlle, pruefung, umbenennen,
knoepfe.append(los, auffrischen, segmente, segmenteAlle, pruefung, umbenennen,
zuruecksetzen, entfernen);
// **Nur wenn hier wirklich etwas liegt.** Ein Knopf, der nichts zu tun
@@ -1,7 +1,7 @@
import {
doppelteZusammenlegen, servernameSetzen,
systemauskunft, laufendeWiedergaben, serverzustand, bildAdresse, speicherorte,
bildspeicherLeeren, kratzplatzAufraeumen, type Wiedergabe,
bildspeicherLeeren, transcodeAufraeumen, type Wiedergabe,
} from '../../api';
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../../util';
import { dialog } from '../../dialog';
@@ -246,7 +246,7 @@ export function speicherbereich(): HTMLElement {
+ `<span class="winzig">Wird gesetzt über ${esc(o.variable)}.</span>`;
oben.appendChild(text);
// **Beim Kratzplatz aufgeschlüsselt.** „39 GB belegt" sagt nicht, ob
// **Beim Transcode-Cache aufgeschlüsselt.** „39 GB belegt" sagt nicht, ob
// das Segmente, umgepackte Filme oder Untertitel sind — und davon
// hängt ab, ob es ein Problem ist. Die Dateizahl steht dabei, weil
// bei den Untertiteln die Anzahl das Problem ist und nicht die Grösse.
@@ -262,7 +262,7 @@ export function speicherbereich(): HTMLElement {
raeumen.onclick = async () => {
raeumen.disabled = true;
try {
const bericht = await kratzplatzAufraeumen();
const bericht = await transcodeAufraeumen();
await laden();
const gemeldet = document.createElement('p');
gemeldet.className = 'hinweis';
+3 -3
View File
@@ -8,7 +8,7 @@ import { abschnitt, zeile } from './teile';
/** Was der Server in Abständen von selbst tut.
*
* **Warum das eine eigene Seite ist.** Die drei Zahlen zum Kratzplatz
* **Warum das eine eigene Seite ist.** Die drei Zahlen zum Transcode-Cache
* standen zuerst unter „Wiedergabe" — dort ist ihr Anlass, aber nicht ihr
* Thema. Sobald es eine Sicherung und ein Aufräumen der Datenbank gibt,
* gehören sie zusammen: alles hier ist etwas, das der Server ohne Zutun
@@ -61,9 +61,9 @@ export function wartungsbereich(): HTMLElement {
liste.innerHTML = '';
stand.forEach(s => liste.appendChild(aufgabe(s, werte)));
/* ---------- Der Kratzplatz ---------- */
/* ---------- Der Transcode-Cache ---------- */
liste.appendChild(abschnitt('Kratzplatz'));
liste.appendChild(abschnitt('Transcode-Cache'));
liste.appendChild(zeile(
'Obergrenze',