Transcripts: say why a show's own transcript was not found; read Podgrab fields whatever their capitals
build-container / build (push) Successful in 4m2s

This commit is contained in:
nmemmert committed 2026-10-23 11:42:43 -04:00
1 parent ae00f61711
commit 747002924d
5 files changed
+27 -16

No files matched your search

+21 -11
View File
@@ -50,18 +50,23 @@ function transcriptLinks(feedXml, guid, fileUrl) {
const unwrap = x => String(x || '').replace(/<!\[CDATA\[|\]\]>/g, '').trim();
const item = items.find(b => { const g = /<guid[^>]*>([\s\S]*?)<\/guid>/i.exec(b); return g && guid && unwrap(g[1]) === guid; })
|| items.find(b => { const e = /<enclosure[^>]*>/i.exec(b); return e && fileUrl && attr(e[0], 'url') === fileUrl; });
if (!item) return [];
if (!item) return null;
return [...item.matchAll(/<podcast:transcript\s+([^>]*?)\/?>/gi)].map(m => ({ url: attr(m[1], 'url'), type: attr(m[1], 'type').toLowerCase() })).filter(x => x.url);
}
/** { transcript } when the show publishes one we can read, otherwise { why }: the reason, so a missing transcript can be explained. */
async function nativeTranscript(info) {
if (!info || !info.feedUrl) return null;
const links = transcriptLinks(await getText(info.feedUrl), info.guid, info.fileUrl);
if (!info) return { why: 'Could not read this episode from the podcast service.' };
if (!info.feedUrl) return { why: 'The podcast service did not give the show\'s feed address.' };
let xml; try { xml = await getText(info.feedUrl); } catch (e) { return { why: 'Could not download the show\'s feed (' + e.message + ').' }; }
const links = transcriptLinks(xml, info.guid, info.fileUrl);
if (!links) return { why: 'Could not find this episode in the show\'s feed (looked for ' + (info.guid ? 'id ' + String(info.guid).slice(0, 60) : 'its audio address') + ').' };
if (!links.length) return { why: 'The show\'s feed lists no transcript for this episode.' };
const rank = l => /json/.test(l.type) ? 0 : /vtt/.test(l.type) ? 1 : /subrip|srt/.test(l.type) ? 2 : 9;
for (const l of links.sort((a, b) => rank(a) - rank(b))) {
if (rank(l) > 2) continue; // (plain HTML or text has no times: not worth showing as a transcript)
try { const t = await getText(l.url), lines = /json/.test(l.type) ? parseJson(t) : parseCues(t); const r = shape(lines, 'Published by the show'); if (r) return r; } catch {}
try { const t = await getText(l.url), lines = /json/.test(l.type) ? parseJson(t) : parseCues(t); const r = shape(lines, 'Published by the show'); if (r) return { transcript: r }; } catch {}
}
return null;
return { why: 'The show lists a transcript (' + links.map(l => l.type || 'unknown type').join(', ') + ') but not in a timed format this can follow, or it could not be downloaded.' };
}
// ---- speech to text on this server
@@ -110,7 +115,7 @@ function startJob(key, src, cookie) {
}
const SRC_OK = [/^\/api\/podcast\/podcastitems\/[\w-]+\/file$/, /^\/api\/ab\/books\/[^?#\s]+$/]; // only audio this hub already serves to the person
const nativeMiss = new Map(); // key -> time: a show with no transcript is not asked again for a day
const nativeMiss = new Map(); // key -> { t, why }: a show with no transcript is not asked again for a day
async function handle(req, res) {
const u = new URL(req.url, 'http://x'), q = u.searchParams, key0 = String(q.get('key') || ''), src = String(q.get('src') || '');
const kind = key0.startsWith('pod:') ? 'pod' : key0.startsWith('ab:') ? 'ab' : '';
@@ -124,12 +129,17 @@ async function handle(req, res) {
if (!cfg.api) return json(res, 400, { error: 'Transcribing is not set up on this server (see the whisper service in docker-compose.yml)' });
startJob(key, src, req.headers.cookie); return json(res, 200, { state: 'running', since: Date.now(), ...base });
}
if (kind === 'pod' && !(nativeMiss.get(key) > Date.now() - 864e5)) {
let r = null; try { r = await nativeTranscript(await podcast.feedInfo(me, key0.slice(4))); } catch {}
if (r) { atomicWrite(fileOf(key), JSON.stringify(r)); return json(res, 200, { state: 'ready', ...base, transcript: r }); }
nativeMiss.set(key, Date.now());
let why = '';
if (kind === 'pod') {
const miss = nativeMiss.get(key);
if (miss && miss.t > Date.now() - 6 * 3600e3) why = miss.why;
else {
let r; try { r = await nativeTranscript(await podcast.feedInfo(me, key0.slice(4))); } catch (e) { r = { why: 'Checking the show\'s feed failed (' + e.message + ').' }; }
if (r.transcript) { atomicWrite(fileOf(key), JSON.stringify(r.transcript)); return json(res, 200, { state: 'ready', ...base, transcript: r.transcript }); }
why = r.why; nativeMiss.set(key, { t: Date.now(), why });
}
}
return json(res, 200, { state: 'none', ...base });
return json(res, 200, { state: 'none', why, ...base });
} catch (e) { json(res, e.status || 500, { error: e.message }); }
}
module.exports = { init, handle, parseCues, parseJson, transcriptLinks, shape, whisper, getText, nativeTranscript, enabled: () => true };