Options (quality, subtitles) are now derived from a yt-dlp -J probe of the submitted URL instead of a static list, so users can't pick combinations the source video doesn't actually support. Submission is blocked until the probe succeeds. - New /api/probe endpoint + src/lib/ytdlp-probe.ts, with its own rate limiter (rate-limit.ts refactored into a createRateLimiter factory). - New options: subtitle language selection, clip start/end trim, MP3 audio quality. - Fixed buildYtdlpArgs: format=mp3 never triggered audio extraction (-x/--audio-format), and quality=best never applied --merge-output-format, so the chosen container had no real effect. - Cross-platform yt-dlp invocation: the bundled bin/yt-dlp is a Python zipapp relying on a shebang, which Windows' spawn() can't run directly. buildYtdlpCommand() runs it through `python` on Windows and execs it directly on Linux/o2switch, where the shebang works natively. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import { spawn } from 'child_process'
|
|
import { readdirSync, statSync } from 'fs'
|
|
import path from 'path'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { buildYtdlpArgs, buildYtdlpCommand } from '@/lib/ytdlp'
|
|
import { createToken } from '@/lib/token'
|
|
import { config } from '../config/app.config'
|
|
|
|
export async function processDownload(downloadId: string): Promise<void> {
|
|
const download = await prisma.download.findUnique({ where: { id: downloadId } })
|
|
if (!download) return
|
|
|
|
const args = buildYtdlpArgs({
|
|
url: download.url,
|
|
uuid: download.uuid,
|
|
format: download.format,
|
|
quality: download.quality,
|
|
subtitles: download.subtitles,
|
|
subtitleLangs: download.subtitleLangs ? download.subtitleLangs.split(',') : null,
|
|
clipStart: download.clipStart,
|
|
clipEnd: download.clipEnd,
|
|
audioQuality: download.audioQuality,
|
|
extraArgs: download.extraArgs,
|
|
})
|
|
|
|
let stderr = ''
|
|
const { command, args: spawnArgs } = buildYtdlpCommand(args)
|
|
|
|
await new Promise<void>((resolve) => {
|
|
const proc = spawn(command, spawnArgs)
|
|
proc.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString() })
|
|
|
|
proc.on('close', async (code) => {
|
|
if (code === 0) {
|
|
const files = readdirSync(config.STORAGE_PATH).filter((f) =>
|
|
f.startsWith(download.uuid)
|
|
)
|
|
const relFile = files[0] ?? null
|
|
const filePath = relFile ? path.join(config.STORAGE_PATH, relFile) : null
|
|
const fileSize = filePath ? BigInt(statSync(filePath).size) : null
|
|
|
|
await prisma.download.update({
|
|
where: { id: downloadId },
|
|
data: {
|
|
status: 'DONE',
|
|
filePath,
|
|
fileName: relFile,
|
|
fileSize,
|
|
completedAt: new Date(),
|
|
},
|
|
})
|
|
await createToken(downloadId)
|
|
} else {
|
|
await prisma.download.update({
|
|
where: { id: downloadId },
|
|
data: {
|
|
status: 'FAILED',
|
|
errorMsg: stderr || `yt-dlp exited with code ${code}`,
|
|
completedAt: new Date(),
|
|
},
|
|
})
|
|
}
|
|
resolve()
|
|
})
|
|
})
|
|
}
|