import { describe, it, expect } from 'vitest' import { buildArgs, CROP_SQUARE_PPA } from '../src/main/buildArgs' import { DEFAULT_DOWNLOAD_OPTIONS, type DownloadOptions, type StartDownloadOptions } from '@shared/ipc' // --- Fixtures --------------------------------------------------------------- const BIN = 'C:/fake/bin' const OUT = 'C:/out/%(title)s.%(ext)s' const URL = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' function opts(overrides: Partial = {}): StartDownloadOptions { return { id: 't', url: URL, kind: 'video', quality: '1080p', ...overrides } } function audio(overrides: Partial = {}): StartDownloadOptions { return opts({ kind: 'audio', quality: 'Best (MP3)', ...overrides }) } function dlo(overrides: Partial = {}): DownloadOptions { return { ...DEFAULT_DOWNLOAD_OPTIONS, ...overrides } } function build(o: StartDownloadOptions, d: DownloadOptions): string[] { return buildArgs(o, OUT, d, BIN) } /** True when `seq` appears as a contiguous run inside `argv`. */ function hasSeq(argv: string[], ...seq: string[]): boolean { if (seq.length === 0) return true for (let i = 0; i + seq.length <= argv.length; i++) { if (seq.every((s, j) => argv[i + j] === s)) return true } return false } // --- Invariants shared by every recipe -------------------------------------- describe('buildArgs scaffolding', () => { it('pins ffmpeg location to the supplied binDir', () => { expect(hasSeq(build(opts(), dlo()), '--ffmpeg-location', BIN)).toBe(true) }) it('terminates options with `--` immediately before the URL', () => { const argv = build(opts(), dlo()) expect(argv[argv.length - 2]).toBe('--') expect(argv[argv.length - 1]).toBe(URL) }) }) // --- Audio extraction format ------------------------------------------------ describe('audio: -x --audio-format ', () => { it.each(['mp3', 'opus', 'flac'] as const)('emits -x --audio-format %s', (fmt) => { const argv = build(audio(), dlo({ audioFormat: fmt })) expect(hasSeq(argv, '-x', '--audio-format', fmt)).toBe(true) }) it('maps the quality label to an --audio-quality value', () => { expect(hasSeq(build(audio({ quality: 'Best (MP3)' }), dlo()), '--audio-quality', '0')).toBe(true) expect(hasSeq(build(audio({ quality: '320 kbps' }), dlo()), '--audio-quality', '320K')).toBe( true ) expect(hasSeq(build(audio({ quality: '128 kbps' }), dlo()), '--audio-quality', '128K')).toBe( true ) }) }) // --- Subtitles (video only) ------------------------------------------------- describe('embedSubtitles', () => { it('emits write/embed/sub-langs/convert without auto-subs by default', () => { const argv = build(opts(), dlo({ embedSubtitles: true })) expect( hasSeq(argv, '--write-subs', '--embed-subs', '--sub-langs', 'en', '--convert-subs', 'srt') ).toBe(true) expect(argv).not.toContain('--write-auto-subs') }) it('inserts --write-auto-subs before --convert-subs when autoSubtitles is on', () => { const argv = build(opts(), dlo({ embedSubtitles: true, autoSubtitles: true })) expect( hasSeq( argv, '--write-subs', '--embed-subs', '--sub-langs', 'en', '--write-auto-subs', '--convert-subs', 'srt' ) ).toBe(true) }) it('honours a custom subtitleLanguages selector', () => { const argv = build(opts(), dlo({ embedSubtitles: true, subtitleLanguages: 'en.*,es' })) expect(hasSeq(argv, '--sub-langs', 'en.*,es')).toBe(true) }) it('never writes subtitles for an audio download', () => { const argv = build(audio(), dlo({ embedSubtitles: true, autoSubtitles: true })) expect(argv).not.toContain('--write-subs') expect(argv).not.toContain('--embed-subs') }) }) // --- SponsorBlock ----------------------------------------------------------- describe('sponsorBlock', () => { it('remove → --sponsorblock-remove --force-keyframes-at-cuts', () => { const argv = build(opts(), dlo({ sponsorBlock: true, sponsorBlockMode: 'remove' })) expect(hasSeq(argv, '--sponsorblock-remove', 'sponsor', '--force-keyframes-at-cuts')).toBe(true) expect(argv).not.toContain('--sponsorblock-mark') }) it('mark → --sponsorblock-mark , no keyframe forcing', () => { const argv = build(opts(), dlo({ sponsorBlock: true, sponsorBlockMode: 'mark' })) expect(hasSeq(argv, '--sponsorblock-mark', 'sponsor')).toBe(true) expect(argv).not.toContain('--sponsorblock-remove') expect(argv).not.toContain('--force-keyframes-at-cuts') }) it('joins multiple categories with commas', () => { const argv = build( opts(), dlo({ sponsorBlock: true, sponsorBlockCategories: ['sponsor', 'intro', 'outro'] }) ) expect(hasSeq(argv, '--sponsorblock-remove', 'sponsor,intro,outro')).toBe(true) }) it('emits nothing when sponsorBlock is off or category list is empty', () => { expect(build(opts(), dlo({ sponsorBlock: false })).join(' ')).not.toContain('sponsorblock') expect( build(opts(), dlo({ sponsorBlock: true, sponsorBlockCategories: [] })).join(' ') ).not.toContain('sponsorblock') }) }) // --- Preferred video codec (sort tiebreaker) -------------------------------- describe('preferredVideoCodec → -S res,fps,vcodec:', () => { it('maps av1 to the av01 codec token', () => { expect(hasSeq(build(opts(), dlo({ preferredVideoCodec: 'av1' })), '-S', 'res,fps,vcodec:av01')).toBe( true ) }) it('passes h264 / vp9 through verbatim', () => { expect(hasSeq(build(opts(), dlo({ preferredVideoCodec: 'h264' })), '-S', 'res,fps,vcodec:h264')).toBe( true ) expect(hasSeq(build(opts(), dlo({ preferredVideoCodec: 'vp9' })), '-S', 'res,fps,vcodec:vp9')).toBe( true ) }) it('omits -S entirely when codec preference is "any"', () => { expect(build(opts(), dlo({ preferredVideoCodec: 'any' }))).not.toContain('-S') }) }) // --- Cropped square cover art (the highest-risk ffmpeg quoting) -------------- describe('cropThumbnail → --ppa crop', () => { it('emits --embed-thumbnail then the square-crop --ppa string', () => { const argv = build(audio(), dlo({ embedThumbnail: true, cropThumbnail: true })) expect(hasSeq(argv, '--embed-thumbnail', '--ppa', CROP_SQUARE_PPA)).toBe(true) }) it('uses single-quoted, comma-protected crop expressions for ffmpeg', () => { // The commas live inside gt(...) and MUST stay quoted so ffmpeg does not read // them as filtergraph separators. The targeted-postprocessor key plus the // ffmpeg_o output-args selector frame the whole value. expect(CROP_SQUARE_PPA).toBe( "EmbedThumbnail+ffmpeg_o:-c:v mjpeg -vf crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\"" ) }) it('does not add --ppa when cropThumbnail is off', () => { const argv = build(audio(), dlo({ embedThumbnail: true, cropThumbnail: false })) expect(argv).not.toContain('--ppa') expect(argv).toContain('--embed-thumbnail') }) it('does not add --ppa when the thumbnail itself is not embedded', () => { const argv = build(audio(), dlo({ embedThumbnail: false, cropThumbnail: true })) expect(argv).not.toContain('--ppa') expect(argv).not.toContain('--embed-thumbnail') }) })