import { spawn } from "child_process"; import { writeFileSync, mkdirSync, existsSync } from "fs"; import { join } from "path"; export interface DownloadOptions { url: string; outputDir: string; format: string; extractChapters: boolean; } interface Chapter { title: string; start: number; end: number; } async function getVideoInfo(url: string): Promise<{ title: string; chapters: Chapter[] }> { return new Promise((resolve, reject) => { const ytDlp = spawn("yt-dlp", [ "--dump-json", "--no-download", url, ]); let stdout = ""; let stderr = ""; ytDlp.stdout.on("data", (data) => { stdout += data.toString(); }); ytDlp.stderr.on("data", (data) => { stderr += data.toString(); }); ytDlp.on("close", (code) => { if (code !== 0) { reject(new Error(`yt-dlp failed: ${stderr}`)); return; } try { const info = JSON.parse(stdout); const chapters: Chapter[] = []; if (info.chapters && Array.isArray(info.chapters)) { for (const chapter of info.chapters) { chapters.push({ title: chapter.title || `Chapter ${chapters.length + 1}`, start: chapter.start_time || 0, end: chapter.end_time || 0, }); } } resolve({ title: info.title || "video", chapters, }); } catch (parseError) { reject(new Error(`Failed to parse video info: ${parseError}`)); } }); ytDlp.on("error", (err) => { reject(new Error(`Failed to run yt-dlp: ${err.message}`)); }); }); } async function downloadSection( url: string, outputPath: string, section: string, format: string ): Promise { return new Promise((resolve, reject) => { const ytDlp = spawn("yt-dlp", [ "-f", format, "--download-sections", section, "-o", outputPath, url, ]); let stderr = ""; ytDlp.stderr.on("data", (data) => { stderr += data.toString(); }); ytDlp.on("close", (code) => { if (code !== 0) { reject(new Error(`yt-dlp failed: ${stderr}`)); return; } resolve(); }); ytDlp.on("error", (err) => { reject(new Error(`Failed to run yt-dlp: ${err.message}`)); }); }); } function sanitizeFilename(filename: string): string { return filename .replace(/[^a-zA-Z0-9\s\-_]/g, "") .replace(/\s+/g, "_") .substring(0, 100); } function formatTime(seconds: number): string { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; } export async function downloadVideoSegments(options: DownloadOptions): Promise { const { url, outputDir, format } = options; // Create output directory if it doesn't exist if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } // Skip chapter extraction if not requested if (!options.extractChapters) { console.log("Downloading full video (chapters disabled)..."); const safeTitle = sanitizeFilename("video"); const outputPath = join(outputDir, `${safeTitle}.%(ext)s`); await downloadSection(url, outputPath, "*", format); return; } // Get video info including chapters console.log("Fetching video information..."); const { title, chapters } = await getVideoInfo(url); const safeTitle = sanitizeFilename(title); console.log(`Video: ${title}`); console.log(`Found ${chapters.length} chapters\n`); if (chapters.length === 0) { console.log("No chapters found. Downloading full video..."); const outputPath = join(outputDir, `${safeTitle}.%(ext)s`); await downloadSection(url, outputPath, "*", format); return; } // Download each chapter as a separate segment for (let i = 0; i < chapters.length; i++) { const chapter = chapters[i]; const segmentNumber = (i + 1).toString().padStart(2, "0"); const outputPath = join(outputDir, `${safeTitle}_${segmentNumber}_${sanitizeFilename(chapter.title)}.%(ext)s`); const section = `*${formatTime(chapter.start)}-${formatTime(chapter.end)}`; console.log(`[${i + 1}/${chapters.length}] Downloading: ${chapter.title} (${formatTime(chapter.end - chapter.start)})`); await downloadSection(url, outputPath, section, format); } // Also download the full video console.log("\nDownloading full video..."); const fullVideoPath = join(outputDir, `${safeTitle}_full.%(ext)s`); await downloadSection(url, fullVideoPath, "*", format); // Save chapter list const chapterListPath = join(outputDir, `${safeTitle}_chapters.txt`); let chapterList = `# ${title}\n\n`; for (const chapter of chapters) { chapterList += `${formatTime(chapter.start)} - ${chapter.title}\n`; } writeFileSync(chapterListPath, chapterList); console.log(`Chapter list saved to: ${chapterListPath}`); }