mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
145 lines
4.5 KiB
TypeScript
145 lines
4.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
downloadWithYtDlp,
|
|
parseYtDlpMetadata,
|
|
readYtDlpMetadata,
|
|
resolveYtDlp,
|
|
YTDLP_FORMAT,
|
|
type YtDlpResult,
|
|
} from '@/utilities/media/ytdlp'
|
|
|
|
// C215 — the yt-dlp owner: fail-closed resolution, metadata parsing and the
|
|
// deterministic download command (format cap, merge, ffmpeg location).
|
|
|
|
const okRun =
|
|
(stdout = '') =>
|
|
async (): Promise<YtDlpResult> => ({ ok: true, stdout, stderr: '' })
|
|
|
|
describe('resolveYtDlp', () => {
|
|
it('accepts the explicit YTDLP_PATH and the PATH fallback', async () => {
|
|
await expect(
|
|
resolveYtDlp({ env: { YTDLP_PATH: '/custom/yt-dlp' }, run: okRun('2026.09.20') }),
|
|
).resolves.toEqual({ ok: true, bin: '/custom/yt-dlp' })
|
|
await expect(resolveYtDlp({ env: {}, run: okRun('2026.09.20') })).resolves.toEqual({
|
|
ok: true,
|
|
bin: 'yt-dlp',
|
|
})
|
|
})
|
|
|
|
it('fails closed with the reason when the binary is missing', async () => {
|
|
const missing = async (): Promise<YtDlpResult> => ({
|
|
ok: false,
|
|
stdout: '',
|
|
stderr: '',
|
|
error: { code: 'ENOENT' },
|
|
})
|
|
const resolved = await resolveYtDlp({ env: {}, run: missing })
|
|
expect(resolved.ok).toBe(false)
|
|
if (!resolved.ok) expect(resolved.reason).toContain('não encontrado')
|
|
})
|
|
})
|
|
|
|
describe('parseYtDlpMetadata', () => {
|
|
it('maps the dump-json document and falls back to uploader', () => {
|
|
expect(
|
|
parseYtDlpMetadata({
|
|
id: 'dQw4w9WgXcQ',
|
|
title: ' Entrevista ',
|
|
uploader: 'Canal do Solla',
|
|
duration: 123.4,
|
|
thumbnail: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hq.jpg',
|
|
}),
|
|
).toEqual({
|
|
externalId: 'dQw4w9WgXcQ',
|
|
title: 'Entrevista',
|
|
channel: 'Canal do Solla',
|
|
durationSeconds: 123.4,
|
|
thumbnailUrl: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hq.jpg',
|
|
})
|
|
})
|
|
|
|
it('keeps absent/invalid fields null instead of guessing', () => {
|
|
expect(parseYtDlpMetadata({ duration: -5, title: '' })).toEqual({
|
|
externalId: null,
|
|
title: null,
|
|
channel: null,
|
|
durationSeconds: null,
|
|
thumbnailUrl: null,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('readYtDlpMetadata', () => {
|
|
it('parses the JSON stdout', async () => {
|
|
const run = okRun(JSON.stringify({ id: 'x', title: 'Fala', duration: 60 }))
|
|
await expect(
|
|
readYtDlpMetadata({ bin: 'yt-dlp', url: 'https://x', run }),
|
|
).resolves.toMatchObject({ externalId: 'x', title: 'Fala', durationSeconds: 60 })
|
|
})
|
|
|
|
it('throws with the stderr tail on failure and on malformed JSON', async () => {
|
|
const failing = async (): Promise<YtDlpResult> => ({
|
|
ok: false,
|
|
stdout: '',
|
|
stderr: 'ERROR: Video unavailable',
|
|
})
|
|
await expect(
|
|
readYtDlpMetadata({ bin: 'yt-dlp', url: 'https://x', run: failing }),
|
|
).rejects.toThrow(/Video unavailable/)
|
|
|
|
await expect(
|
|
readYtDlpMetadata({ bin: 'yt-dlp', url: 'https://x', run: okRun('not json') }),
|
|
).rejects.toThrow(/fora do formato/)
|
|
})
|
|
})
|
|
|
|
describe('downloadWithYtDlp', () => {
|
|
it('caps quality, merges to mp4 and forwards the ffmpeg location', async () => {
|
|
const calls: string[][] = []
|
|
const run = async (_bin: string, args: string[]): Promise<YtDlpResult> => {
|
|
calls.push(args)
|
|
return { ok: true, stdout: '', stderr: '' }
|
|
}
|
|
|
|
await downloadWithYtDlp({
|
|
bin: '/bin/yt-dlp',
|
|
url: 'https://www.youtube.com/watch?v=x',
|
|
outputTemplate: '/tmp/source.%(ext)s',
|
|
ffmpegLocation: '/packed/ffmpeg',
|
|
run,
|
|
})
|
|
|
|
expect(calls).toHaveLength(1)
|
|
const args = calls[0]
|
|
expect(args).toContain('--no-playlist')
|
|
expect(args.slice(args.indexOf('-f'), args.indexOf('-f') + 2)).toEqual(['-f', YTDLP_FORMAT])
|
|
expect(
|
|
args.slice(args.indexOf('--merge-output-format'), args.indexOf('--merge-output-format') + 2),
|
|
).toEqual(['--merge-output-format', 'mp4'])
|
|
expect(
|
|
args.slice(args.indexOf('--ffmpeg-location'), args.indexOf('--ffmpeg-location') + 2),
|
|
).toEqual(['--ffmpeg-location', '/packed/ffmpeg'])
|
|
expect(args.slice(args.indexOf('-o'), args.indexOf('-o') + 2)).toEqual([
|
|
'-o',
|
|
'/tmp/source.%(ext)s',
|
|
])
|
|
expect(args.at(-1)).toBe('https://www.youtube.com/watch?v=x')
|
|
})
|
|
|
|
it('throws with the stderr tail when yt-dlp fails', async () => {
|
|
const failing = async (): Promise<YtDlpResult> => ({
|
|
ok: false,
|
|
stdout: '',
|
|
stderr: 'ERROR: unable to download',
|
|
})
|
|
await expect(
|
|
downloadWithYtDlp({
|
|
bin: 'yt-dlp',
|
|
url: 'https://x',
|
|
outputTemplate: '/tmp/source.%(ext)s',
|
|
run: failing,
|
|
}),
|
|
).rejects.toThrow(/unable to download/)
|
|
})
|
|
})
|