diff --git a/src/cli/downloader.ts b/src/cli/downloader.ts index 5cbef44..a7fdb7b 100644 --- a/src/cli/downloader.ts +++ b/src/cli/downloader.ts @@ -77,11 +77,14 @@ async function getVideoInfo(url: string): Promise { }); } -async function downloadFullVideo(url: string, outputPath: string): Promise { +async function downloadFullVideo(url: string, outputDir: string, safeTitle: string): Promise { + // Download to a temp pattern, yt-dlp will fill in the filename + const tempPattern = join(outputDir, `${safeTitle}_temp_%(id)s.%(ext)s`); + return new Promise((resolve, reject) => { // Use minimal options - let yt-dlp use its config file const ytDlp = spawn("yt-dlp", [ - "-o", outputPath, + "-o", tempPattern, url, ]); @@ -96,7 +99,7 @@ async function downloadFullVideo(url: string, outputPath: string): Promise { @@ -105,6 +108,33 @@ async function downloadFullVideo(url: string, outputPath: string): Promise { + // Video extensions to look for (not subtitles) + const videoExtensions = ["mp4", "mkv", "webm", "mov", "avi", "m4v"]; + const subtitleExtensions = ["vtt", "srt", "ass", "lrc"]; + + return new Promise((resolve) => { + const glob = spawn("find", [outputDir, "-name", `${safeTitle}_temp_*`, "-type", "f"]); + let output = ""; + glob.stdout.on("data", (data) => { output += data.toString(); }); + glob.on("close", () => { + const files = output.split("\n").filter(f => f.length > 0); + + // Find video file (not subtitle) + for (const file of files) { + const ext = file.split(".").pop()?.toLowerCase(); + if (ext && videoExtensions.includes(ext)) { + resolve(file); + return; + } + } + + // If no video file, return null + resolve(null); + }); + }); +} + async function extractSegment( inputPath: string, outputPath: string, @@ -180,17 +210,14 @@ function clusterHighIntensitySegments( segments: RawSegment[], threshold: number ): ProcessedSegment[] { - // Filter to only high-intensity segments const highIntensity = segments.filter(seg => seg.intensity >= threshold); if (highIntensity.length === 0) { return []; } - // Sort by start time highIntensity.sort((a, b) => a.start - b.start); - // Cluster adjacent/overlapping segments const clusters: ProcessedSegment[] = []; let currentCluster: RawSegment | null = null; @@ -200,15 +227,12 @@ function clusterHighIntensitySegments( continue; } - // Check if this segment is adjacent or overlapping with current cluster const gap = seg.start - currentCluster.end; - const maxGap = 10; // Allow up to 10 second gap + const maxGap = 10; - if (gap <= maxGap && gap >= -1) { // -1 allows for small overlaps - // Merge into current cluster + if (gap <= maxGap && gap >= -1) { currentCluster.end = Math.max(currentCluster.end, seg.end); } else { - // Finalize current cluster and start new one const totalIntensity = highIntensity .filter(s => s.start >= currentCluster!.start && s.end <= currentCluster!.end) .reduce((sum, s) => sum + s.intensity, 0); @@ -227,7 +251,6 @@ function clusterHighIntensitySegments( } } - // Don't forget the last cluster if (currentCluster) { const totalIntensity = highIntensity .filter(s => s.start >= currentCluster.start && s.end <= currentCluster.end) @@ -244,9 +267,7 @@ function clusterHighIntensitySegments( }); } - // Sort clusters by total intensity (highest first) clusters.sort((a, b) => b.totalIntensity - a.totalIntensity); - return clusters; } @@ -315,42 +336,31 @@ export async function downloadMostWatchedSegment(options: DownloadOptions): Prom console.log(""); - // Download the top segment const topSegment = topSegments[0]; // Step 1: Download full video (uses your yt-dlp config) - const fullVideoPath = join(outputDir, `${safeTitle}_full_temp.%(ext)s`); console.log(`Downloading full video (using your yt-dlp config)...`); - await downloadFullVideo(url, fullVideoPath); + await downloadFullVideo(url, outputDir, safeTitle); - // Find the actual file (yt-dlp may have changed extension) - // The downloaded path should already be correct, but let's handle the pattern - const tempFiles = await new Promise((resolve) => { - const glob = spawn("find", [outputDir, "-name", `${safeTitle}_full_temp.*`, "-type", "f"]); - let output = ""; - glob.stdout.on("data", (data) => { output += data.toString(); }); - glob.on("close", () => { - resolve(output.split("\n").filter(f => f.length > 0)); - }); - }); + // Find video file (not subtitle) + const videoPath = await findVideoFile(outputDir, safeTitle); - if (tempFiles.length === 0) { - throw new Error("Could not find downloaded video file"); + if (!videoPath) { + throw new Error("No video file found. Your yt-dlp config may be downloading subtitles instead."); } - const actualFullPath = tempFiles[0]; - console.log(`Downloaded to: ${actualFullPath}`); + console.log(`Downloaded video: ${videoPath}`); - // Step 2: Extract segment with ffmpeg - const outputPath = join(outputDir, `${safeTitle}_segment.${actualFullPath.split(".").pop()}`); + // Step 2: Extract segment with ffmpeg to mkv + const outputPath = join(outputDir, `${safeTitle}_segment.mkv`); console.log(`Extracting segment: ${formatTime(topSegment.start)} - ${formatTime(topSegment.end)}`); - await extractSegment(actualFullPath, outputPath, topSegment.start, topSegment.end); + await extractSegment(videoPath, outputPath, topSegment.start, topSegment.end); - // Clean up temp file + // Clean up temp files try { - unlinkSync(actualFullPath); + unlinkSync(videoPath); } catch { // Ignore cleanup errors }