/** * End-to-end verification that buildArgs' argv actually works against the real * bundled yt-dlp.exe + ffmpeg.exe — the part typecheck can't prove. It spawns * the binaries exactly the way src/main/download.ts does (argv array, no shell, * windowsHide) so the args travel the identical path, then inspects the output. * * Skipped by default (hits YouTube + the live SponsorBlock DB). To run it: * AEROFETCH_REAL_DOWNLOAD=1 npx vitest run test/real-download.integration.test.ts * (PowerShell: $env:AEROFETCH_REAL_DOWNLOAD=1; npx vitest run test/real-download.integration.test.ts) */ import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { spawnSync } from 'child_process' import { mkdtempSync, existsSync, rmSync, statSync } from 'fs' import { tmpdir } from 'os' import { join, resolve } from 'path' import { buildArgs } from '../src/main/buildArgs' import { DEFAULT_DOWNLOAD_OPTIONS, type StartDownloadOptions } from '@shared/ipc' const RUN = process.env.AEROFETCH_REAL_DOWNLOAD === '1' const BIN_DIR = resolve('resources/bin') const YTDLP = join(BIN_DIR, 'yt-dlp.exe') const FFMPEG = join(BIN_DIR, 'ffmpeg.exe') interface RunResult { status: number | null stdout: string stderr: string filePath?: string } /** Spawn yt-dlp with a buildArgs-produced argv, mirroring download.ts. */ function runYtdlp(argv: string[]): RunResult { const r = spawnSync(YTDLP, argv, { encoding: 'utf8', windowsHide: true, maxBuffer: 128 * 1024 * 1024 }) // download.ts reads the final path from the `path|` print line. const line = (r.stdout || '') .split(/\r?\n/) .reverse() .find((l) => l.startsWith('path|')) return { status: r.status, stdout: r.stdout || '', stderr: r.stderr || '', filePath: line ? line.slice('path|'.length).trim() : undefined } } /** ffmpeg prints container/stream info to stderr; we read it back as a probe. */ function ffinfo(file: string): string { const r = spawnSync(FFMPEG, ['-hide_banner', '-i', file], { encoding: 'utf8', windowsHide: true }) return r.stderr || '' } function parseDurationSec(info: string): number | undefined { const m = info.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/) return m ? Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]) : undefined } /** The attached cover image shows up as an mjpeg Video stream with WxH. */ function parseCoverDims(info: string): { w: number; h: number } | undefined { const m = info.match(/Video:.*?,\s*(\d+)x(\d+)/) return m ? { w: Number(m[1]), h: Number(m[2]) } : undefined } function probeSourceDuration(url: string): number | undefined { const r = spawnSync(YTDLP, ['--no-warnings', '--no-playlist', '--print', '%(duration)s', '--', url], { encoding: 'utf8', windowsHide: true }) const n = Number((r.stdout || '').trim()) return Number.isFinite(n) ? n : undefined } let outDir: string beforeAll(() => { if (!RUN) return expect(existsSync(YTDLP), `yt-dlp.exe missing at ${YTDLP}`).toBe(true) expect(existsSync(FFMPEG), `ffmpeg.exe missing at ${FFMPEG}`).toBe(true) outDir = mkdtempSync(join(tmpdir(), 'aerofetch-real-')) }) afterAll(() => { if (RUN && outDir) rmSync(outDir, { recursive: true, force: true }) }) describe.skipIf(!RUN)('real yt-dlp downloads (buildArgs end-to-end)', () => { it( 'cropThumbnail: audio mp3 gets a SQUARE embedded cover (--ppa crop survives ffmpeg)', () => { const opts: StartDownloadOptions = { id: 'crop', url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', // "Me at the zoo", 19s kind: 'audio', quality: 'Best (MP3)' } const options = { ...DEFAULT_DOWNLOAD_OPTIONS, audioFormat: 'mp3' as const, embedThumbnail: true, cropThumbnail: true } const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR) console.log('[crop] argv:', JSON.stringify(argv)) const res = runYtdlp(argv) if (res.status !== 0) console.error('[crop] stderr:\n' + res.stderr) expect(res.status, 'yt-dlp should exit 0').toBe(0) expect(res.filePath, 'after_move path should be printed').toBeTruthy() expect(existsSync(res.filePath!)).toBe(true) expect(statSync(res.filePath!).size).toBeGreaterThan(1000) const info = ffinfo(res.filePath!) const dims = parseCoverDims(info) console.log('[crop] output:', res.filePath, 'cover dims:', dims) expect(dims, 'embedded cover image should be present').toBeTruthy() expect(dims!.w, 'cover must be cropped to a square').toBe(dims!.h) }, 240_000 ) it( 'sponsorBlock remove: output is shorter than source by the removed segments', () => { const url = 'https://www.youtube.com/watch?v=kJQP7kiw5Fk' // has ~54s of music_offtopic const source = probeSourceDuration(url) console.log('[sb] source duration:', source) expect(source, 'should read source duration').toBeGreaterThan(60) const opts: StartDownloadOptions = { id: 'sb', url, kind: 'audio', quality: 'Best (MP3)' } const options = { ...DEFAULT_DOWNLOAD_OPTIONS, audioFormat: 'mp3' as const, embedThumbnail: false, sponsorBlock: true, sponsorBlockMode: 'remove' as const, sponsorBlockCategories: ['music_offtopic' as const] } const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR) console.log('[sb] argv:', JSON.stringify(argv)) const res = runYtdlp(argv) if (res.status !== 0) console.error('[sb] stderr:\n' + res.stderr) expect(res.status, 'yt-dlp should exit 0').toBe(0) expect(res.filePath).toBeTruthy() expect(existsSync(res.filePath!)).toBe(true) const outDur = parseDurationSec(ffinfo(res.filePath!)) console.log('[sb] output duration:', outDur, 'removed ≈', (source ?? 0) - (outDur ?? 0), 's') expect(outDur, 'should read output duration').toBeGreaterThan(0) // ~54s of segments are removed; require a clear, unambiguous shrink. expect(source! - outDur!).toBeGreaterThan(30) }, 240_000 ) })