Vorspann und Abspann — erkannt, abgelegt, überspringbar

Beides mit derselben Rechnung, nur in einem anderen Fenster: vorn die
ersten fünfzehn Minuten, hinten die letzten zehn. Was `Tonabgleich`
findet, hängt allein daran, welchen Ausschnitt man ihm gibt — eine
zweite Erkennung für den Abspann wäre dieselbe Suche ein zweites Mal.

Je Staffel verglichen, nicht je Serie: Serien wechseln ihren Vorspann
zwischen Staffeln, und über alle hinweg fände man nichts oder das
Falsche. Gefundenes über fünf Minuten wird verworfen — zwei Folgen, die
dieselbe Datei tragen, teilen vierzig, und darüber stünde sonst ein
Überspringen-Knopf.

Der Lauf geht auf Zuruf, nicht bei jedem Scan: er liest von jeder Folge
ein Stück Ton und dauert Minuten. Der Knopf dafür steht auf der
Serienseite, bei den Verwalterknöpfen.

Die Marken gehen im Folgenvertrag mit — als „intro" und „credits", die
Wörter, die die Apps von Jellyfin her ohnehin kennen. In der App zeichnet
sie der vorhandene Überspringen-Knopf, der bisher nie etwas zu tun
hatte; im Web ist er neu, rechtsbündig über der Zeitleiste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-12 21:28:36 +02:00
co-authored by Claude Opus 5
parent acba683c94
commit 8cf2118723
17 changed files with 2009 additions and 10 deletions
+35 -4
View File
@@ -1,7 +1,7 @@
import type HlsTyp from 'hls.js';
import {
stromStarten, stromBeenden, untertitelHolen, fortschritt, nachbarfolge,
type Stromauskunft, type Spurauskunft, type Folge,
stromStarten, stromBeenden, untertitelHolen, fortschritt, nachbarfolge, folgeLesen,
type Stromauskunft, type Spurauskunft, type Folge, type Marke,
} from './api';
import { dauer, knopf, meldung, hinweisZeigen } from './util';
import { serienton, serientext, spurFuer } from './sprachwahl';
@@ -600,6 +600,8 @@ export function spieler(anfrage: Spieleranfrage): void {
/** Die Nachbarfolgen, sobald der Server sie genannt hat. */
let vorige: Folge | null = null;
let naechste: Folge | null = null;
/** Vorspann und Abspann dieser Folge — leer heisst: keine erkannt. */
let marken: Marke[] = [];
// **Eine Folge weiter, eine zurück.**
//
@@ -627,16 +629,28 @@ export function spieler(anfrage: Spieleranfrage): void {
unten.className = 'pl-unten';
unten.append(links, zeitmitte, rechts);
bedienung.append(zeitleiste, unten);
// **„Intro überspringen" über der Leiste, rechtsbündig.**
//
// Dort erwartet man ihn — so macht es jeder Spieler —, und dort verdeckt
// er weder das Bild noch die Zeitleiste. Er steht im selben Block wie die
// Bedienung und blendet mit ihr ein und aus.
const ueberspringen = document.createElement('button');
ueberspringen.className = 'btn pl-ueberspringen hidden';
bedienung.append(ueberspringen, zeitleiste, unten);
// **Beide Nachbarn gleich zu Beginn holen.** Zwei kleine Anfragen, einmal
// je Wiedergabe. Erst beim Antippen zu fragen hiesse, einen Knopf zu
// zeigen, von dem noch niemand weiss, ob er etwas tut.
void (async () => {
[vorige, naechste] = await Promise.all([
const [davor, danach, diese] = await Promise.all([
nachbarfolge(anfrage.titelId, 'previous'),
nachbarfolge(anfrage.titelId, 'next'),
folgeLesen(anfrage.titelId),
]);
vorige = davor;
naechste = danach;
marken = diese?.markers ?? [];
if (geschlossen) return;
vorigeKnopf.classList.toggle('hidden', vorige?.files[0] == null);
naechsteKnopf.classList.toggle('hidden', naechste?.files[0] == null);
@@ -741,6 +755,23 @@ 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);
}
/** Zeigt den Überspringen-Knopf, solange man in einer Marke steht.
*
* **Keine eigene Uhr.** Die Zeit wird ohnehin bei jedem Fortschreiten
* nachgeführt; ein zweiter Zeitgeber dafür liefe neben dem ersten her und
* wäre eine zweite Stelle, an der etwas hängen bleibt. */
function markenstand(jetzt: number): void {
const drin = marken.find(m => jetzt >= m.startSeconds && jetzt < m.endSeconds);
ueberspringen.classList.toggle('hidden', !drin);
if (!drin) return;
const text = drin.kind === 'intro' ? 'Intro überspringen' : 'Abspann überspringen';
if (ueberspringen.textContent !== text) ueberspringen.textContent = text;
ueberspringen.onclick = e => { e.stopPropagation(); void springen(drin.endSeconds); };
}
function umschalten(): void {