Docs/Skills and code/src/
src/viewer.js
import express from "express";
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { config } from "../config.js";
import { getDb } from "./db.js";
import { ensureResearchSchema } from "./analysis/research-schema.js";
import { registerChannelExplorer } from "./channel-explorer.js";
import { registerVideoExplorer } from "./video-explorer.js";
import { createViewerDatabaseRegistry } from "./viewer-database-registry.js";
import { TRANSCRIPT_ROOT, SAMPLE_FILE_PATTERN, channelFolderName } from "./transcript-sampling.js";
const app = express();
const defaultDb = getDb();
ensureResearchSchema();
const databaseRegistry = createViewerDatabaseRegistry(defaultDb, config.dbPath);
let activeDb = defaultDb;
let activeDatabaseName = databaseRegistry.defaultName;
// Existing viewer routes can keep using `db`; the proxy delegates each operation
// to the request-selected database. Legacy editorial routes stay on the configured DB.
const db = new Proxy({}, { get(_target, property) {
const value = activeDb[property];
return typeof value === "function" ? value.bind(activeDb) : value;
}});
app.use(express.json({ limit: "32kb" }));
const PORT = config.viewer?.port ?? 3000;
function readCookie(request, key) {
const pair = String(request.headers.cookie ?? "").split(";").map(x => x.trim()).find(x => x.startsWith(`${key}=`));
if (!pair) return "";
try { return decodeURIComponent(pair.slice(key.length + 1)); } catch { return ""; }
}
app.use((request, response, next) => {
// Database switching is scoped to the two explorers. The older editorial pages
// remain on config.dbPath because they rely on extra project-specific tables.
if (request.path.startsWith("/explore")) {
const requested = String(request.query.db ?? readCookie(request, "ecosystem_db") ?? "");
const entry = requested ? databaseRegistry.get(requested) : databaseRegistry.get(databaseRegistry.defaultName);
if (request.query.db && !entry) return response.status(400).type("text").send("Unknown or incompatible database. Choose a validated database from the selector.");
const selected = entry ?? databaseRegistry.get(databaseRegistry.defaultName);
activeDb = selected.db;
activeDatabaseName = selected.name;
if (request.query.db) response.setHeader("Set-Cookie", `ecosystem_db=${encodeURIComponent(selected.name)}; Path=/; SameSite=Lax; HttpOnly`);
} else {
activeDb = defaultDb;
activeDatabaseName = databaseRegistry.defaultName;
}
next();
});
function escapeHtml(value) {
return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<")
.replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
}
function formatCount(value) {
if (value == null || value === "") return "—";
return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value);
}
function getVideoFilters(query) {
return { text: String(query.q ?? "").trim(), channel: String(query.channel ?? "").trim(),
minViews: Math.max(0, Number(query.minViews ?? 0) || 0), sort: String(query.sort ?? "views") };
}
function getVideos(filters) {
const conditions = [];
const params = {};
if (filters.text) { conditions.push("(v.title LIKE @text OR v.channel_name LIKE @text)"); params.text = `%${filters.text}%`; }
if (filters.channel) { conditions.push("v.channel_name = @channel"); params.channel = filters.channel; }
if (filters.minViews > 0) { conditions.push("COALESCE(v.view_count,0) >= @minViews"); params.minViews = filters.minViews; }
const orderBy = { views:"v.view_count DESC", newest:"v.published_at DESC", oldest:"v.published_at ASC", discovered:"v.discovered_at DESC" }[filters.sort] ?? "v.view_count DESC";
return db.prepare(`
SELECT v.video_id,v.title,v.url,v.channel_name,v.channel_url,v.thumbnail_url,v.duration,v.view_count,v.published_at,
ea.important AS editorial_important, ea.editorial_status, ea.note AS editorial_note,
GROUP_CONCAT(DISTINCT s.query) AS discovery_queries
FROM videos v
LEFT JOIN video_searches vs ON vs.video_id=v.video_id
LEFT JOIN searches s ON s.id=vs.search_id
LEFT JOIN editorial_annotations ea ON ea.object_type='video' AND ea.object_id=v.video_id
${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
GROUP BY v.video_id ORDER BY ${orderBy} LIMIT 300
`).all(params);
}
function getChannelFilters(query) {
return { text: String(query.q ?? "").trim(), status: String(query.status ?? "").trim(),
priority: String(query.priority ?? "").trim(), sort: String(query.sort ?? "important") };
}
function getChannelRows(filters) {
const where = [];
const params = {};
if (filters.text) { where.push("(c.name LIKE @text OR c.channel_url LIKE @text OR COALESCE(m.description,c.description,'') LIKE @text)"); params.text = `%${filters.text}%`; }
if (filters.status) { where.push("COALESCE(ea.editorial_status,'unknown') = @status"); params.status = filters.status; }
if (filters.priority === "important") where.push("COALESCE(ea.important,0)=1");
if (filters.priority === "unreviewed") where.push("(ea.object_id IS NULL OR COALESCE(ea.editorial_status,'unknown')='unknown')");
const orderBy = {
important: "COALESCE(ea.important,0) DESC, CASE COALESCE(ea.editorial_status,'unknown') WHEN 'core' THEN 0 WHEN 'adjacent' THEN 1 WHEN 'unknown' THEN 2 ELSE 3 END, COALESCE(m.subscriber_count,c.subscriber_count,0) DESC",
subscribers: "COALESCE(m.subscriber_count,c.subscriber_count,0) DESC",
name: "c.name COLLATE NOCASE ASC", discovered: "c.discovered_at DESC"
}[filters.sort] ?? "COALESCE(ea.important,0) DESC, c.name COLLATE NOCASE ASC";
return db.prepare(`
SELECT c.channel_url,c.name,c.discovered_at,
COALESCE(m.description,c.description) AS description,
COALESCE(m.subscriber_count,c.subscriber_count) AS subscribers,
COALESCE(m.subscriber_count_text,c.subscriber_count_text) AS subscriber_text,
COALESCE(m.video_count,c.video_count) AS declared_videos,
q.classification AS machine_classification,q.score AS machine_score,
cp.generated_at AS profiled_at,
ea.important AS editorial_important,ea.editorial_status,ea.reasons_json,ea.note AS editorial_note,
(SELECT COUNT(*) FROM channel_videos cv WHERE cv.channel_url=c.channel_url) AS sampled_videos,
(SELECT COUNT(*) FROM channel_concepts cc WHERE cc.channel_url=c.channel_url) AS concept_links,
(SELECT COUNT(*) FROM channel_patterns pat WHERE pat.channel_url=c.channel_url) AS pattern_links
FROM channels c
LEFT JOIN channel_metadata m ON m.channel_url=c.channel_url
LEFT JOIN channel_qualifications q ON q.channel_url=c.channel_url
LEFT JOIN channel_profiles cp ON cp.channel_url=c.channel_url
LEFT JOIN editorial_annotations ea ON ea.object_type='channel' AND ea.object_id=c.channel_url
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
ORDER BY ${orderBy} LIMIT 500
`).all(params);
}
function discoveryQueries() {
return db.prepare(`SELECT s.query,COUNT(DISTINCT vs.video_id) AS video_count FROM searches s LEFT JOIN video_searches vs ON vs.search_id=s.id GROUP BY s.id ORDER BY s.query`).all();
}
function channelNames() {
return db.prepare(`SELECT channel_name,COUNT(*) AS n FROM videos WHERE channel_name IS NOT NULL GROUP BY channel_name ORDER BY channel_name`).all();
}
function nav(active, withDatabaseSelector = true) {
const links = `<a class="${active === 'channels' ? 'active' : ''}" href="/channels">Channels & editorial review</a><a class="${active === 'explore' ? 'active' : ''}" href="/explore">Channel Explorer</a><a class="${active === 'video-explorer' ? 'active' : ''}" href="/explore/videos">Video Explorer</a><a class="${active === 'videos' ? 'active' : ''}" href="/">Videos</a><a href="/health">Data health</a>`;
const selector = withDatabaseSelector ? `<label class="db-switch">Explore database <select id="ecosystem-db-select" aria-label="Choose ecosystem database">${databaseRegistry.list().map(item => `<option value="${escapeHtml(item.name)}" ${item.name === activeDatabaseName ? "selected" : ""}>${escapeHtml(item.name)}${item.isDefault ? " · configured default" : ""}</option>`).join("")}</select></label><script>document.getElementById('ecosystem-db-select')?.addEventListener('change',e=>{const u=new URL(location.href);u.searchParams.set('db',e.target.value);u.searchParams.delete('refresh');location.href=u.toString();});</script>` : "";
return `<nav>${links}${selector}</nav>`;
}
function renderVideo(video) {
const queries = video.discovery_queries ? video.discovery_queries.split(",") : [];
return `<article class="video">
<a class="thumbnail" href="${escapeHtml(video.url)}" target="_blank" rel="noopener noreferrer">${video.thumbnail_url ? `<img src="${escapeHtml(video.thumbnail_url)}" alt="" loading="lazy">` : ""}</a>
<div class="video-info">
<a class="title" href="${escapeHtml(video.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(video.title)}</a>
<a class="channel" href="/channels?q=${encodeURIComponent(video.channel_name ?? "")}">${escapeHtml(video.channel_name)}</a>
<div class="metadata">${escapeHtml(video.duration ?? "Unknown duration")} · ${formatCount(video.view_count)} views · ${escapeHtml(video.published_at ?? "date unknown")}</div>
<div class="discovery">${queries.map(q => `<span class="tag">${escapeHtml(q)}</span>`).join("")}</div>
<div class="annotation-row">
<label><input type="checkbox" data-annotate="important" data-type="video" data-id="${escapeHtml(video.video_id)}" ${video.editorial_important ? "checked" : ""}> Important</label>
<select data-annotate="status" data-type="video" data-id="${escapeHtml(video.video_id)}">
${["unknown","core","adjacent","outside"].map(s => `<option value="${s}" ${((video.editorial_status ?? "unknown") === s) ? "selected" : ""}>${s}</option>`).join("")}
</select>
<span class="save-state" data-state-for="video:${escapeHtml(video.video_id)}"></span>
</div>
</div>
</article>`;
}
function renderChannel(channel) {
const status = channel.editorial_status ?? "unknown";
return `<article class="channel-card">
<div class="channel-heading">
<div><a class="title" href="${escapeHtml(channel.channel_url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(channel.name)}</a>
<div class="metadata">${formatCount(channel.subscribers)} subscribers · ${formatCount(channel.declared_videos)} reported videos · ${channel.sampled_videos} sampled links</div></div>
<label class="important-toggle"><input type="checkbox" data-annotate="important" data-type="channel" data-id="${escapeHtml(channel.channel_url)}" ${channel.editorial_important ? "checked" : ""}> ★ Important</label>
</div>
<div class="channel-description">${escapeHtml((channel.description ?? "").slice(0, 360))}${(channel.description ?? "").length > 360 ? "…" : ""}</div>
<div class="meta-tags"><span class="tag">Machine: ${escapeHtml(channel.machine_classification ?? "unclassified")}</span><span class="tag">Profile: ${channel.profiled_at ? "yes" : "no"}</span><span class="tag">${channel.concept_links} concept links</span><span class="tag">${channel.pattern_links} pattern links</span></div>
<div class="annotation-controls">
<label>Status <select data-annotate="status" data-type="channel" data-id="${escapeHtml(channel.channel_url)}">${["unknown","core","adjacent","outside"].map(s => `<option value="${s}" ${status === s ? "selected" : ""}>${s}</option>`).join("")}</select></label>
<label class="note-label">Editorial note <input type="text" data-annotate="note" data-type="channel" data-id="${escapeHtml(channel.channel_url)}" value="${escapeHtml(channel.editorial_note ?? "")}" placeholder="Why is this channel useful?" maxlength="1000"></label>
<button type="button" data-save-channel="${escapeHtml(channel.channel_url)}">Save annotation</button>
<span class="save-state" data-state-for="channel:${escapeHtml(channel.channel_url)}"></span>
</div>
</article>`;
}
function baseHead(title) {
return `<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escapeHtml(title)}</title><style>
*{box-sizing:border-box}body{margin:0;padding:24px;background:#f5f5f5;color:#151515;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}header,.filters,main,nav{max-width:1280px;margin-left:auto;margin-right:auto}header{margin-bottom:18px}h1{font-size:28px;margin:0 0 5px}.summary,.metadata{font-size:13px;color:#666}nav{display:flex;gap:8px;margin-bottom:18px;align-items:center;flex-wrap:wrap}.db-switch{margin-left:auto;display:flex;align-items:center;gap:8px;padding:7px 9px;border:1px solid #dce1e7;border-radius:8px;background:#fff;font-size:12px;color:#536071}.db-switch select{max-width:240px;padding:6px;font-size:12px}nav a{padding:9px 12px;border:1px solid #ddd;border-radius:8px;background:white;color:#222;text-decoration:none}nav a.active{background:#222;color:white}.filters{background:white;border:1px solid #ddd;border-radius:10px;padding:14px;margin-bottom:18px}.filter-grid{display:grid;grid-template-columns:2fr 1fr 1fr 1fr auto;gap:9px}input,select,button{font:inherit;border:1px solid #ccc;border-radius:6px;padding:8px;background:white;color:#111;min-height:36px}button{cursor:pointer;background:#171717;color:white;border-color:#171717}input[type=checkbox]{min-height:auto;width:17px;height:17px;vertical-align:middle}.videos,.channels{display:grid;gap:12px}.video{display:grid;grid-template-columns:260px 1fr;gap:16px;background:white;border:1px solid #ddd;border-radius:10px;padding:12px}.thumbnail{display:block;aspect-ratio:16/9;background:#ddd;border-radius:6px;overflow:hidden}.thumbnail img{width:100%;height:100%;object-fit:cover}.video-info{display:flex;flex-direction:column;align-items:flex-start;gap:7px;min-width:0}.title{font-size:18px;font-weight:650;line-height:1.3;color:#111;text-decoration:none}.title:hover{text-decoration:underline}.channel{font-size:14px;color:#555;text-decoration:none}.tag{display:inline-block;padding:3px 7px;background:#eee;color:#444;border-radius:5px;font-size:12px;margin:2px}.annotation-row,.annotation-controls,.channel-heading{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.annotation-row{margin-top:5px}.channel-card{padding:15px;background:white;border:1px solid #ddd;border-radius:10px}.channel-heading{justify-content:space-between}.important-toggle{white-space:nowrap}.channel-description{font-size:13px;color:#555;line-height:1.45;margin:10px 0}.meta-tags{margin-bottom:10px}.annotation-controls label{display:flex;gap:7px;align-items:center;font-size:13px}.annotation-controls .note-label{flex:1;min-width:250px}.note-label input{flex:1;min-width:150px}.save-state{font-size:12px;color:#287341}.empty{padding:35px;text-align:center;background:white;border:1px solid #ddd;border-radius:10px;color:#666}.small{font-size:12px;color:#666;margin-top:10px}@media(max-width:850px){.filter-grid{grid-template-columns:1fr 1fr}.video{grid-template-columns:1fr}}@media(max-width:550px){body{padding:12px}.filter-grid{grid-template-columns:1fr}.video{grid-template-columns:1fr}}
</style></head>`;
}
function renderVideoPage(filters, videos) {
const discoveries = discoveryQueries(); const channels = channelNames();
return `<!doctype html><html lang="en">${baseHead("YouTube Ecosystem Intelligence — Videos")}<body><header><h1>YouTube Ecosystem Intelligence</h1><div class="summary">${videos.length} matching videos (maximum 300 shown). Editorial annotations are stored separately from machine observations.</div></header>${nav("videos", false)}
<form class="filters" method="GET"><div class="filter-grid"><input name="q" placeholder="Search title or channel" value="${escapeHtml(filters.text)}"><select name="channel"><option value="">All channels</option>${channels.map(c=>`<option value="${escapeHtml(c.channel_name)}" ${filters.channel===c.channel_name?"selected":""}>${escapeHtml(c.channel_name)} (${c.n})</option>`).join("")}</select><select name="sort"><option value="views" ${filters.sort==="views"?"selected":""}>Most views</option><option value="newest" ${filters.sort==="newest"?"selected":""}>Newest</option><option value="oldest" ${filters.sort==="oldest"?"selected":""}>Oldest</option><option value="discovered" ${filters.sort==="discovered"?"selected":""}>Recently discovered</option></select><input type="number" name="minViews" min="0" placeholder="Minimum views" value="${filters.minViews || ""}"><button type="submit">Filter</button></div><div class="small">Discovery queries: ${discoveries.map(d=>`${escapeHtml(d.query)} (${d.video_count})`).join(" · ")}</div></form>
<main class="videos">${videos.length ? videos.map(renderVideo).join("") : `<div class="empty">No videos match these filters.</div>`}</main>${clientScript()}</body></html>`;
}
function renderChannelPage(filters, channels) {
return `<!doctype html><html lang="en">${baseHead("YouTube Ecosystem Intelligence — Channels")}<body><header><h1>Channel research & editorial review</h1><div class="summary">${channels.length} channels shown. Your annotations do not overwrite automated qualification or extracted evidence.</div></header>${nav("channels", false)}
<form class="filters" method="GET"><div class="filter-grid"><input name="q" placeholder="Search channel, handle, description" value="${escapeHtml(filters.text)}"><select name="status"><option value="">All editorial statuses</option>${["unknown","core","adjacent","outside"].map(s=>`<option value="${s}" ${filters.status===s?"selected":""}>${s}</option>`).join("")}</select><select name="priority"><option value="">All priorities</option><option value="important" ${filters.priority==="important"?"selected":""}>Important only</option><option value="unreviewed" ${filters.priority==="unreviewed"?"selected":""}>Unreviewed / unknown</option></select><select name="sort"><option value="important" ${filters.sort==="important"?"selected":""}>Editorial priority</option><option value="subscribers" ${filters.sort==="subscribers"?"selected":""}>Subscribers</option><option value="name" ${filters.sort==="name"?"selected":""}>Name</option><option value="discovered" ${filters.sort==="discovered"?"selected":""}>Recently discovered</option></select><button type="submit">Filter</button></div></form>
<main class="channels">${channels.length ? channels.map(renderChannel).join("") : `<div class="empty">No channels match these filters.</div>`}</main>${clientScript()}</body></html>`;
}
function clientScript() {
return `<script>
async function saveAnnotation(type,id,patch){const state=document.querySelector('[data-state-for="'+type+':'+CSS.escape(id)+'"]');if(state)state.textContent='Saving…';try{const response=await fetch('/api/annotations',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type,id,...patch})});const data=await response.json();if(!response.ok)throw new Error(data.error||'Save failed');if(state)state.textContent='Saved';}catch(error){if(state)state.textContent='Error: '+error.message;}}
document.querySelectorAll('input[data-annotate="important"]').forEach(el=>el.addEventListener('change',()=>saveAnnotation(el.dataset.type,el.dataset.id,{important:el.checked})));
document.querySelectorAll('select[data-annotate="status"]').forEach(el=>el.addEventListener('change',()=>saveAnnotation(el.dataset.type,el.dataset.id,{status:el.value})));
document.querySelectorAll('button[data-save-channel]').forEach(button=>button.addEventListener('click',()=>{const id=button.dataset.saveChannel;const status=document.querySelector('select[data-annotate="status"][data-type="channel"][data-id="'+CSS.escape(id)+'"]')?.value||'unknown';const note=document.querySelector('input[data-annotate="note"][data-id="'+CSS.escape(id)+'"]')?.value||'';const important=document.querySelector('input[data-annotate="important"][data-type="channel"][data-id="'+CSS.escape(id)+'"]')?.checked||false;saveAnnotation('channel',id,{status,note,important});}));
document.querySelectorAll('input[data-annotate="note"]').forEach(el=>el.addEventListener('keydown',event=>{if(event.key==='Enter'){event.preventDefault();const id=el.dataset.id;const status=document.querySelector('select[data-annotate="status"][data-type="channel"][data-id="'+CSS.escape(id)+'"]')?.value||'unknown';const important=document.querySelector('input[data-annotate="important"][data-type="channel"][data-id="'+CSS.escape(id)+'"]')?.checked||false;saveAnnotation('channel',id,{status,note:el.value,important});}}));
</script>`;
}
app.post("/api/annotations", (request, response) => {
try {
const { type, id } = request.body ?? {};
if (!['channel','video'].includes(type) || typeof id !== 'string' || !id.trim()) return response.status(400).json({ error: "type must be channel/video and id must be non-empty" });
const exists = type === 'channel'
? db.prepare("SELECT 1 FROM channels WHERE channel_url=?").get(id)
: db.prepare("SELECT 1 FROM videos WHERE video_id=?").get(id);
if (!exists) return response.status(404).json({ error: `${type} not found: ${id}` });
const prior = db.prepare("SELECT * FROM editorial_annotations WHERE object_type=? AND object_id=?").get(type,id);
const important = request.body.important === undefined ? (prior?.important ?? 0) : (request.body.important ? 1 : 0);
const status = request.body.status === undefined ? (prior?.editorial_status ?? 'unknown') : String(request.body.status);
if (!['unknown','core','adjacent','outside'].includes(status)) return response.status(400).json({ error: "status must be unknown/core/adjacent/outside" });
const note = request.body.note === undefined ? (prior?.note ?? null) : String(request.body.note).slice(0,1000);
const reasons = prior?.reasons_json ?? '[]';
db.prepare(`INSERT INTO editorial_annotations(object_type,object_id,important,editorial_status,reasons_json,note,source,updated_at)
VALUES(?,?,?,?,?,?, 'human', ?) ON CONFLICT(object_type,object_id) DO UPDATE SET
important=excluded.important,editorial_status=excluded.editorial_status,note=excluded.note,source='human',updated_at=excluded.updated_at`)
.run(type,id,important,status,reasons,note,new Date().toISOString());
return response.json({ ok:true,type,id,important:!!important,status,note });
} catch (error) {
console.error('[viewer] Annotation save failed:', error);
return response.status(500).json({ error: "Annotation save failed; inspect the viewer terminal." });
}
});
app.get("/", (request, response) => {
try { const filters=getVideoFilters(request.query); response.send(renderVideoPage(filters,getVideos(filters))); }
catch(error){console.error('[viewer] Video page failed:',error);response.status(500).send('Viewer error. Check terminal.');}
});
app.get("/channels", (request,response)=>{
try { const filters=getChannelFilters(request.query); response.send(renderChannelPage(filters,getChannelRows(filters))); }
catch(error){console.error('[viewer] Channel page failed:',error);response.status(500).send('Channel viewer error. Check terminal.');}
});
app.get("/health", (_request,response)=>{
try {
const counts = {};
for (const table of ['channels','videos','channel_profiles','video_profiles','video_observations','editorial_annotations','concepts','patterns']) {
try { counts[table]=db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n; } catch { counts[table]=null; }
}
response.type('html').send(`<!doctype html><html>${baseHead('YouTube Ecosystem Intelligence — Data health')}<body><header><h1>Data health</h1><div class="summary">Counts and known measurement limitations.</div></header>${nav('', false)}<main class="channel-card"><pre>${escapeHtml(JSON.stringify(counts,null,2))}</pre><p>Editorial status is human judgment. Machine qualification remains a separate recorded field. A single view snapshot cannot establish growth or momentum.</p></main></body></html>`);
} catch(error){console.error('[viewer] Health failed:',error);response.status(500).send('Health page error. Check terminal.');}
});
// ---- Channel transcript sampling (button on the Channel Explorer detail page) ----
// Runs stages/33 as a child process against the database being browsed, one job per channel at a time.
const transcriptJobs = new Map();
const transcriptStage = path.join(path.dirname(fileURLToPath(import.meta.url)), "stages", "33-sample-channel-transcripts.js");
function transcriptStatus(jobKey, channelName) {
const job = transcriptJobs.get(jobKey);
const dir = path.join(TRANSCRIPT_ROOT, channelFolderName(channelName));
let files = [];
try { files = fs.readdirSync(dir).filter(f => SAMPLE_FILE_PATTERN.test(f)).sort(); } catch {}
let logTail = "";
if (job) { try { logTail = fs.readFileSync(job.logPath, "utf8").split(/\r?\n/).filter(Boolean).slice(-12).join("\n"); } catch {} }
return { state: job?.state ?? "idle", startedAt: job?.startedAt ?? null, exitCode: job?.exitCode ?? null, logTail, outputDir: dir, files };
}
function transcriptTarget(request) {
const url = String(request.query.url ?? request.body?.url ?? "").trim();
if (!url || url.length > 2048) return { error: "A channel url is required." };
const row = activeDb.prepare("SELECT channel_url, name FROM channels WHERE channel_url=?").get(url);
if (!row) return { error: "Channel not found in the selected database." };
return { row, jobKey: activeDatabaseName + "|" + row.channel_url };
}
app.get("/explore/api/transcripts", (request, response) => {
const target = transcriptTarget(request);
if (target.error) return response.status(400).json({ error: target.error });
response.json(transcriptStatus(target.jobKey, target.row.name));
});
app.post("/explore/api/transcripts", (request, response) => {
const target = transcriptTarget(request);
if (target.error) return response.status(400).json({ error: target.error });
if (transcriptJobs.get(target.jobKey)?.state === "running") return response.status(409).json({ error: "A transcript job is already running for this channel.", ...transcriptStatus(target.jobKey, target.row.name) });
const logDir = path.join(process.cwd(), "reports", "transcript-jobs");
fs.mkdirSync(logDir, { recursive: true });
const logPath = path.join(logDir, channelFolderName(target.row.name) + "-" + Date.now() + ".log");
const logFd = fs.openSync(logPath, "w");
const job = { state: "running", startedAt: new Date().toISOString(), exitCode: null, logPath };
transcriptJobs.set(target.jobKey, job);
const child = spawn(process.execPath, [transcriptStage, "--channel-url=" + target.row.channel_url], {
stdio: ["ignore", logFd, logFd],
env: { ...process.env, ECOSYSTEM_DB_PATH: databaseRegistry.get(activeDatabaseName).path },
windowsHide: true
});
child.on("error", error => { fs.writeSync(logFd, String(error) + "\n"); job.state = "failed"; });
child.on("close", code => { job.exitCode = code; job.state = code === 0 ? "done" : "failed"; try { fs.closeSync(logFd); } catch {} });
response.status(202).json(transcriptStatus(target.jobKey, target.row.name));
});
registerChannelExplorer(app, () => activeDb, { formatCount, baseHead, nav });
registerVideoExplorer(app, () => activeDb, { baseHead, nav });
app.listen(PORT,()=>console.log(`[viewer] Running at http://localhost:${PORT} (channels: /channels, explorer: /explore, video explorer: /explore/videos)`));
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.