#!/usr/bin/env bun /** * Find the NeetCode explanation video for every docs page that has none. * * A finished solution page carries frontmatter `video: ` — * NeetCode's walkthrough of the problem — which the docs MDX `wrapper` * (`apps/docs/components/ProblemBadges.astro`) renders as the red * "Explanation" badge beside the topic badges. `sync.ts` never writes that key * (it owns `icon:`, `topics:` and `## Solution`), so pages accumulate without * one. This script lists every numbered page lacking `video:` and looks each * up on NeetCode's two channels, main channel first, accepting only a title * that names the page's exact LC number (`… - Leetcode 226 - …`) and is not a * #short. No YouTube API key: the channel search page embeds its results as * `ytInitialData` JSON, which is all we parse. Ranking is the channel's own — * the first exact-number match wins. * * bun apps/cli/videos.ts # report: page → video (or "no match") * bun apps/cli/videos.ts --write # also add `video:` to the frontmatter * * --write inserts the key before `sidebar:`. A page with no match is left * untouched, so re-runs are no-ops once every page has its video. */ import { join } from "node:path"; import { printTable } from "./report.ts"; import { ROOT } from "./work.ts"; const DOCS = join(ROOT, "apps", "docs", "content"); const WRITE = process.argv.includes("--write"); /** Channel handles searched in order; the first exact match wins. */ const CHANNELS = ["NeetCode", "NeetCodeIO"] as const; interface Video { id: string; title: string; channel: string; } // ── YouTube channel search (no API key) ────────────────────────── const HEADERS = { "accept-language": "en-US,en;q=0.9", "user-agent": "Mozilla/5.0" }; /** * The `var ytInitialData = {…};` blob of a YouTube page, parsed. * YouTube intermittently answers 200 with a page that lacks the blob or cuts * it short, so a bad page is retried a few times before it is an error. */ async function initialData(url: string): Promise { let reason = ""; for (let attempt = 0; attempt < 3; attempt++) { if (attempt) await Bun.sleep(500 * attempt); const res = await fetch(url, { headers: HEADERS }); if (!res.ok) { reason = `status ${res.status}`; continue; } const html = await res.text(); // Slice rather than regex: the blob is ~1 MB, and a lazy `.*?` across it // is what made the first cut of this script fail at random. const open = html.search(/ytInitialData\s*=\s*\{/); const close = open === -1 ? -1 : html.indexOf("", open); if (close === -1) { reason = `no ytInitialData in ${html.length}-byte page`; continue; } try { return JSON.parse(html.slice(html.indexOf("{", open), close).replace(/;\s*$/, "")); } catch { reason = "truncated ytInitialData"; } } throw new Error(`GET ${url} -> ${reason}`); } /** Every videoRenderer in a channel search page, in the page's own order. */ async function channelSearch(channel: string, query: string): Promise { const url = `https://www.youtube.com/@${channel}/search?query=${encodeURIComponent(query)}`; const data = await initialData(url); const videos: Video[] = []; const walk = (node: unknown): void => { if (!node || typeof node !== "object") return; if ("videoRenderer" in node) { const v = node.videoRenderer; if (v && typeof v === "object" && "videoId" in v && typeof v.videoId === "string") { const runs = "title" in v && v.title && typeof v.title === "object" && "runs" in v.title ? v.title.runs : undefined; const title = Array.isArray(runs) ? runs.map((r) => (r && typeof r === "object" && "text" in r ? String(r.text) : "")).join("") : ""; videos.push({ id: v.videoId, title, channel }); } return; } for (const child of Object.values(node)) walk(child); }; walk(data); return videos; } /** The LC number a NeetCode title names (`… - Leetcode 226 - …`), if any. */ function titledNumber(title: string): number | undefined { const m = title.match(/\bleetcode\s*#?\s*(\d+)\b/i); return m ? Number(m[1]) : undefined; } async function findVideo(num: number): Promise