Files
jakevin d2974a9ff6 refactor(adapters): convert adapter layer from TypeScript to JavaScript (#928)
* refactor(adapters): convert adapter layer from TypeScript to JavaScript

Core framework stays TypeScript; adapter layer moves to JS-first.
Adapters are essentially "executable config + browser scripts" that
barely use TS features — this simplifies the build/distribution pipeline
by removing the dist/clis/ intermediate compilation step.

Changes:
- Convert all 753 adapter files in clis/ from .ts to .js
- Update tsconfig to exclude clis/ from compilation
- Simplify build-manifest to scan clis/*.js directly (no dist/clis/)
- Update discovery, main, fetch-adapters to load JS adapters from clis/
- Update generate-verified to output .js artifacts
- Update package.json files field: dist/clis/ → clis/
- Fix all test files for the .ts → .js transition

* fix(main): use findPackageRoot for BUILTIN_CLIS path

The previous relative path (../../clis from __dirname) only worked for
dist/src/main.js but broke dev mode (tsx src/main.ts) where __dirname
is <repo>/src — resolving to /clis instead of <repo>/clis.

Use findPackageRoot() which works for both dev and prod paths.
2026-04-10 14:52:18 +08:00

93 lines
2.9 KiB
JavaScript

/**
* Extract a YouTube video ID from a URL or bare video ID string.
* Supports: watch?v=, youtu.be/, /shorts/, /embed/, /live/, /v/
*/
export function parseVideoId(input) {
if (!input.startsWith('http'))
return input;
try {
const parsed = new URL(input);
if (parsed.searchParams.has('v')) {
return parsed.searchParams.get('v');
}
if (parsed.hostname === 'youtu.be') {
return parsed.pathname.slice(1).split('/')[0];
}
// Handle /shorts/xxx, /embed/xxx, /live/xxx, /v/xxx
const pathMatch = parsed.pathname.match(/^\/(shorts|embed|live|v)\/([^/?]+)/);
if (pathMatch)
return pathMatch[2];
}
catch {
// Not a valid URL — treat entire input as video ID
}
return input;
}
/**
* Extract a JSON object assigned to a known bootstrap variable inside YouTube HTML.
*/
export function extractJsonAssignmentFromHtml(html, keys) {
const candidates = Array.isArray(keys) ? keys : [keys];
for (const key of candidates) {
const markers = [
`var ${key} = `,
`window["${key}"] = `,
`window.${key} = `,
`${key} = `,
];
for (const marker of markers) {
const markerIndex = html.indexOf(marker);
if (markerIndex === -1)
continue;
const jsonStart = html.indexOf('{', markerIndex + marker.length);
if (jsonStart === -1)
continue;
let depth = 0;
let inString = false;
let escaping = false;
for (let i = jsonStart; i < html.length; i += 1) {
const ch = html[i];
if (inString) {
if (escaping) {
escaping = false;
}
else if (ch === '\\') {
escaping = true;
}
else if (ch === '"') {
inString = false;
}
continue;
}
if (ch === '"') {
inString = true;
continue;
}
if (ch === '{') {
depth += 1;
continue;
}
if (ch === '}') {
depth -= 1;
if (depth === 0) {
try {
return JSON.parse(html.slice(jsonStart, i + 1));
}
catch {
break;
}
}
}
}
}
}
return null;
}
/**
* Prepare a quiet YouTube API-capable page without opening the watch UI.
*/
export async function prepareYoutubeApiPage(page) {
await page.goto('https://www.youtube.com', { waitUntil: 'none' });
await page.wait(2);
}