From e7bd24bbaad582abd5f57762d9a9dc9ab608d574 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:43:34 -0400 Subject: [PATCH] Transcripts: a podcast episode gets its transcript going as soon as you play it (published, else made on your server) --- public/lyrics-view.js | 22 +++++++++++++++++++--- public/sw.js | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/public/lyrics-view.js b/public/lyrics-view.js index a0c4a37..c894955 100644 --- a/public/lyrics-view.js +++ b/public/lyrics-view.js @@ -43,12 +43,15 @@ async function lyrFromPlex(t) { // lyrics stored with the song (embedded, a .lrc /** A transcript: the show's own if it publishes one, else (on request) one made on this server. Polls while that is being made. */ async function lyrSpoken(t, start) { const q = new URLSearchParams({ key: t.key, src: t.src || '' }); - for (let n = 0; ; n++) { + for (let n = 0; ; n++) { // (n restarts at -1 after an automatic start, so the POST goes out once) if (!lyr.el || lyr.key !== t.key) return null; - let r; try { r = await jfetch('/api/transcript?' + q, start && n === 0 ? { method: 'POST' } : undefined); } catch (e) { return { found: false, error: e.message }; } + let r; try { r = await jfetch('/api/transcript?' + q, start && n <= 0 ? { method: 'POST' } : undefined); } catch (e) { return { found: false, error: e.message }; } if (r.state === 'ready') { r.transcript.spoken = true; return r.transcript; } if (r.state === 'error') return { found: false, error: r.error, canTranscribe: r.canTranscribe, none: true }; - if (r.state === 'none') return { found: false, none: true, canTranscribe: r.canTranscribe, why: r.why }; + if (r.state === 'none') { + if (r.canTranscribe && !start && String(t.key).startsWith('pod:') && !lyrPref().noAuto) { start = true; n = -1; continue; } // nothing published: make one (an episode, not a whole book) + return { found: false, none: true, canTranscribe: r.canTranscribe, why: r.why }; + } const body = lyr.el.querySelector('#lyrbody'); if (body) body.innerHTML = `

Making a transcript on your server… ${Math.max(0, Math.round((Date.now() - r.since) / 60000))} min so far.
You can close this: it keeps going, and it is kept for next time.

`; await new Promise(ok => setTimeout(ok, 4000)); } @@ -147,3 +150,16 @@ function lyrTick() { // follows the song: which line is current, and how far int if (l.words) on.querySelectorAll('.w').forEach((w, j) => { const x = l.words[j]; if (!x) return; const p = pos >= x.e ? 1 : pos <= x.t ? 0 : (pos - x.t) / Math.max(1, x.e - x.t); w.classList.toggle('sung', p >= 1); w.style.setProperty('--p', p); }); else { const e = l.e && l.e > l.t ? l.e : l.t + 4000, p = Math.max(0, Math.min(1, (pos - l.t) / Math.max(400, Math.min(e - l.t, 7000)))); on.style.setProperty('--p', p); } // (no word timing: the colour sweeps across the line) } + +// Playing a podcast episode is also the signal to get its transcript going in the background (the published one if there is one, else one made on this +// server), so the words are usually ready, or well on the way, by the time Transcript is opened. Once per episode per visit; nothing for audiobooks. +const lyrAsked = new Set(); +pl.a.addEventListener('playing', async () => { + const t = typeof cur === 'function' ? cur() : null; + if (!t || !String(t.key).startsWith('pod:') || !t.src || lyrAsked.has(t.key) || !cfg.transcribe || lyrPref().noAuto) return; + lyrAsked.add(t.key); + try { + const q = new URLSearchParams({ key: t.key, src: t.src }), r = await jfetch('/api/transcript?' + q); + if (r.state === 'none' && r.canTranscribe) await jfetch('/api/transcript?' + q, { method: 'POST' }); + } catch { lyrAsked.delete(t.key); } +}); diff --git a/public/sw.js b/public/sw.js index c31744a..b6039bc 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v31'; +const CACHE = 'hub-v32'; const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];