From 66b963f2c5503d5020bb22c102ef364ae2793c56 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 14 Sep 2026 17:43:03 +0200 Subject: [PATCH] =?UTF-8?q?Keine=20Emojis=20als=20Symbole=20=E2=80=94=20un?= =?UTF-8?q?d=20der=20R=C3=BCckfall=20tat=20das=20Gegenteil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Dinge. Erstens: Symbole sind jetzt Vektoren, keine Schriftzeichen. Ein Lautsprecher als Emoji wird auf jeder Plattform anders gezeichnet, trägt fremde Farben, sitzt schief auf der Grundlinie und lässt sich weder in Strichstärke noch Farbe anpassen — es sieht nach Platzhalter aus, weil es einer ist. Elf Stellen umgestellt, die Pfade stehen an einer Stelle beisammen. Zweitens, und das ist der Grund, warum nichts lief: Nennt der Server keine eigene Adresse, fiel der Code auf "static=true" zurück — also auf die Datei, wie sie auf der Platte liegt. Bei einem MKV mit DTS ist das für einen Browser unlesbar, und er meldet bloß "Format nicht unterstützt". Der Rückfall tat genau das Gegenteil dessen, was er sollte. Jetzt verlangt er master.m3u8, also ausdrücklich einen HLS-Strom. Damit so etwas nicht wieder im Dunkeln liegt, nennt die Fehleranzeige nun auch, was der Server geantwortet hat und welche Adresse verwendet wurde — der Zugangsschlüssel darin gekürzt. Dazu: während des Ladens treten die Transportknöpfe zurück. Der Kreisel lag vorher genau hinter dem größten von ihnen. Co-Authored-By: Claude Opus 5 --- web/frontend/index.html | 122 +++++++++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 20 deletions(-) diff --git a/web/frontend/index.html b/web/frontend/index.html index 250f06b..8fd9aec 100644 --- a/web/frontend/index.html +++ b/web/frontend/index.html @@ -278,6 +278,13 @@ img { display: block; } transition: background .15s ease; } .pl-icon:hover { background: rgba(40,40,46,.75); } +/* Eine Vorgabegröße für alle Symbole, damit sie zueinander passen; einzelne + überschreiben sie über `icon(name, size)`. */ +.pl-icon svg, .pl-round svg, .tiny svg, .hero-nav svg { width: 18px; height: 18px; } +.pl-round svg { width: 22px; height: 22px; } +.btn svg { width: 15px; height: 15px; vertical-align: -2px; margin-right: 2px; } +.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; } +.tiny, .pl-icon, .pl-round, .hero-nav { display: inline-flex; align-items: center; justify-content: center; } .pl-center { display: flex; align-items: center; justify-content: center; gap: var(--s-section); } /* Dunkel statt hell durchscheinend: über einer sonnigen Szene verschwanden @@ -331,6 +338,11 @@ img { display: block; } .pl-rate { font-size: 13px; color: var(--text-2); display: flex; align-items: center; gap: var(--s-snug); } .pl-rate .pick { width: auto; padding: 6px var(--s-regular); font-size: 13px; } +/* Während geladen wird, treten die Transportknöpfe zurück: sie tun ohnehin + nichts, und der Kreisel lag vorher genau hinter dem größten von ihnen. */ +#player.loading .pl-center { opacity: 0; pointer-events: none; } +.pl-center { transition: opacity .2s ease; } + .pl-busy { position: absolute; display: none; pointer-events: none; } @@ -350,6 +362,11 @@ img { display: block; } .pl-error strong { color: #F08A8D; font-size: 16px; font-weight: 600; } .pl-error span { color: var(--text-2); font-size: 14px; } .pl-error .btn { align-self: flex-start; } +.pl-error code { + font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; + color: var(--text-3); background: rgba(0,0,0,.4); padding: var(--s-snug); + border-radius: 6px; word-break: break-all; line-height: 1.5; +} @media (max-width: 600px) { .pl-top, .pl-bottom { padding-left: var(--s-loose); padding-right: var(--s-loose); } @@ -1281,7 +1298,7 @@ function buildHero(items) {
${bits.join(' · ')}
${esc(it.Overview || '')}
- +
`; body.querySelector('.btn-primary').onclick = () => openSheet(it, true); @@ -1293,12 +1310,12 @@ function buildHero(items) { if (items.length > 1) { const prev = document.createElement('button'); - prev.className = 'hero-nav prev'; prev.innerHTML = '‹'; + prev.className = 'hero-nav prev'; prev.innerHTML = icon('left', 22); prev.setAttribute('aria-label', 'Vorheriger Titel'); prev.onclick = () => step(-1); const next = document.createElement('button'); - next.className = 'hero-nav next'; next.innerHTML = '›'; + next.className = 'hero-nav next'; next.innerHTML = icon('right', 22); next.setAttribute('aria-label', 'Nächster Titel'); next.onclick = () => step(1); @@ -1397,8 +1414,8 @@ function renderRowArrangement() { ${esc(row.title)} - - `; + + `; line.querySelector('input').onchange = (e) => { const next = e.target.checked @@ -1460,7 +1477,7 @@ function openSheet(item, playNow) {
${bits.join(' · ')}

${esc(item.Overview || '')}

- ${playable ? `` : ''} + ${playable ? `` : ''}
`; @@ -1471,7 +1488,7 @@ function openSheet(item, playNow) { const start = async (button) => { if (button) { button.disabled = true; button.textContent = 'Suche Folge …'; } const target = isFolder ? await nextEpisode(item) : item; - if (button) { button.disabled = false; button.textContent = isFolder ? '▶ Weiterschauen' : '▶ Abspielen'; } + if (button) { button.disabled = false; button.innerHTML = icon('play', 15) + (isFolder ? ' Weiterschauen' : ' Abspielen'); } if (!target) { back.querySelector('.btn-row').insertAdjacentHTML('afterend', '

Diese Serie hat keine Folgen, die sich abspielen ließen.

'); @@ -1535,7 +1552,7 @@ function openPlayer(item) {
- +
${esc(item.SeriesName || item.Name)} ${item.SeriesName ? `${esc(episodeLabel(item))}` : ''} @@ -1544,7 +1561,7 @@ function openPlayer(item) {
- +
@@ -1555,11 +1572,11 @@ function openPlayer(item) { –:--
- + - +
@@ -1571,12 +1588,14 @@ function openPlayer(item) { const chrome = layer.querySelector('.pl-chrome'); const el2 = (id) => layer.querySelector('#' + id); - const fail = (satz, hinweis) => { + const fail = (satz, hinweis, adresse) => { + layer.classList.remove('loading'); el2('pl-busy').classList.remove('on'); layer.querySelector('.pl-error')?.remove(); const box = document.createElement('div'); box.className = 'pl-error'; box.innerHTML = `${esc(satz)}${hinweis ? `${esc(hinweis)}` : ''} + ${adresse ? `${esc(adresse.replace(/api_key=[^&]*/, 'api_key=…'))}` : ''} `; box.querySelector('button').onclick = close; layer.appendChild(box); @@ -1602,14 +1621,17 @@ function openPlayer(item) { v.addEventListener('click', toggle); const paint = () => { - el2('pl-toggle').textContent = v.paused ? '▶' : '❚❚'; + el2('pl-toggle').innerHTML = icon(v.paused ? 'play' : 'pause', 30); el2('pl-toggle').setAttribute('aria-label', v.paused ? 'Abspielen' : 'Anhalten'); show(); }; v.addEventListener('play', paint); v.addEventListener('pause', paint); v.addEventListener('waiting', () => el2('pl-busy').classList.add('on')); - v.addEventListener('playing', () => el2('pl-busy').classList.remove('on')); + v.addEventListener('playing', () => { + el2('pl-busy').classList.remove('on'); + layer.classList.remove('loading'); + }); // --- Fortschrittsleiste --- const track = el2('pl-track'); @@ -1653,7 +1675,7 @@ function openPlayer(item) { vol.oninput = () => { v.volume = +vol.value; v.muted = +vol.value === 0; }; el2('pl-mute').onclick = () => { v.muted = !v.muted; - el2('pl-mute').textContent = v.muted ? '🔇' : '🔊'; + el2('pl-mute').innerHTML = icon(v.muted ? 'muted' : 'sound'); }; const rate = el2('pl-rate'); @@ -1698,6 +1720,7 @@ function openPlayer(item) { el2('pl-close').onclick = close; show(); + layer.classList.add('loading'); el2('pl-busy').classList.add('on'); startStream(item, v, fail); } @@ -1716,11 +1739,14 @@ function episodeLabel(item) { mitführen. Die Adresse selbst zusammenzubauen wäre eine Vermutung darüber, was der Server kopieren kann und was er rechnen muss. */ async function startStream(item, v, fail) { + // Erst nach dem Aushandeln bekannt — deshalb hier eine Schachtel, die + // später gefüllt wird. + const diagnose = { text: '', url: '' }; v.addEventListener('error', () => { const c = v.error?.code; fail('Der Browser kann diesen Strom nicht abspielen.', - c === 4 ? 'Das Format wird nicht unterstützt — der Server sollte umrechnen, tat es aber nicht.' - : `Fehlercode ${c ?? '?'}. Im Netzwerk-Tab steht, was der Server geantwortet hat.`); + (c === 4 ? 'Das Format wird nicht unterstützt. ' : `Fehlercode ${c ?? '?'}. `) + + diagnose.text, diagnose.url); }); let info; @@ -1733,13 +1759,40 @@ async function startStream(item, v, fail) { const source = info.MediaSources?.[0]; if (!source) return fail('Der Server nennt keine abspielbare Quelle.'); + // **Der Rückfall muss Umrechnung erzwingen, nicht sie umgehen.** + // + // Hier stand `static=true` — das schickt die Datei so, wie sie auf der + // Platte liegt. Bei einem MKV mit DTS ist das für einen Browser unlesbar, + // und er meldet bloß „Format nicht unterstützt". Genau dieser Rückfall war + // der Grund, warum nichts lief: er tat das Gegenteil dessen, was er sollte. + // + // `master.m3u8` verlangt dagegen ausdrücklich einen HLS-Strom. Nennt der + // Server keine eigene Adresse, ist das die richtige Bitte — nicht das + // Original. const url = source.TranscodingUrl ? new URL(source.TranscodingUrl, S.base).href - : streamURL(`/Videos/${item.Id}/stream`, { - static: 'true', mediaSourceId: source.Id || item.Id, - api_key: S.token, PlaySessionId: info.PlaySessionId || '', + : streamURL(`/Videos/${item.Id}/master.m3u8`, { + mediaSourceId: source.Id || item.Id, + api_key: S.token, + PlaySessionId: info.PlaySessionId || '', + DeviceId: S.deviceId, + VideoCodec: 'h264', AudioCodec: 'aac,mp3', + TranscodingMaxAudioChannels: '2', SegmentContainer: 'ts', + MinSegments: '1', BreakOnNonKeyFrames: 'true', + TranscodingProtocol: 'hls', }); + // Was der Server geantwortet hat — damit im Fehlerfall nicht geraten werden + // muss, sondern dasteht, woran es lag. + const auskunft = [ + source.TranscodingUrl ? 'Server nannte eine Adresse' : 'Server nannte keine Adresse (Rückfall auf HLS)', + source.Container ? `Container ${source.Container}` : null, + source.SupportsTranscoding === false ? 'Server sagt: kann nicht umrechnen' : null, + ].filter(Boolean).join(' · '); + + diagnose.text = auskunft; + diagnose.url = url; + v.addEventListener('loadedmetadata', () => { v.playbackRate = P.rate; v.play(); }, { once: true }); wireProgress(v, { id: info.PlaySessionId, sourceID: source.Id || item.Id, itemID: item.Id }); @@ -1838,6 +1891,35 @@ function streamURL(path, query) { return u.href; } +/* Die Symbole der Oberfläche. + ---------------------------------------------------------------------------- + Als Vektor, nicht als Schriftzeichen. Ein ▶ oder ein 🔊 wird auf jeder + Plattform anders gezeichnet, trägt fremde Farben, sitzt schief auf der + Grundlinie und lässt sich weder in der Strichstärke noch in der Farbe + anpassen — es sieht nach Platzhalter aus, weil es einer ist. + + `currentColor` erbt Farbe und Zustand vom Knopf darum; `stroke-width` bleibt + überall gleich, damit die Symbole zueinander passen. */ +const ICONS = { + play: '', + pause: '', + close: '', + sound: '', + muted: '', + expand: '', + left: '', + right: '', + up: '', + down: '', +}; + +/// Ein Symbol als Vektor. `size` nur, wo es von der Vorgabe abweicht. +function icon(name, size) { + return ``; +} + /* ---------- Kleinkram ---------- */ function esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) =>