import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { probeYoutubeTracks } from './track-probe'; async function withTempDir(fn: (dir: string) => Promise): Promise { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-youtube-track-probe-')); try { return await fn(dir); } finally { fs.rmSync(dir, { recursive: true, force: true }); } } function makeFakeYtDlpScript(dir: string, payload: unknown, rawScript = false): void { const scriptPath = path.join(dir, 'yt-dlp'); const stdoutBody = typeof payload === 'string' ? payload : JSON.stringify(payload); const script = process.platform === 'win32' ? rawScript ? stdoutBody : `#!/usr/bin/env bun process.stdout.write(${JSON.stringify(stdoutBody)}); ` : `#!/bin/sh PATH=/usr/bin:/bin:/usr/local/bin cat <<'SUBMINER_EOF' | base64 -d ${Buffer.from(stdoutBody).toString('base64')} SUBMINER_EOF `; fs.writeFileSync(scriptPath, script, 'utf8'); if (process.platform !== 'win32') { fs.chmodSync(scriptPath, 0o755); } fs.writeFileSync(scriptPath + '.cmd', `@echo off\r\nnode "${scriptPath}"\r\n`, 'utf8'); } async function withFakeYtDlp( payload: unknown, fn: () => Promise, options: { rawScript?: boolean } = {}, ): Promise { return await withTempDir(async (root) => { const binDir = path.join(root, 'bin'); fs.mkdirSync(binDir, { recursive: true }); makeFakeYtDlpScript(binDir, payload, options.rawScript === true); const originalPath = process.env.PATH ?? ''; const originalCommand = process.env.SUBMINER_YTDLP_BIN; process.env.PATH = `${binDir}${path.delimiter}${originalPath}`; process.env.SUBMINER_YTDLP_BIN = process.platform === 'win32' ? path.join(binDir, 'yt-dlp.cmd') : path.join(binDir, 'yt-dlp'); try { return await fn(); } finally { process.env.PATH = originalPath; if (originalCommand === undefined) { delete process.env.SUBMINER_YTDLP_BIN; } else { process.env.SUBMINER_YTDLP_BIN = originalCommand; } } }); } async function withFakeYtDlpCommand( payload: unknown, fn: () => Promise, options: { rawScript?: boolean } = {}, ): Promise { return await withTempDir(async (root) => { const binDir = path.join(root, 'bin'); fs.mkdirSync(binDir, { recursive: true }); makeFakeYtDlpScript(binDir, payload, options.rawScript === true); const originalPath = process.env.PATH; const originalCommand = process.env.SUBMINER_YTDLP_BIN; process.env.PATH = ''; process.env.SUBMINER_YTDLP_BIN = process.platform === 'win32' ? path.join(binDir, 'yt-dlp.cmd') : path.join(binDir, 'yt-dlp'); try { return await fn(); } finally { if (originalPath === undefined) { delete process.env.PATH; } else { process.env.PATH = originalPath; } if (originalCommand === undefined) { delete process.env.SUBMINER_YTDLP_BIN; } else { process.env.SUBMINER_YTDLP_BIN = originalCommand; } } }); } test('probeYoutubeTracks prefers srv3 over vtt for automatic captions', async () => { await withFakeYtDlp( { id: 'abc123', title: 'Example', automatic_captions: { 'ja-orig': [ { ext: 'vtt', url: 'https://example.com/ja.vtt', name: 'Japanese auto' }, { ext: 'srv3', url: 'https://example.com/ja.srv3', name: 'Japanese auto' }, ], }, }, async () => { const result = await probeYoutubeTracks('https://www.youtube.com/watch?v=abc123'); assert.equal(result.videoId, 'abc123'); assert.equal(result.tracks[0]?.downloadUrl, 'https://example.com/ja.srv3'); assert.equal(result.tracks[0]?.fileExtension, 'srv3'); }, ); }); test('probeYoutubeTracks honors SUBMINER_YTDLP_BIN when yt-dlp is not on PATH', async () => { if (process.platform === 'win32') { return; } await withFakeYtDlpCommand( { id: 'abc123', title: 'Example', subtitles: { ja: [{ ext: 'vtt', url: 'https://example.com/ja.vtt', name: 'Japanese manual' }], }, }, async () => { const result = await probeYoutubeTracks('https://www.youtube.com/watch?v=abc123'); assert.equal(result.videoId, 'abc123'); assert.equal(result.tracks[0]?.downloadUrl, 'https://example.com/ja.vtt'); assert.equal(result.tracks[0]?.fileExtension, 'vtt'); }, ); }); test('probeYoutubeTracks keeps preferring srt for manual captions', async () => { await withFakeYtDlp( { id: 'abc123', title: 'Example', subtitles: { ja: [ { ext: 'srv3', url: 'https://example.com/ja.srv3', name: 'Japanese manual' }, { ext: 'srt', url: 'https://example.com/ja.srt', name: 'Japanese manual' }, ], }, }, async () => { const result = await probeYoutubeTracks('https://www.youtube.com/watch?v=abc123'); assert.equal(result.tracks[0]?.downloadUrl, 'https://example.com/ja.srt'); assert.equal(result.tracks[0]?.fileExtension, 'srt'); }, ); }); test('probeYoutubeTracks reports malformed yt-dlp JSON with context', async () => { await withFakeYtDlp('not-json', async () => { await assert.rejects( async () => await probeYoutubeTracks('https://www.youtube.com/watch?v=abc123'), /Failed to parse yt-dlp output as JSON/, ); }); });