Docs/Skills and code/skill within-channel-study
.claude/skills/within-channel-study/scripts/lib.js
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.