Docs/Skills and code/skill within-channel-study

.claude/skills/within-channel-study/scripts/lib.js

Code113 lines5.2 KBSkill: within-channel-study

What the file says about itself

Shared helpers: arg parsing, DB lookup (read-only), sampler-compatible ranking and folder naming.

// Shared helpers: arg parsing, DB lookup (read-only), sampler-compatible ranking and folder naming.
const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '../../../..');
const Database = require(path.join(ROOT, 'node_modules', 'better-sqlite3'));

// Must match src/transcript-sampling.js so the probe ranks videos exactly like the sampler did.
const MIN_AGE_DAYS = 14, AGE_FLOOR_DAYS = 30, MIN_ELIGIBLE = 10;

function parseArgs(argv) {
  const out = { _: [] };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (!a.startsWith('--')) { out._.push(a); continue; }
    const eq = a.indexOf('=');
    const key = eq > 0 ? a.slice(2, eq) : a.slice(2);
    const val = eq > 0 ? a.slice(eq + 1) : (argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true);
    out[key] = out[key] === undefined ? val : [].concat(out[key], val);
  }
  return out;
}

function channelFolderName(name) {
  const cleaned = String(name ?? '').replace(/[<>:"/\\|?*\u0000-\u001f]/g, '').replace(/\s+/g, ' ').trim().replace(/[. ]+$/, '').slice(0, 80);
  return cleaned || 'channel';
}

function candidateDbs(dbArg) {
  if (dbArg) return [path.resolve(dbArg)];
  if (process.env.ECOSYSTEM_DB_PATH) return [path.resolve(process.env.ECOSYSTEM_DB_PATH)];
  const dir = path.join(ROOT, 'data');
  return fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.endsWith('.db')).map(f => path.join(dir, f)) : [];
}

const CHANNEL_SQL = `
  SELECT c.channel_url, COALESCE(m.name, c.name) AS name, COALESCE(m.handle, c.handle) AS handle,
    COALESCE(m.subscriber_count, c.subscriber_count) AS subscriber_count,
    COALESCE(m.video_count, c.video_count) AS declared_videos
  FROM channels c LEFT JOIN channel_metadata m ON m.channel_url = c.channel_url
  WHERE c.channel_url=@a OR c.canonical_url=@a OR c.handle=@a OR lower(c.name)=lower(@a) OR lower(COALESCE(m.name,''))=lower(@a) LIMIT 1`;

/** Returns [{dbPath, db, channel}] for every candidate DB that knows the channel. */
function findChannel(channelArg, dbArg) {
  const hits = [];
  for (const dbPath of candidateDbs(dbArg)) {
    if (!fs.existsSync(dbPath)) continue;
    let db;
    try { db = new Database(dbPath, { readonly: true, fileMustExist: true }); } catch { continue; }
    try {
      const channel = db.prepare(CHANNEL_SQL).get({ a: channelArg });
      if (channel) { hits.push({ dbPath, db, channel }); continue; }
    } catch { /* DB without a channels table */ }
    db.close();
  }
  return hits;
}

function loadVideos(db, channelUrl) {
  return db.prepare(`
    SELECT video_id, title, url, view_count, published_at_normalized, published_at_precision, duration_seconds, likes_count, category, description
    FROM videos WHERE channel_url=@u OR video_id IN (SELECT video_id FROM channel_videos WHERE channel_url=@u)`).all({ u: channelUrl });
}

function median(xs) {
  const s = xs.filter(Number.isFinite).sort((a, b) => a - b);
  if (!s.length) return null;
  const m = Math.floor(s.length / 2);
  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
}
function quantile(xs, q) {
  const s = xs.filter(Number.isFinite).sort((a, b) => a - b);
  if (!s.length) return null;
  const pos = (s.length - 1) * q, lo = Math.floor(pos), hi = Math.ceil(pos);
  return s[lo] + (s[hi] - s[lo]) * (pos - lo);
}

/** Same eligibility / views-per-day definition as the sampler. */
function rankVideos(videos, now = Date.now()) {
  const dated = videos
    .filter(v => Number.isFinite(Number(v.view_count)) && v.published_at_normalized && Number.isFinite(Date.parse(v.published_at_normalized)))
    .map(v => ({ ...v, view_count: Number(v.view_count), age_days: (now - Date.parse(v.published_at_normalized)) / 86400000 }));
  let pool = dated.filter(v => v.age_days >= MIN_AGE_DAYS), fallback = false;
  if (pool.length < MIN_ELIGIBLE) { pool = dated; fallback = true; }
  const ranked = pool.map(v => ({ ...v, vpd: v.view_count / Math.max(v.age_days, AGE_FLOOR_DAYS) })).sort((a, b) => b.vpd - a.vpd);
  ranked.forEach((v, i) => { v.rank = i + 1; v.rank_total = ranked.length; });
  return { ranked, fallback, excluded: videos.length - ranked.length };
}

function parseTranscriptFile(file) {
  const raw = fs.readFileSync(file, 'utf8');
  const tIdx = raw.indexOf('=== TRANSCRIPT ===');
  const header = tIdx < 0 ? raw : raw.slice(0, tIdx);
  const field = label => (header.match(new RegExp('^' + label + ':\\s*(.*)$', 'm')) || [])[1]?.trim() ?? null;
  const segs = [];
  for (const line of (tIdx < 0 ? '' : raw.slice(tIdx)).split(/\r?\n/)) {
    const m = line.match(/^\[(\d{1,2}(?::\d{2}){1,2})\]\s*(.*)$/);
    if (!m) continue;
    const p = m[1].split(':').map(Number);
    const t = p.length === 3 ? p[0] * 3600 + p[1] * 60 + p[2] : p[0] * 60 + p[1];
    segs.push({ ts: m[1], t, text: m[2] });
  }
  return { file, header, field, segs };
}

const fmtTs = s => {
  s = Math.round(s);
  const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
  return (h ? h + ':' : '') + String(m).padStart(h ? 2 : 1, '0') + ':' + String(s % 60).padStart(2, '0');
};

module.exports = { ROOT, MIN_AGE_DAYS, AGE_FLOOR_DAYS, MIN_ELIGIBLE, parseArgs, channelFolderName, findChannel, loadVideos, median, quantile, rankVideos, parseTranscriptFile, fmtTs };

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.