Docs/Skills and code/src/

src/channel-explorer.js

Code423 lines48.7 KBsrc/

const QUEUES = Object.freeze([
  { key: "learn_from", label: "Learn from", table: "channel_queue_learn_from" },
  { key: "breaking_out", label: "Breaking out", table: "channel_queue_breaking_out" },
  { key: "durable_exemplars", label: "Durable exemplars", table: "channel_queue_durable_exemplars" },
  { key: "unexplored_niches", label: "Unexplored niches", table: "channel_queue_unexplored_niches" },
  { key: "reference_inspiration", label: "Reference inspiration", table: "channel_queue_reference_inspiration" }
]);
const PRESETS = Object.freeze({
  young_strong: { label: "Young channels · strong audience", ageMax: "24", minSubs: "50000", maxVideos: "100", sort: "subs_per_video" },
  very_young: { label: "Very young · few uploads", ageMax: "12", minSubs: "10000", maxVideos: "30", sort: "subs_per_video" },
  efficient_growth: { label: "Audience efficiency · subscribers per month", ageMax: "60", minSubs: "10000", sort: "subs_per_month" },
  low_output_big_audience: { label: "Large audience · modest catalogue", minSubs: "100000", maxVideos: "100", sort: "subs_per_video" },
  high_views_few_uploads: { label: "High lifetime views · few uploads", minViews: "1000000", maxVideos: "100", sort: "views_per_video" },
  high_median_video: { label: "Strong typical video · high median views", minMedianVideoViews: "10000", sort: "median_video_views" },
  breakout_efficiency: { label: "Few uploads · strong typical video", minMedianVideoViews: "1000", maxVideos: "100", sort: "median_video_views" },
  consistent_performers: { label: "Consistent performers · low volatility", minMedianVideoViews: "1000", minSavedVideos: "5", maxVideoViewCv: "0.8", sort: "median_video_views" },
  one_hit_dependency: { label: "One-hit dependency · top video dominates", minSavedVideos: "5", minTopVideoShare: "0.5", sort: "top_video_share" },
  durable_catalogue: { label: "Durable catalogue · established channel", ageMin: "36", minViews: "1000000", sort: "views_per_video" },
  queue_strength: { label: "Strong queue score", queue: "breaking_out", minQueueScore: "60", minConfidence: "50", sort: "queue_score" },
  broad_exploration: { label: "Broad exploration", sort: "subscribers" }
});

// Expensive per-channel video distributions are computed once per open database.
// Filter/preset changes then reuse these aggregates instead of rescanning every video.
const videoStatsCache = new WeakMap();
const qualityCache = new WeakMap();
const schemaCache = new WeakMap();
const channelDatasetCache = new WeakMap();
function getVideoStats(db, schema) {
  const cached = videoStatsCache.get(db);
  if (cached) return cached;
  const videoCols = columns(db, "videos");
  const byChannel = new Map();
  const push = (channel, value) => {
    if (!channel || value == null || !Number.isFinite(Number(value))) return;
    if (!byChannel.has(channel)) byChannel.set(channel, []);
    byChannel.get(channel).push(Number(value));
  };
  if (videoCols.has("channel_url") && videoCols.has("view_count")) {
    for (const row of db.prepare("SELECT channel_url, view_count FROM videos WHERE channel_url IS NOT NULL AND view_count IS NOT NULL").all()) push(row.channel_url, row.view_count);
  } else if (schema.channelVideoCols.has("channel_url") && schema.channelVideoCols.has("video_id") && videoCols.has("video_id") && videoCols.has("view_count")) {
    for (const row of db.prepare("SELECT cv.channel_url, v.view_count FROM channel_videos cv JOIN videos v ON v.video_id=cv.video_id WHERE cv.channel_url IS NOT NULL AND v.view_count IS NOT NULL").all()) push(row.channel_url, row.view_count);
  }
  const stats = new Map();
  for (const [channel, values] of byChannel) {
    values.sort((a,b)=>a-b);
    const n=values.length, median=n%2?values[(n-1)/2]:(values[n/2-1]+values[n/2])/2;
    const mean=values.reduce((a,b)=>a+b,0)/n;
    const variance=values.reduce((a,b)=>a+(b-mean)**2,0)/n;
    const top=values[n-1], sum=values.reduce((a,b)=>a+b,0);
    stats.set(channel,{median_video_views:median,sampled_view_counts:n,mean_video_views:mean,video_view_cv:mean>0?Math.sqrt(variance)/mean:null,top_saved_video_views:top,top_video_share:sum>0?top/sum:null});
  }
  videoStatsCache.set(db, stats);
  return stats;
}
function hasTable(db, name) {
  return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name));
}
function columns(db, table) {
  if (!hasTable(db, table)) return new Set();
  return new Set(db.prepare(`PRAGMA table_info("${table.replaceAll('"', '""')}")`).all().map(row => row.name));
}
function col(cols, alias, name, fallback = "NULL") {
  return cols.has(name) ? `${alias}."${name}"` : fallback;
}
function firstExisting(cols, alias, names, fallback = "NULL") {
  const candidates = names.filter(name => cols.has(name)).map(name => `${alias}."${name}"`);
  if (!candidates.length) return fallback;
  return candidates.length === 1 ? candidates[0] : `COALESCE(${candidates.join(",")})`;
}
function effectiveExpr(metaCols, channelCols, field, aliases = [field]) {
  const m = firstExisting(metaCols, "m", aliases);
  const c = firstExisting(channelCols, "c", aliases);
  if (m === "NULL") return c;
  if (c === "NULL") return m;
  return `COALESCE(${m},${c})`;
}
function asNumber(value, min = 0, max = 1e15) {
  if (value == null || value === "") return null;
  const n = Number(value);
  return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : null;
}
function validSort(value) {
  const allowed = new Set(["subs_per_video", "views_per_video", "subscribers", "total_views", "video_count", "age", "queue_score", "confidence", "name", "subs_per_month", "median_video_views", "views_per_year", "top_video_share"]);
  return allowed.has(value) ? value : "subs_per_video";
}
function readFilters(query) {
  return {
    q: String(query.q ?? "").trim().slice(0, 160),
    ageMax: asNumber(query.ageMax, 0, 1200), ageMin: asNumber(query.ageMin, 0, 1200),
    minMedianVideoViews: asNumber(query.minMedianVideoViews), minSubsPerMonth: asNumber(query.minSubsPerMonth),
    minSavedVideos: asNumber(query.minSavedVideos,0,1e7), maxVideoViewCv: asNumber(query.maxVideoViewCv,0,1e6), minTopVideoShare: asNumber(query.minTopVideoShare,0,1),
    minSubs: asNumber(query.minSubs), maxSubs: asNumber(query.maxSubs),
    minViews: asNumber(query.minViews), maxVideos: asNumber(query.maxVideos, 0, 1e7),
    minSubsPerVideo: asNumber(query.minSubsPerVideo), minViewsPerVideo: asNumber(query.minViewsPerVideo),
    queue: QUEUES.some(q => q.key === query.queue) ? query.queue : "",
    minQueueScore: asNumber(query.minQueueScore, 0, 100), minConfidence: asNumber(query.minConfidence, 0, 100),
    sort: validSort(String(query.sort ?? "subs_per_video")), preset: String(query.preset ?? "")
  };
}
function getSchema(db) {
  const cached = schemaCache.get(db); if (cached) return cached;
  const channelCols = columns(db, "channels");
  const discoveredMetaCols = columns(db, "channel_metadata");
  const metaCols = discoveredMetaCols.has("channel_url") && channelCols.has("channel_url") ? discoveredMetaCols : new Set();
  const videoCols = columns(db, "videos");
  const channelVideoCols = columns(db, "channel_videos");
  const queueSchemas = new Map(QUEUES.map(queue => [queue.key, { ...queue, cols: columns(db, queue.table) }]));
  const result = { channelCols, metaCols, videoCols, channelVideoCols, queueSchemas };
  schemaCache.set(db, result);
  return result;
}
function buildSelect(schema) {
  const { channelCols: cc, metaCols: mc, queueSchemas } = schema;
  const urlExpr = col(cc, "c", "channel_url");
  const nameExpr = effectiveExpr(mc, cc, "name");
  const descriptionExpr = effectiveExpr(mc, cc, "description");
  const subsExpr = effectiveExpr(mc, cc, "subscriber_count");
  const subsTextExpr = effectiveExpr(mc, cc, "subscriber_count_text");
  const videosExpr = effectiveExpr(mc, cc, "video_count");
  const videoTextExpr = effectiveExpr(mc, cc, "video_count_text");
  const viewsExpr = effectiveExpr(mc, cc, "total_view_count");
  const viewsTextExpr = effectiveExpr(mc, cc, "total_view_count_text");
  const joinedExpr = effectiveExpr(mc, cc, "joined_date");
  const joinedTextExpr = effectiveExpr(mc, cc, "joined_date_text");
  const thumbExpr = effectiveExpr(mc, cc, "thumbnail_url", ["thumbnail_url", "avatar_url", "channel_thumbnail_url"]);
  const handleExpr = effectiveExpr(mc, cc, "handle");
  const countryExpr = effectiveExpr(mc, cc, "country");
  const discoveredExpr = col(cc, "c", "discovered_at");
  const joins = [];
  if (mc.size && mc.has("channel_url") && cc.has("channel_url")) joins.push("LEFT JOIN channel_metadata m ON m.channel_url=c.channel_url");
  for (const [key, q] of queueSchemas) {
    if (!q.cols.size || !q.cols.has("channel_url")) continue;
    let subquery;
    if (q.cols.has("run_id") && hasTableForRun(q)) {
      subquery = `(SELECT * FROM "${q.table}" WHERE run_id=(SELECT run_id FROM "${q.table}" ORDER BY ${q.cols.has("generated_at") ? "generated_at" : "rowid"} DESC LIMIT 1))`;
    } else subquery = `"${q.table}"`;
    joins.push(`LEFT JOIN ${subquery} q_${key} ON q_${key}.channel_url=c.channel_url`);
  }
  const queueSelects = [];
  for (const [key, q] of queueSchemas) {
    const alias = `q_${key}`;
    if (!q.cols.size || !q.cols.has("channel_url")) {
      queueSelects.push(`NULL AS score_${key}`, `NULL AS confidence_${key}`, `NULL AS status_${key}`, `NULL AS rank_${key}`, `NULL AS rationale_${key}`);
      continue;
    }
    queueSelects.push(`${col(q.cols, alias, "final_channel_score", col(q.cols, alias, "score"))} AS score_${key}`);
    queueSelects.push(`${col(q.cols, alias, "confidence")} AS confidence_${key}`);
    queueSelects.push(`${col(q.cols, alias, "status")} AS status_${key}`);
    queueSelects.push(`${col(q.cols, alias, "rank")} AS rank_${key}`);
    queueSelects.push(`${col(q.cols, alias, "rationale")} AS rationale_${key}`);
    queueSelects.push(`${col(q.cols, alias, "qualification_classification")} AS qualification_${key}`);
  }
  const sampledVideos = hasTableForChannelVideos(schema)
    ? `(SELECT COUNT(DISTINCT cv.video_id) FROM channel_videos cv WHERE cv.channel_url=c.channel_url)`
    : (schema.videoCols.has("channel_url") ? `(SELECT COUNT(*) FROM videos vv WHERE vv.channel_url=c.channel_url)` : "NULL");
  const select = `SELECT ${urlExpr} AS channel_url, ${nameExpr} AS name, ${descriptionExpr} AS description,
    ${subsExpr} AS subscribers, ${subsTextExpr} AS subscriber_text, ${videosExpr} AS declared_videos, ${videoTextExpr} AS video_text,
    ${viewsExpr} AS total_views, ${viewsTextExpr} AS views_text, ${joinedExpr} AS joined_date, ${joinedTextExpr} AS joined_text,
    ${thumbExpr} AS thumbnail_url, ${handleExpr} AS handle, ${countryExpr} AS country, ${discoveredExpr} AS discovered_at,
    CASE WHEN ${joinedExpr} IS NOT NULL AND julianday('now') >= julianday(${joinedExpr}) THEN (julianday('now')-julianday(${joinedExpr}))/30.4375 ELSE NULL END AS age_months,
    CASE WHEN CAST(${videosExpr} AS REAL)>0 THEN CAST(${subsExpr} AS REAL)/${videosExpr} ELSE NULL END AS subscribers_per_video,
    CASE WHEN CAST(${videosExpr} AS REAL)>0 THEN CAST(${viewsExpr} AS REAL)/${videosExpr} ELSE NULL END AS views_per_video,
    ${sampledVideos} AS sampled_videos${queueSelects.length ? ","+queueSelects.join(",") : ""}
    FROM channels c ${joins.join(" ")}`;
  return { select, expressions: { urlExpr, nameExpr, descriptionExpr, subsExpr, videosExpr, viewsExpr, joinedExpr } };
}
function hasTableForRun(q) { return q.cols.has("run_id"); }
function hasTableForChannelVideos(schema) { return schema.channelVideoCols.size > 0 && schema.channelVideoCols.has("channel_url") && schema.channelVideoCols.has("video_id"); }
function buildWhere(filters, schema) {
  const where = [];
  const params = {};
  const cc = schema.channelCols;
  if (!cc.has("channel_url")) return { where: ["1=0"], params };
  if (filters.q) {
    const searchFields = [];
    searchFields.push("COALESCE(name,'') LIKE @q");
    searchFields.push("COALESCE(handle,'') LIKE @q");
    searchFields.push("COALESCE(description,'') LIKE @q");
    searchFields.push("channel_url LIKE @q");
    where.push(`(${searchFields.join(" OR ")})`); params.q = `%${filters.q}%`;
  }
  if (filters.ageMax != null) { where.push("age_months <= @ageMax"); params.ageMax = filters.ageMax; }
  if (filters.ageMin != null) { where.push("age_months >= @ageMin"); params.ageMin = filters.ageMin; }
  if (filters.minSubs != null) { where.push("subscribers >= @minSubs"); params.minSubs = filters.minSubs; }
  if (filters.maxSubs != null) { where.push("subscribers <= @maxSubs"); params.maxSubs = filters.maxSubs; }
  if (filters.minViews != null) { where.push("total_views >= @minViews"); params.minViews = filters.minViews; }
  if (filters.maxVideos != null) { where.push("declared_videos <= @maxVideos"); params.maxVideos = filters.maxVideos; }
  if (filters.minSubsPerVideo != null) { where.push("subscribers_per_video >= @minSubsPerVideo"); params.minSubsPerVideo = filters.minSubsPerVideo; }
  if (filters.minViewsPerVideo != null) { where.push("views_per_video >= @minViewsPerVideo"); params.minViewsPerVideo = filters.minViewsPerVideo; }
  if (filters.queue && filters.minQueueScore != null) { where.push(`score_${filters.queue} >= @minQueueScore`); params.minQueueScore = filters.minQueueScore; }
  if (filters.queue && filters.minConfidence != null) { where.push(`confidence_${filters.queue} >= @minConfidence`); params.minConfidence = filters.minConfidence; }
  return { where, params };
}
function sortExpr(sort, queue) {
  const map = { subs_per_video: "subscribers_per_video DESC", views_per_video: "views_per_video DESC", subscribers: "subscribers DESC", total_views: "total_views DESC", video_count: "declared_videos ASC", age: "age_months ASC", name: "name COLLATE NOCASE ASC", top_video_share: "top_video_share DESC", subs_per_month: "subscribers_per_month DESC", median_video_views: "median_video_views DESC", views_per_year: "views_per_year DESC", confidence: `confidence_${queue || "breaking_out"} DESC`, queue_score: `score_${queue || "breaking_out"} DESC` };
  return map[sort] ?? map.subs_per_video;
}
function getChannelDataset(db, schema) {
  const cached = channelDatasetCache.get(db);
  if (cached) return cached;
  const { select } = buildSelect(schema);
  const baseRows = db.prepare(`WITH channel_facts AS (${select}) SELECT * FROM channel_facts`).all();
  const stats = getVideoStats(db, schema);
  const rows = baseRows.map(row => {
    const videoStats = stats.get(row.channel_url) || { sampled_view_counts: 0, median_video_views: null, mean_video_views: null, video_view_cv: null, top_saved_video_views: null, top_video_share: null };
    const ageYears = row.age_months != null && row.age_months > 0 ? row.age_months / 12 : null;
    return { ...row, ...videoStats,
      subscribers_per_month: row.subscribers != null && row.age_months > 0 ? Number(row.subscribers) / row.age_months : null,
      views_per_year: row.total_views != null && ageYears > 0 ? Number(row.total_views) / ageYears : null,
      declared_videos_per_year: row.declared_videos != null && ageYears > 0 ? Number(row.declared_videos) / ageYears : null
    };
  });
  channelDatasetCache.set(db, rows);
  return rows;
}
function getRows(db, schema, filters, extraWhere = null, extraParams = {}) {
  let rows = getChannelDataset(db, schema);
  const query = String(filters.q || "").toLocaleLowerCase();
  rows = rows.filter(row => {
    if (extraWhere && extraParams.channelUrl && row.channel_url !== extraParams.channelUrl) return false;
    if (query && ![row.name,row.handle,row.description,row.channel_url].some(value => String(value ?? "").toLocaleLowerCase().includes(query))) return false;
    if (filters.ageMax != null && !(row.age_months != null && row.age_months <= filters.ageMax)) return false;
    if (filters.ageMin != null && !(row.age_months != null && row.age_months >= filters.ageMin)) return false;
    if (filters.minSubs != null && !(row.subscribers != null && Number(row.subscribers) >= filters.minSubs)) return false;
    if (filters.maxSubs != null && !(row.subscribers != null && Number(row.subscribers) <= filters.maxSubs)) return false;
    if (filters.minViews != null && !(row.total_views != null && Number(row.total_views) >= filters.minViews)) return false;
    if (filters.maxVideos != null && !(row.declared_videos != null && Number(row.declared_videos) <= filters.maxVideos)) return false;
    if (filters.minSubsPerVideo != null && !(row.subscribers_per_video != null && row.subscribers_per_video >= filters.minSubsPerVideo)) return false;
    if (filters.minViewsPerVideo != null && !(row.views_per_video != null && row.views_per_video >= filters.minViewsPerVideo)) return false;
    if (filters.minMedianVideoViews != null && !(row.median_video_views != null && row.median_video_views >= filters.minMedianVideoViews)) return false;
    if (filters.minSubsPerMonth != null && !(row.subscribers_per_month != null && row.subscribers_per_month >= filters.minSubsPerMonth)) return false;
    if (filters.minSavedVideos != null && !(row.sampled_view_counts >= filters.minSavedVideos)) return false;
    if (filters.maxVideoViewCv != null && !(row.video_view_cv != null && row.video_view_cv <= filters.maxVideoViewCv)) return false;
    if (filters.minTopVideoShare != null && !(row.top_video_share != null && row.top_video_share >= filters.minTopVideoShare)) return false;
    if (filters.queue && filters.minQueueScore != null && !(row[`score_${filters.queue}`] != null && Number(row[`score_${filters.queue}`]) >= filters.minQueueScore)) return false;
    if (filters.queue && filters.minConfidence != null && !(row[`confidence_${filters.queue}`] != null && Number(row[`confidence_${filters.queue}`]) >= filters.minConfidence)) return false;
    return true;
  });
  const ascending = new Set(["age","video_count","name"]);
  const keyMap = { subs_per_video:"subscribers_per_video",views_per_video:"views_per_video",subscribers:"subscribers",total_views:"total_views",video_count:"declared_videos",age:"age_months",name:"name",queue_score:`score_${filters.queue || "breaking_out"}`,confidence:`confidence_${filters.queue || "breaking_out"}`,subs_per_month:"subscribers_per_month",median_video_views:"median_video_views",views_per_year:"views_per_year",top_video_share:"top_video_share" };
  const key = keyMap[filters.sort] || "subscribers_per_video";
  rows.sort((a,b) => {
    const av=a[key], bv=b[key];
    if (av == null && bv == null) return 0;
    if (av == null) return 1;
    if (bv == null) return -1;
    if (key === "name") return (ascending.has(filters.sort)?1:-1) * String(av).localeCompare(String(bv));
    return (ascending.has(filters.sort)?1:-1) * (Number(av)-Number(bv));
  });
  return rows.slice(0, 300);
}

function display(value) { return value == null || value === "" ? "—" : String(value); }
function dateLabel(row) { return row.joined_date ?? row.joined_text ?? "Join date unknown"; }
function scorePills(row) {
  return QUEUES.map(q => {
    const score = row[`score_${q.key}`];
    const conf = row[`confidence_${q.key}`];
    return `<span class="ce-score ${score == null ? "muted" : Number(score)>=70 ? "good" : Number(score)>=45 ? "mid" : "low"}"><b>${esc(q.label)}</b> ${score == null ? "—" : esc(Number(score).toFixed(1))}${conf == null ? "" : `<small> · ${esc(Math.round(Number(conf)))}% conf.</small>`}</span>`;
  }).join("");
}
let esc = value => String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
function renderStyles() {
  return `<style>
  .ce-wrap{max-width:1440px;margin:0 auto}.ce-panel{background:#fff;border:1px solid #e3e5e8;border-radius:16px;padding:18px;margin:16px 0;box-shadow:0 4px 16px #14213d08}.ce-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}.ce-heading h1{margin:0 0 6px;font-size:30px;letter-spacing:-.7px}.ce-muted{color:#697386;font-size:13px}.ce-presets{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}.ce-preset{display:inline-flex;align-items:center;gap:7px;background:#f6f7f9;border:1px solid #dfe3e8;color:#222;padding:8px 11px;border-radius:999px;text-decoration:none;font-size:13px}.ce-preset:hover{background:#edf3ff;border-color:#9bbcff}.ce-form-grid{display:grid;grid-template-columns:repeat(4,minmax(130px,1fr));gap:12px}.ce-field{display:flex;flex-direction:column;gap:5px;font-size:12px;color:#4b5563}.ce-field input,.ce-field select{width:100%;min-width:0;padding:10px;border:1px solid #d7dce2;border-radius:8px;background:#fff;color:#111;font:inherit;font-size:14px}.ce-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:14px}.ce-actions button,.ce-button{background:#172033;color:#fff;border:0;border-radius:8px;padding:10px 14px;font-weight:650;text-decoration:none;cursor:pointer}.ce-actions .ce-clear{background:#eef0f3;color:#222}.ce-results-head{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}.ce-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:14px}.ce-card{background:#fff;border:1px solid #e1e5ea;border-radius:14px;overflow:hidden;display:grid;grid-template-columns:180px minmax(0,1fr);min-height:180px;box-shadow:0 4px 14px #11108}.ce-avatar{width:100%;height:100%;min-height:180px;object-fit:cover;background:#eef1f4}.ce-avatar-empty{min-height:180px;background:linear-gradient(135deg,#edf1f6,#dfe6ef);display:grid;place-items:center;color:#8993a2;font-size:38px;font-weight:700}.ce-card-main{padding:14px;min-width:0}.ce-name{font-size:17px;line-height:1.3;font-weight:750;color:#111;text-decoration:none}.ce-name:hover{text-decoration:underline}.ce-handle{font-size:12px;color:#697386;margin:4px 0 10px}.ce-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;margin:9px 0}.ce-metric{background:#f7f8fa;border-radius:8px;padding:8px;min-width:0}.ce-metric strong{display:block;font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#111}.ce-metric span{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.4px;color:#778091;margin-top:3px}.ce-score-list{display:flex;gap:5px;flex-wrap:wrap;margin:9px 0}.ce-score{font-size:11px;border-radius:6px;padding:5px 7px;background:#f2f4f7;color:#374151}.ce-score small{opacity:.75}.ce-score.good{background:#e6f6eb;color:#146c36}.ce-score.mid{background:#fff4d8;color:#805900}.ce-score.low{background:#fdeaea;color:#9e2929}.ce-score.muted{color:#8b93a0}.ce-card-foot{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin-top:10px}.ce-detail{font-size:12px;font-weight:700;color:#244f9c;text-decoration:none}.ce-description{font-size:12px;color:#596273;line-height:1.45;margin-top:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ce-table-wrap{overflow-x:auto}.ce-table{width:100%;border-collapse:collapse;white-space:nowrap}.ce-table th,.ce-table td{padding:11px;border-bottom:1px solid #edf0f3;text-align:left;font-size:13px}.ce-table th{color:#667085;font-size:11px;text-transform:uppercase;letter-spacing:.4px}.ce-table img{width:54px;height:54px;object-fit:cover;border-radius:50%;vertical-align:middle;margin-right:9px}.ce-video-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.ce-video{border:1px solid #e3e6ea;border-radius:10px;overflow:hidden;background:white}.ce-video img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block;background:#e9edf2}.ce-video-body{padding:10px}.ce-video-title{font-size:13px;font-weight:700;line-height:1.35;color:#111;text-decoration:none}.ce-video-meta{font-size:11px;color:#737d8c;margin-top:7px}.ce-back{display:inline-block;margin:0 0 14px;color:#315da8;text-decoration:none;font-size:13px;font-weight:650}.ce-empty{padding:32px;text-align:center;color:#6b7280;border:1px dashed #cfd5dd;border-radius:12px}.ce-chip{display:inline-block;background:#eef2f6;padding:4px 7px;border-radius:6px;font-size:11px;color:#536071;margin:3px 3px 0 0}.ce-notice{font-size:12px;line-height:1.5;color:#677181;margin-top:10px}@media(max-width:1100px){.ce-cards{grid-template-columns:1fr}.ce-form-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:700px){body{padding:12px}.ce-form-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ce-card{grid-template-columns:110px minmax(0,1fr)}.ce-avatar,.ce-avatar-empty{min-height:150px}.ce-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.ce-video-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ce-heading h1{font-size:24px}}@media(max-width:450px){.ce-form-grid{grid-template-columns:1fr}.ce-card{grid-template-columns:1fr}.ce-avatar,.ce-avatar-empty{height:170px;min-height:170px}.ce-video-grid{grid-template-columns:1fr}}
  </style>`;
}
function renderFilters(filters) {
  const input = (name, label, value, placeholder = "") => `<label class="ce-field">${esc(label)}<input name="${name}" type="number" min="0" step="any" value="${value == null ? "" : esc(value)}" placeholder="${esc(placeholder)}"></label>`;
  return `<form class="ce-panel" method="GET" action="/explore" id="ce-filters">
    <div class="ce-form-grid">
      <label class="ce-field">Preset (fills fields below)<select id="ce-preset" name="preset"><option value="">Custom filters</option>${Object.entries(PRESETS).map(([key,p])=>`<option value="${key}" ${filters.preset===key?"selected":""}>${esc(p.label)}</option>`).join("")}</select></label>
      <label class="ce-field">Search name, handle, description<input name="q" value="${esc(filters.q)}" placeholder="e.g. science, astronomy"></label>
      ${input("ageMin", "Minimum age (months)", filters.ageMin, "0")}
      ${input("ageMax", "Maximum age (months)", filters.ageMax, "24")}
      ${input("minSubs", "Minimum subscribers", filters.minSubs, "50000")}
      ${input("maxSubs", "Maximum subscribers", filters.maxSubs, "No limit")}
      ${input("minViews", "Minimum total channel views", filters.minViews, "1000000")}
      ${input("minMedianVideoViews", "Minimum median saved-video views", filters.minMedianVideoViews, "10000")}
      ${input("minSubsPerMonth", "Minimum subscribers per month of channel age", filters.minSubsPerMonth, "Optional")}
      ${input("minSavedVideos", "Minimum saved videos with views", filters.minSavedVideos, "5")}
      ${input("maxVideoViewCv", "Maximum saved-video view volatility (CV)", filters.maxVideoViewCv, "0.8")}
      ${input("minTopVideoShare", "Minimum top saved video share (0–1)", filters.minTopVideoShare, "0.5")}
      ${input("maxVideos", "Maximum declared videos", filters.maxVideos, "100")}
      ${input("minSubsPerVideo", "Minimum subscribers / video", filters.minSubsPerVideo, "Optional")}
      ${input("minViewsPerVideo", "Minimum views / video", filters.minViewsPerVideo, "Optional")}
      <label class="ce-field">Queue for score/confidence filters<select name="queue"><option value="">No queue filter</option>${QUEUES.map(q=>`<option value="${q.key}" ${filters.queue===q.key?"selected":""}>${esc(q.label)}</option>`).join("")}</select></label>
      ${input("minQueueScore", "Minimum selected queue score (0–100)", filters.minQueueScore, "60")}
      ${input("minConfidence", "Minimum queue confidence (0–100)", filters.minConfidence, "50")}
      <label class="ce-field">Sort results<select name="sort">${[["subs_per_video","Subscribers per declared video"],["views_per_video","Total views per declared video"],["subs_per_month","Subscribers per month of channel age"],["median_video_views","Median views of saved videos"],["views_per_year","Total views per year of channel age"],["top_video_share","Top saved-video share"],["subscribers","Subscriber count"],["total_views","Total channel views"],["video_count","Fewest declared videos"],["age","Youngest channel"],["queue_score","Selected queue score"],["confidence","Selected queue confidence"],["name","Channel name"]].map(([v,l])=>`<option value="${v}" ${filters.sort===v?"selected":""}>${esc(l)}</option>`).join("")}</select></label>
    </div>
    <div class="ce-actions"><button type="submit">Apply filters</button><a class="ce-button ce-clear" href="/explore">Clear all</a><span class="ce-muted">Custom filters combine with AND. Blank fields are ignored.</span></div>
    <div class="ce-notice">Channel age uses a normalized join date where available. Subscribers/video and views/video are descriptive ratios, not causal growth rates. Missing values remain missing and are not treated as zero.</div>
  </form>`;
}
function renderCard(row, formatCount) {
  const image = row.thumbnail_url ? `<img class="ce-avatar" src="${esc(row.thumbnail_url)}" alt="${esc(row.name)} thumbnail" loading="lazy" referrerpolicy="no-referrer">` : `<div class="ce-avatar-empty">${esc((row.name || "?").slice(0,1).toUpperCase())}</div>`;
  const url = `/explore/channel?url=${encodeURIComponent(row.channel_url)}`;
  return `<article class="ce-card">${image}<div class="ce-card-main"><a class="ce-name" href="${url}">${esc(row.name || row.handle || row.channel_url)}</a><div class="ce-handle">${esc(row.handle || row.channel_url)} · ${esc(dateLabel(row))}</div>
    <div class="ce-metrics"><div class="ce-metric"><strong>${formatCount(row.subscribers)}</strong><span>Subscribers</span></div><div class="ce-metric"><strong>${formatCount(row.total_views)}</strong><span>Total views</span></div><div class="ce-metric"><strong>${formatCount(row.declared_videos)}</strong><span>Declared videos</span></div><div class="ce-metric"><strong>${row.age_months == null ? "—" : `${Number(row.age_months).toFixed(1)} mo`}</strong><span>Channel age</span></div><div class="ce-metric"><strong>${formatCount(row.subscribers_per_video)}</strong><span>Subs / video</span></div><div class="ce-metric"><strong>${formatCount(row.views_per_video)}</strong><span>Views / declared video</span></div><div class="ce-metric"><strong>${formatCount(row.subscribers_per_month)}</strong><span>Subs / channel-age month</span></div><div class="ce-metric"><strong>${formatCount(row.median_video_views)}</strong><span>Median saved-video views</span></div><div class="ce-metric"><strong>${formatCount(row.views_per_year)}</strong><span>Views / channel-age year</span></div><div class="ce-metric"><strong>${row.video_view_cv == null ? "—" : Number(row.video_view_cv).toFixed(2)}</strong><span>Saved-view volatility (CV)</span></div><div class="ce-metric"><strong>${row.top_video_share == null ? "—" : `${(row.top_video_share*100).toFixed(1)}%`}</strong><span>Top saved video share</span></div></div>
    <div class="ce-score-list">${scorePills(row)}</div><div class="ce-description">${esc(row.description || "No channel description saved.")}</div><div class="ce-card-foot"><span class="ce-muted">${row.sampled_videos == null ? "Sampled videos unknown" : `${formatCount(row.sampled_videos)} linked videos sampled`}${row.country ? ` · ${esc(row.country)}` : ""}</span><a class="ce-detail" href="${url}">Explore channel →</a></div></div></article>`;
}
function getChannelQuality(db, schema) {
  const cached = qualityCache.get(db); if (cached) return cached;
  try {
    const rows = getChannelDataset(db, schema);
    const missing = field => rows.filter(row => row[field] == null || row[field] === "").length;
    const result = {
      total: rows.length,
      missing_subscribers: missing("subscribers"),
      missing_video_count: missing("declared_videos"),
      missing_total_views: missing("total_views"),
      missing_join_date: rows.filter(row => !row.joined_date && !row.joined_text).length,
      missing_thumbnail: rows.filter(row => !row.thumbnail_url).length,
      missing_learn_from: missing("score_learn_from"),
      missing_breaking_out: missing("score_breaking_out"),
      missing_durable_exemplars: missing("score_durable_exemplars"),
      missing_unexplored_niches: missing("score_unexplored_niches"),
      missing_reference_inspiration: missing("score_reference_inspiration"),
      channels_without_saved_video_views: rows.filter(row => !(Number(row.sampled_view_counts) > 0)).length,
      channels_with_fewer_than_5_saved_video_views: rows.filter(row => Number(row.sampled_view_counts || 0) < 5).length
    };
    qualityCache.set(db, result);
    return result;
  } catch (error) {
    const result = { total: 0, error: error.message };
    qualityCache.set(db, result);
    return result;
  }
}

function renderChannelPresetReadiness(q, preset, formatCount) {
  const requirements = {
    young_strong:[[q.missing_join_date,"join dates"],[q.missing_subscribers,"subscriber counts"],[q.missing_video_count,"declared video counts"]],
    very_young:[[q.missing_join_date,"join dates"],[q.missing_subscribers,"subscriber counts"],[q.missing_video_count,"declared video counts"]],
    efficient_growth:[[q.missing_join_date,"join dates"],[q.missing_subscribers,"subscriber counts"]],
    low_output_big_audience:[[q.missing_subscribers,"subscriber counts"],[q.missing_video_count,"declared video counts"]],
    high_views_few_uploads:[[q.missing_total_views,"total channel views"],[q.missing_video_count,"declared video counts"]],
    high_median_video:[[q.channels_without_saved_video_views,"channels without saved video view counts"]],
    breakout_efficiency:[[q.channels_without_saved_video_views,"channels without saved video view counts"],[q.missing_video_count,"declared video counts"]],
    consistent_performers:[[q.channels_with_fewer_than_5_saved_video_views,"channels with fewer than 5 saved video view counts"]],
    one_hit_dependency:[[q.channels_with_fewer_than_5_saved_video_views,"channels with fewer than 5 saved video view counts"]],
    durable_catalogue:[[q.missing_join_date,"join dates"],[q.missing_total_views,"total channel views"]],
    queue_strength:[[q.missing_breaking_out,"Breaking out queue scores"]], broad_exploration:[]
  };
  const needed = requirements[preset] || [];
  if (!preset || !needed.length) return `<div class="ce-notice">Select a preset to see its key data dependencies. The coverage summary below shows the missing-field counts across the full channel corpus.</div>`;
  const missing = needed.filter(x => Number(x[0] || 0) > 0);
  return `<div class="ce-notice" style="background:#fff8e8;border:1px solid #f0dba6;border-radius:9px;padding:10px"><b>Preset readiness — ${esc(PRESETS[preset]?.label || preset)}:</b> ${missing.length ? missing.map(x => `${formatCount(x[0])} ${esc(x[1])} missing`).join("; ") + ". Affected channels may be excluded or show unavailable derived metrics." : "No missing values detected for the core inputs checked for this preset."} Fix metadata gaps with channel metadata enrichment; saved-video medians improve when more channel videos and their view counts are catalogued. Missing queue scores require queue scoring, not metadata backfill.</div>`;
}
function renderChannelQuality(q, preset, formatCount) {
  const item=(value,label)=>`<div class="ce-metric"><strong>${formatCount(value)}</strong><span>${esc(label)}</span></div>`;
  return `${renderChannelPresetReadiness(q, preset, formatCount)}<section class="ce-panel"><strong>Data coverage &amp; preset readiness</strong><div class="ce-notice">Coverage across ${formatCount(q.total)} channels. Ratios and age-based metrics are only available when their inputs are present; saved-video medians use videos currently stored in this database, not necessarily a complete channel catalogue.</div><div class="ce-metrics">${item(q.missing_join_date,"Missing join date")}${item(q.missing_subscribers,"Missing subscribers")}${item(q.missing_video_count,"Missing declared video count")}${item(q.missing_total_views,"Missing total views")}${item(q.missing_thumbnail,"Missing channel thumbnail")}${item(q.missing_learn_from,"Missing Learn from score")}${item(q.missing_breaking_out,"Missing Breaking out score")}${item(q.missing_durable_exemplars,"Missing Durable exemplars score")}${item(q.missing_unexplored_niches,"Missing Unexplored niches score")}${item(q.missing_reference_inspiration,"Missing Reference inspiration score")}</div><div class="ce-notice"><b>How to improve it:</b> channel metadata gaps may be repairable with <code>npm run channel-metadata</code> or <code>npm run enrich</code>; video medians improve as more channel videos are catalogued. Missing queue scores need the queue-rating stage, not metadata enrichment. Check <code>npm run enrichment:report</code> and stage logs first. Missing values are not interpreted as zero.</div></section>`;
}
function renderExplorePage(filters, rows, quality, formatCount, baseHead, nav) {
  return `<!doctype html><html lang="en">${baseHead("Channel Explorer — YouTube Ecosystem Intelligence").replace("</head>", `${renderStyles()}</head>`)}<body><div class="ce-wrap"><header class="ce-heading"><div><h1>Channel Explorer</h1><div class="ce-muted">Find promising channels, compare queue scores, and inspect the underlying videos.</div></div><div class="ce-muted">${rows.length} results · up to 300 shown · <a href="/explore?refresh=1" style="color:#315da8">Refresh cached data</a></div></header>${nav("explore")}<section class="ce-panel"><strong>Discovery presets</strong><div class="ce-presets">${Object.entries(PRESETS).map(([key,p])=>`<button class="ce-preset" type="button" data-preset="${key}">${esc(p.label)}</button>`).join("")}</div></section>${renderFilters(filters)}${renderChannelQuality(quality, filters.preset, formatCount)}<div class="ce-results-head"><strong>Matching channels</strong><span class="ce-muted">Click a channel for its saved video thumbnails and evidence.</span></div><main class="ce-cards">${rows.length ? rows.map(row=>renderCard(row,formatCount)).join("") : `<div class="ce-empty">No channels match these filters. Try relaxing a threshold or clearing the filters.</div>`}</main></div><script>
  const presets=${JSON.stringify(PRESETS)};const form=document.getElementById('ce-filters');
  function applyPreset(key,submit){const p=presets[key];if(!p)return;for(const el of form.elements){if(el.name && el.name!=='preset' && Object.prototype.hasOwnProperty.call(p,el.name))el.value=p[el.name];else if(el.name && el.name!=='preset' && ['ageMin','maxSubs','minViews','minSubsPerVideo','minViewsPerVideo','minQueueScore','minConfidence','maxVideos','minSubs','ageMax','queue','minMedianVideoViews','minSubsPerMonth','minSavedVideos','maxVideoViewCv','minTopVideoShare'].includes(el.name) && !Object.prototype.hasOwnProperty.call(p,el.name))el.value='';}form.elements.preset.value=key;if(submit)form.requestSubmit();}
  document.querySelectorAll('[data-preset]').forEach(b=>b.addEventListener('click',()=>applyPreset(b.dataset.preset,true)));
  document.getElementById('ce-preset').addEventListener('change',e=>{if(e.target.value)applyPreset(e.target.value,false);else{form.reset();form.elements.preset.value='';}});
  </script></body></html>`;
}
function renderVideoDetail(row, video, esc, formatCount) {
  const image = video.thumbnail_url ? `<img src="${esc(video.thumbnail_url)}" alt="${esc(video.title || "Video thumbnail")}" loading="lazy">` : `<div class="ce-avatar-empty">▶</div>`;
  return `<article class="ce-video">${video.url ? `<a href="${esc(video.url)}" target="_blank" rel="noopener noreferrer">${image}</a>` : image}<div class="ce-video-body">${video.url ? `<a class="ce-video-title" href="${esc(video.url)}" target="_blank" rel="noopener noreferrer">${esc(video.title || video.video_id)}</a>` : `<div class="ce-video-title">${esc(video.title || video.video_id)}</div>`}<div class="ce-video-meta">${formatCount(video.view_count)} views · ${esc(video.published_at || "Date unknown")}${video.duration ? ` · ${esc(video.duration)}` : ""}</div></div></article>`;
}
function renderTranscriptPanel(row) {
  const url = esc(row.channel_url || "");
  if (!url) return "";
  return `<section class="ce-panel" id="transcript-panel" data-channel="${url}"><h2 style="margin-top:0">Transcript sample</h2><div class="ce-muted">Mines this channel&#39;s catalogue (up to 300 videos), then saves the original-language transcripts of its 2 best, 2 median (random) and 2 worst performing videos, each with performance stats on top. Takes several minutes; you can leave this page open or come back.</div><div style="margin:12px 0"><button type="button" id="transcript-run" style="padding:9px 14px;border-radius:8px;border:1px solid #171717;background:#171717;color:#fff;cursor:pointer;font:inherit">Fetch transcript sample</button> <span id="transcript-state" class="ce-muted"></span></div><pre id="transcript-log" style="display:none;max-height:220px;overflow:auto;background:#f6f8fa;border:1px solid #e1e4e8;border-radius:8px;padding:10px;font-size:12px;white-space:pre-wrap"></pre><div id="transcript-files" class="ce-muted"></div></section><script>(function(){const panel=document.getElementById("transcript-panel"),btn=document.getElementById("transcript-run"),state=document.getElementById("transcript-state"),logEl=document.getElementById("transcript-log"),filesEl=document.getElementById("transcript-files");const channel=panel.dataset.channel,db=new URLSearchParams(location.search).get("db");const q=()=>"?url="+encodeURIComponent(channel)+(db?"&db="+encodeURIComponent(db):"");let timer=null;function render(s){const running=s.state==="running";btn.disabled=running;btn.textContent=running?"Running…":(s.files.length?"Re-run transcript sample":"Fetch transcript sample");state.textContent=running?"Running since "+new Date(s.startedAt).toLocaleTimeString():s.state==="done"?"Finished":s.state==="failed"?"Failed (exit code "+s.exitCode+"), see log below":"";logEl.style.display=s.logTail?"block":"none";logEl.textContent=s.logTail||"";logEl.scrollTop=logEl.scrollHeight;filesEl.innerHTML=s.files.length?"<strong>"+s.files.length+" transcript file(s)</strong> in <code></code>:<ul>"+s.files.map(f=>"<li></li>").join("")+"</ul>":"No transcripts saved yet for this channel.";if(s.files.length){filesEl.querySelector("code").textContent=s.outputDir;filesEl.querySelectorAll("li").forEach((li,i)=>li.textContent=s.files[i]);}if(running&&!timer)timer=setInterval(poll,3000);if(!running&&timer){clearInterval(timer);timer=null;}}async function poll(){try{render(await(await fetch("/explore/api/transcripts"+q())).json());}catch(e){state.textContent="Status check failed: "+e.message;}}btn.addEventListener("click",async()=>{btn.disabled=true;try{const r=await fetch("/explore/api/transcripts"+q(),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:channel})});const d=await r.json();if(!r.ok&&r.status!==409)throw new Error(d.error||"Request failed");render(d);}catch(e){state.textContent="Error: "+e.message;btn.disabled=false;}});poll();})();</script>`;
}
function renderDetail(row, videos, formatCount, baseHead, nav) {
  const videoHtml = videos.length ? videos.map(v=>renderVideoDetail(row,v,esc,formatCount)).join("") : `<div class="ce-empty">No linked video records were found for this channel in the current database schema.</div>`;
  return `<!doctype html><html lang="en">${baseHead(`Channel details — ${row.name || row.channel_url}`).replace("</head>", `${renderStyles()}</head>`)}<body><div class="ce-wrap"><a class="ce-back" href="/explore">← Back to Channel Explorer</a>${nav("explore")}<section class="ce-panel"><div class="ce-card" style="grid-template-columns:180px minmax(0,1fr);border:0;box-shadow:none">${row.thumbnail_url?`<img class="ce-avatar" src="${esc(row.thumbnail_url)}" alt="${esc(row.name)} thumbnail">`:`<div class="ce-avatar-empty">${esc((row.name||"?").slice(0,1))}</div>`}<div class="ce-card-main"><h1 style="margin:0 0 6px">${esc(row.name || row.channel_url)}</h1><div class="ce-handle">${esc(row.handle || row.channel_url)} · ${esc(dateLabel(row))}${row.country ? ` · ${esc(row.country)}` : ""}</div><p>${esc(row.description || "No description saved.")}</p><div class="ce-metrics"><div class="ce-metric"><strong>${formatCount(row.subscribers)}</strong><span>Subscribers</span></div><div class="ce-metric"><strong>${formatCount(row.total_views)}</strong><span>Total views</span></div><div class="ce-metric"><strong>${formatCount(row.declared_videos)}</strong><span>Declared videos</span></div><div class="ce-metric"><strong>${row.age_months==null?"—":`${Number(row.age_months).toFixed(1)} months`}</strong><span>Channel age</span></div><div class="ce-metric"><strong>${formatCount(row.subscribers_per_video)}</strong><span>Subs / video</span></div><div class="ce-metric"><strong>${formatCount(row.views_per_video)}</strong><span>Views / declared video</span></div><div class="ce-metric"><strong>${formatCount(row.subscribers_per_month)}</strong><span>Subs / channel-age month</span></div><div class="ce-metric"><strong>${formatCount(row.median_video_views)}</strong><span>Median saved-video views</span></div><div class="ce-metric"><strong>${formatCount(row.views_per_year)}</strong><span>Views / channel-age year</span></div><div class="ce-metric"><strong>${row.video_view_cv == null ? "—" : Number(row.video_view_cv).toFixed(2)}</strong><span>Saved-view volatility (CV)</span></div><div class="ce-metric"><strong>${row.top_video_share == null ? "—" : `${(row.top_video_share*100).toFixed(1)}%`}</strong><span>Top saved video share</span></div></div><div class="ce-score-list">${scorePills(row)}</div></div></div></section><section class="ce-panel"><h2 style="margin-top:0">Saved videos</h2><div class="ce-muted">Video thumbnails and metrics are rendered from saved database rows; missing thumbnails are left blank rather than fabricated.</div><div class="ce-video-grid" style="margin-top:14px">${videoHtml}</div></section>${renderTranscriptPanel(row)}<section class="ce-panel"><h2 style="margin-top:0">Queue rationale and evidence</h2>${QUEUES.map(q=>{const rationale=row[`rationale_${q.key}`];const status=row[`status_${q.key}`];const rank=row[`rank_${q.key}`];return `<div style="padding:12px 0;border-bottom:1px solid #edf0f3"><strong>${esc(q.label)}</strong><div class="ce-muted">Score: ${row[`score_${q.key}`]==null?"—":esc(row[`score_${q.key}`])} · confidence: ${row[`confidence_${q.key}`]==null?"—":esc(row[`confidence_${q.key}`])} · rank: ${rank==null?"—":esc(rank)} · status: ${esc(status||"unknown")}</div><p style="font-size:13px;line-height:1.5">${esc(rationale || "No rationale saved in this queue table.")}</p></div>`;}).join("")}</section></div></body></html>`;
}
function getVideosForChannel(db, schema, channelUrl) {
  const vc = schema.videoCols, cvc = schema.channelVideoCols;
  if (!vc.size) return [];
  const idCol = vc.has("video_id") ? "video_id" : null;
  if (!idCol) return [];
  const fields = ["video_id","title","url","thumbnail_url","view_count","published_at","duration"].map(f=>`${col(vc,"v",f)} AS ${f}`).join(",");
  if (cvc.size && cvc.has("channel_url") && cvc.has("video_id")) {
    return db.prepare(`SELECT DISTINCT ${fields} FROM channel_videos cv JOIN videos v ON v.video_id=cv.video_id WHERE cv.channel_url=? ORDER BY ${vc.has("view_count") ? "v.view_count DESC" : "v.video_id"} LIMIT 60`).all(channelUrl);
  }
  if (vc.has("channel_url")) return db.prepare(`SELECT ${fields} FROM videos v WHERE v.channel_url=? ORDER BY ${vc.has("view_count") ? "v.view_count DESC" : "v.video_id"} LIMIT 60`).all(channelUrl);
  return [];
}
export function registerChannelExplorer(app, dbSource, { formatCount, baseHead, nav }) {
  app.get("/explore", (request, response) => {
    try {
      const db = typeof dbSource === "function" ? dbSource() : dbSource;
      if (request.query.refresh === "1") { videoStatsCache.delete(db); qualityCache.delete(db); schemaCache.delete(db); channelDatasetCache.delete(db); }
      const schema = getSchema(db);
      if (!schema.channelCols.has("channel_url")) return response.status(400).type("text").send("This database passed the basic check but does not expose channels.channel_url, which this channel explorer needs.");
      const filters = readFilters(request.query);
      const rows = getRows(db, schema, filters);
      const quality = getChannelQuality(db, schema);
      response.type("html").send(renderExplorePage(filters, rows, quality, formatCount, baseHead, nav));
    } catch (error) {
      console.error("[viewer] Channel Explorer failed:", error);
      response.status(500).type("text").send(`Channel Explorer failed. Inspect the viewer terminal. Error: ${error.message}`);
    }
  });
  app.get("/explore/channel", (request, response) => {
    try {
      const db = typeof dbSource === "function" ? dbSource() : dbSource;
      const schema = getSchema(db);
      const url = String(request.query.url ?? "").trim();
      if (!url || url.length > 2048) return response.status(400).send("A valid channel URL is required.");
      const filters = readFilters({});
      const rows = getRows(db, schema, filters, "channel_url=@channelUrl", { channelUrl: url });
      if (!rows.length) return response.status(404).send("Channel not found in the current database.");
      const videos = getVideosForChannel(db, schema, url);
      response.type("html").send(renderDetail(rows[0], videos, formatCount, baseHead, nav));
    } catch (error) {
      console.error("[viewer] Channel Explorer detail failed:", error);
      response.status(500).type("text").send(`Channel detail failed. Inspect the viewer terminal. Error: ${error.message}`);
    }
  });
  console.log("[viewer] Channel Explorer registered at /explore (schema-adaptive mode)");
}

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.