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:
co-authored by
Claude Opus 5
parent
32f592b83e
commit
ed13d5fa43
+22
-22
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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>` : '');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user