import { exec } from 'child_process'; import { promisify } from 'util'; import { readFile, unlink, mkdir, copyFile } from 'fs/promises'; import { existsSync } from 'fs'; import path from 'path'; import type { VideoInfo, FormatOption, DownloadRequest, DownloadResponse } from './types'; const execAsync = promisify(exec); // Get yt-dlp path from environment variable or use default // Read at runtime to ensure env vars are loaded function getYtDlpPath(): string { return process.env.YT_DLP_PATH || 'yt-dlp'; } // Get ffmpeg location flag for yt-dlp (optional) function getFfmpegLocationFlag(): string { const ffmpegPath = process.env.FFMPEG_PATH; return ffmpegPath ? `--ffmpeg-location "${ffmpegPath}"` : ''; } const DOWNLOADS_DIR = path.join(process.cwd(), 'downloads'); const PUBLIC_DOWNLOADS_DIR = path.join(process.cwd(), 'public', 'downloads'); // Sanitize filename for safe storage function sanitizeFilename(filename: string): string { return filename .replace(/[^a-z0-9]/gi, '_') .replace(/_+/g, '_') .substring(0, 200); } // Ensure directories exist async function ensureDirectories() { if (!existsSync(DOWNLOADS_DIR)) { await mkdir(DOWNLOADS_DIR, { recursive: true }); } if (!existsSync(PUBLIC_DOWNLOADS_DIR)) { await mkdir(PUBLIC_DOWNLOADS_DIR, { recursive: true }); } } export async function getVideoInfo(url: string): Promise { try { await ensureDirectories(); // Get video info as JSON const ytDlpPath = getYtDlpPath(); console.log('[getVideoInfo] Using yt-dlp path:', ytDlpPath); // Increase maxBuffer to handle large JSON responses from long videos // Default is 1MB, increase to 10MB to handle videos with many formats const { stdout } = await execAsync( `"${ytDlpPath}" --dump-json --no-download "${url}"`, { maxBuffer: 10 * 1024 * 1024 } // 10MB buffer ); const info = JSON.parse(stdout); const formats: FormatOption[] = (info.formats || []).map((f: any) => ({ formatId: f.format_id, ext: f.ext, resolution: f.resolution || `${f.width}x${f.height}`, filesize: f.filesize, formatNote: f.format_note, })); return { title: info.title || 'Unknown', duration: info.duration || 0, thumbnail: info.thumbnail, formats, }; } catch (error) { console.error('Error getting video info:', error); return null; } } export async function downloadVideo( request: DownloadRequest ): Promise { try { await ensureDirectories(); const { url, format, formatType } = request; const fileId = `${Date.now()}-${Math.random().toString(36).substring(7)}`; let ytDlpFormat = ''; let outputExt = format; if (formatType === 'audio') { // Audio formats if (format === 'mp3') { ytDlpFormat = 'bestaudio[ext=m4a]/bestaudio'; outputExt = 'mp3'; } else if (format === 'wav') { ytDlpFormat = 'bestaudio[ext=m4a]/bestaudio'; outputExt = 'wav'; } else { ytDlpFormat = `bestaudio[ext=${format}]/bestaudio`; } } else { // Video formats ytDlpFormat = `bestvideo[ext=${format}]+bestaudio[ext=m4a]/best[ext=${format}]/best`; } const outputPath = path.join(DOWNLOADS_DIR, `${fileId}.%(ext)s`); const finalPath = path.join(PUBLIC_DOWNLOADS_DIR, `${fileId}.${outputExt}`); // Get yt-dlp path and ffmpeg location at runtime const ytDlpPath = getYtDlpPath(); const ffmpegFlag = getFfmpegLocationFlag(); console.log('[downloadVideo] Using yt-dlp path:', ytDlpPath); if (ffmpegFlag) console.log('[downloadVideo] Using ffmpeg location flag:', ffmpegFlag); // Download with yt-dlp let command = `"${ytDlpPath}" ${ffmpegFlag} -f "${ytDlpFormat}" -o "${outputPath}" "${url}"`; if (formatType === 'audio' && format === 'mp3') { // For MP3, we need to extract audio and convert command = `"${ytDlpPath}" ${ffmpegFlag} -f "bestaudio[ext=m4a]/bestaudio" -x --audio-format mp3 --audio-quality 0 -o "${outputPath}" "${url}"`; } else if (formatType === 'audio' && format === 'wav') { // For WAV, extract and convert command = `"${ytDlpPath}" ${ffmpegFlag} -f "bestaudio[ext=m4a]/bestaudio" -x --audio-format wav -o "${outputPath}" "${url}"`; } // Increase maxBuffer for downloads as well await execAsync(command, { maxBuffer: 10 * 1024 * 1024 }); // 10MB buffer // Find the downloaded file const files = await import('fs/promises').then(fs => fs.readdir(DOWNLOADS_DIR) ); const downloadedFile = files.find(f => f.startsWith(fileId)); if (!downloadedFile) { throw new Error('Downloaded file not found'); } const downloadedPath = path.join(DOWNLOADS_DIR, downloadedFile); const downloadedExt = path.extname(downloadedFile).slice(1); // Get video info for response (before moving file) const videoInfo = await getVideoInfo(url); const safeFilename = sanitizeFilename(videoInfo?.title || 'video'); // If extension matches, just copy. Otherwise, rename during copy if (downloadedExt === outputExt) { // Copy file to public directory await copyFile(downloadedPath, finalPath); await unlink(downloadedPath); } else { // For format conversions, yt-dlp should have already converted // But if extension doesn't match, copy with new extension await copyFile(downloadedPath, finalPath); await unlink(downloadedPath); } return { success: true, downloadUrl: `/downloads/${fileId}.${outputExt}`, filename: `${safeFilename}.${outputExt}`, videoInfo: videoInfo || undefined, }; } catch (error: any) { console.error('Error downloading video:', error); return { success: false, error: error.message || 'Failed to download video', }; } }