Docs/Skills and code/src/

src/series-viewer/server.js

Code137 lines8.3 KBsrc/

What the file says about itself

Series-analysis viewer: browse the videos and channels found for each series, open a video to see its title, thumbnail, description, transcript and teardown, and generate a teardown with a button if none exists.

Mentioned on: page 12

// Series-analysis viewer: browse the videos and channels found for each series, open a video to see its
// title, thumbnail, description, transcript and teardown, and generate a teardown with a button if none exists.
//   npm run series:viewer      (http://localhost:3100, override with SERIES_VIEWER_PORT)
import express from "express";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { openStore } from "../series-analysis/store.js";
import { findTranscriptFile, parseTranscriptFile, readTeardown, jobFor, startTeardown } from "./teardown-job.js";

const here = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(here, "..", "..");
const PORT = Number(process.env.SERIES_VIEWER_PORT ?? 3100);
const db = openStore();
const app = express();

const specs = () => {
  const dir = path.join(ROOT, "series");
  if (!fs.existsSync(dir)) return [];
  return fs.readdirSync(dir).filter(f => f.endsWith(".json")).map(f => {
    try { return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); } catch { return null; }
  }).filter(Boolean);
};

const latestRun = series => db.prepare("SELECT run_id FROM series_rankings WHERE series_id=? ORDER BY run_id DESC LIMIT 1").get(series)?.run_id ?? null;

/** Video ids that already have a teardown on disk (batch run or viewer-generated). */
function teardownIds(series) {
  const base = path.join(ROOT, "transcripts", "series-analysis", series, "teardown");
  const ids = new Set();
  try {
    const manifest = JSON.parse(fs.readFileSync(path.join(base, "manifest.json"), "utf8"));
    for (const c of manifest.cases) if (fs.existsSync(path.join(base, "analyses", `${c.cid}.json`))) ids.add(c.video_id);
  } catch { /* no batch run */ }
  const single = path.join(base, "single");
  if (fs.existsSync(single)) for (const id of fs.readdirSync(single)) if (fs.existsSync(path.join(single, id, "analysis.json")) && jobFor(series, id)?.status !== "running") ids.add(id);
  return ids;
}

function transcriptIds(series) {
  const ids = new Set(db.prepare("SELECT video_id FROM series_transcripts WHERE series_id=? AND status='ok'").all(series).map(r => r.video_id));
  const base = path.join(ROOT, "transcripts", "series-analysis", series);
  for (const group of ["cohort-new", "cohort-established", "viewer"]) {
    const dir = path.join(base, group);
    if (!fs.existsSync(dir)) continue;
    for (const f of fs.readdirSync(dir)) { const m = f.match(/([\w-]{11})\.txt$/); if (m) ids.add(m[1]); }
  }
  return ids;
}

app.get("/api/series", (_req, res) => {
  const out = specs().map(s => {
    const c = db.prepare("SELECT COUNT(*) videos, COUNT(DISTINCT COALESCE(channel_id, channel_url, channel_name)) channels FROM series_videos WHERE series_id=?").get(s.id);
    return { id: s.id, name: s.name, premise: s.premise, ecosystem: s.ecosystem, videos: c.videos, channels: c.channels, teardowns: teardownIds(s.id).size, transcripts: transcriptIds(s.id).size };
  });
  res.json(out);
});

app.get("/api/series/:s/videos", (req, res) => {
  const S = req.params.s;
  const run = latestRun(S);
  const rows = db.prepare(`
    SELECT v.video_id, v.title, v.channel_name, COALESCE(v.channel_id, v.channel_url, v.channel_name) AS channel_key,
           COALESCE(v.views, v.search_views) AS views, v.likes, v.published_at, v.search_published_text,
           COALESCE(v.duration_seconds, v.search_duration_seconds) AS duration_seconds, v.info_status,
           r.rank, r.selected, r.tier, r.learn_score, r.fit, r.performance, r.learnability, r.excluded, r.detail_json,
           f.fit AS llm_fit, f.subject AS llm_subject
    FROM series_videos v
    LEFT JOIN series_rankings r ON r.series_id=v.series_id AND r.video_id=v.video_id AND r.run_id=?
    LEFT JOIN series_llm_fit f ON f.series_id=v.series_id AND f.video_id=v.video_id
    WHERE v.series_id=?`).all(run, S);
  const td = teardownIds(S), tr = transcriptIds(S);
  res.json(rows.map(r => {
    const d = r.detail_json ? JSON.parse(r.detail_json) : {};
    const { detail_json, ...rest } = r;
    return { ...rest, subject: d.subject ?? r.llm_subject ?? null, outlier_ratio: d.outlier_ratio ?? null, subscribers: d.subscribers ?? null, views_per_day: d.views_per_day ?? null,
      age_days: d.age_days ?? null, fit_heuristic: d.fit ?? null, has_teardown: td.has(r.video_id), has_transcript: tr.has(r.video_id), job: jobFor(S, r.video_id)?.status ?? null };
  }));
});

app.get("/api/series/:s/channels", (req, res) => {
  const S = req.params.s;
  const rows = db.prepare(`
    SELECT COALESCE(v.channel_id, v.channel_url, v.channel_name) AS channel_key, MAX(v.channel_name) AS name, MAX(v.channel_url) AS channel_url,
           COUNT(*) AS videos, MAX(COALESCE(v.views, v.search_views)) AS best_views, SUM(r.selected) AS selected
    FROM series_videos v
    LEFT JOIN series_rankings r ON r.series_id=v.series_id AND r.video_id=v.video_id AND r.run_id=?
    WHERE v.series_id=? GROUP BY channel_key`).all(latestRun(S), S);
  const info = new Map(db.prepare("SELECT * FROM series_channels WHERE series_id=?").all(S).map(c => [c.channel_key, c]));
  const td = teardownIds(S);
  const tdByChannel = new Map();
  for (const v of db.prepare("SELECT video_id, COALESCE(channel_id, channel_url, channel_name) AS k FROM series_videos WHERE series_id=?").all(S))
    if (td.has(v.video_id)) tdByChannel.set(v.k, (tdByChannel.get(v.k) ?? 0) + 1);
  res.json(rows.map(r => {
    const c = info.get(r.channel_key) ?? {};
    return { ...r, name: c.name ?? r.name, handle: c.handle ?? null, description: c.description ?? null, country: c.country ?? null, subscribers: c.subscriber_count ?? null,
      total_videos: c.video_count ?? null, total_views: c.total_view_count ?? null, joined: c.joined_date ?? null, verified: c.is_verified ?? null,
      baseline_median_views: c.baseline_median_views ?? null, baseline_p75_views: c.baseline_p75_views ?? null, enriched: c.status === "ok", teardowns: tdByChannel.get(r.channel_key) ?? 0 };
  }));
});

app.get("/api/series/:s/videos/:id", (req, res) => {
  const { s: S, id } = req.params;
  const v = db.prepare("SELECT * FROM series_videos WHERE series_id=? AND video_id=?").get(S, id);
  if (!v) return res.status(404).json({ error: "Unknown video" });
  const run = latestRun(S);
  const rk = run && db.prepare("SELECT * FROM series_rankings WHERE series_id=? AND run_id=? AND video_id=?").get(S, run, id);
  const detail = rk?.detail_json ? JSON.parse(rk.detail_json) : null;
  const channel = db.prepare("SELECT * FROM series_channels WHERE series_id=? AND channel_key=?").get(S, v.channel_id ?? v.channel_url ?? v.channel_name);
  const queries = db.prepare("SELECT query, position FROM series_hits WHERE series_id=? AND video_id=? ORDER BY position LIMIT 15").all(S, id);
  const llm = db.prepare("SELECT fit, subject, packaging, note FROM series_llm_fit WHERE series_id=? AND video_id=?").get(S, id) ?? null;
  const tFile = findTranscriptFile(S, id, db);
  const transcript = tFile ? parseTranscriptFile(tFile) : null;
  const { detail_json, ...ranking } = rk ?? {};
  res.json({ video: v, ranking: rk ? { ...ranking, detail } : null, channel: channel ?? null, queries, llm, transcript, teardown: readTeardown(S, id), job: jobFor(S, id) });
});

app.post("/api/series/:s/videos/:id/teardown", (req, res) => {
  const { s: S, id } = req.params;
  const v = db.prepare("SELECT * FROM series_videos WHERE series_id=? AND video_id=?").get(S, id);
  if (!v) return res.status(404).json({ error: "Unknown video" });
  const rk = db.prepare("SELECT detail_json FROM series_rankings WHERE series_id=? AND video_id=? ORDER BY run_id DESC LIMIT 1").get(S, id);
  const job = startTeardown(S, id, { ...v, views: v.views ?? v.search_views, detail: rk?.detail_json ? JSON.parse(rk.detail_json) : null });
  res.status(202).json(job);
});

app.get("/api/series/:s/videos/:id/teardown", (req, res) => {
  const { s: S, id } = req.params;
  res.json({ job: jobFor(S, id), teardown: readTeardown(S, id) });
});

app.use(express.static(path.join(here, "public")));
// Every page is served by the same shell; the client reads the path.
app.get(/^\/(s|series)\/.*/, (_req, res) => res.sendFile(path.join(here, "public", "index.html")));

app.listen(PORT, () => console.log(`[series-viewer] http://localhost:${PORT}`));

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.