diff --git a/src/cli/args.ts b/src/cli/args.ts index b0ade52..89c8b20 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -2,14 +2,14 @@ export interface CliArgs { url?: string; output: string; format: string; - noChapters: boolean; + segments: number; } export function parseArgs(): CliArgs { const args: CliArgs = { output: "./downloads", format: "best", - noChapters: false, + segments: 1, }; const rawArgs = Bun.argv; @@ -24,10 +24,11 @@ export function parseArgs(): CliArgs { } else if (arg === "-f" || arg === "--format") { args.format = nextArg || "best"; i++; - } else if (arg === "--no-chapters") { - args.noChapters = true; + } else if (arg === "-n" || arg === "--segments") { + args.segments = parseInt(nextArg || "1", 10); + i++; } else if (arg === "-h" || arg === "--help") { - console.log(`YouTube Video Segments Downloader + console.log(`YouTube Most Watched Segments Downloader Usage: yt-segments [options] @@ -37,13 +38,13 @@ Arguments: Options: -o, --output Output directory (default: ./downloads) -f, --format Video format (default: best) - --no-chapters Skip chapter extraction + -n, --segments Number of top segments to download (default: 1) -h, --help Show this help message Examples: yt-segments "https://www.youtube.com/watch?v=abc123" yt-segments "https://youtu.be/abc123" -o ./videos -f mp4 - yt-segments "https://www.youtube.com/watch?v=abc123" --no-chapters + yt-segments "https://www.youtube.com/watch?v=abc123" -n 3 `); process.exit(0); } else if (!arg.startsWith("-") && !arg.includes("bun")) { diff --git a/src/cli/downloader.ts b/src/cli/downloader.ts index 281e38b..e549ee1 100644 --- a/src/cli/downloader.ts +++ b/src/cli/downloader.ts @@ -9,13 +9,20 @@ export interface DownloadOptions { extractChapters: boolean; } -interface Chapter { - title: string; +interface MostWatchedSegment { start: number; end: number; + intensity?: number; } -async function getVideoInfo(url: string): Promise<{ title: string; chapters: Chapter[] }> { +interface VideoInfo { + title: string; + chapters: Array<{ title: string; start_time: number; end_time: number }>; + heatmap?: Array<{ start_seconds: number; end_seconds: number; intensity: number }>; + duration: number; +} + +async function getVideoInfo(url: string): Promise { return new Promise((resolve, reject) => { const ytDlp = spawn("yt-dlp", [ "--dump-json", @@ -42,21 +49,12 @@ async function getVideoInfo(url: string): Promise<{ title: string; chapters: Cha 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, + chapters: info.chapters || [], + heatmap: info.heatmap || [], + duration: info.duration || 0, }); } catch (parseError) { reject(new Error(`Failed to parse video info: ${parseError}`)); @@ -116,6 +114,28 @@ function formatTime(seconds: number): string { return `${mins}:${secs.toString().padStart(2, "0")}`; } +function getMostWatchedSegments( + heatmap: Array<{ start_seconds: number; end_seconds: number; intensity: number }>, + duration: number, + topN: number = 1 +): MostWatchedSegment[] { + if (!heatmap || heatmap.length === 0) { + return []; + } + + // Sort by intensity (most watched first) + const sorted = [...heatmap].sort((a, b) => b.intensity - a.intensity); + + // Get top N segments + const topSegments = sorted.slice(0, topN); + + return topSegments.map((segment) => ({ + start: segment.start_seconds, + end: segment.end_seconds, + intensity: segment.intensity, + })); +} + export async function downloadVideoSegments(options: DownloadOptions): Promise { const { url, outputDir, format } = options; @@ -124,53 +144,65 @@ export async function downloadVideoSegments(options: DownloadOptions): Promise 0) { + console.log("\nNo most watched segments found. Falling back to chapters..."); + const chapter = info.chapters[0]; // Download first chapter as most relevant + const outputPath = join(outputDir, `${safeTitle}_most_watched.%(ext)s`); + const section = `*${formatTime(chapter.start_time || 0)}-${formatTime(chapter.end_time || 60)}`; + console.log(`Downloading chapter: ${chapter.title || "First Chapter"}`); await downloadSection(url, outputPath, section, format); + return; } - // 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`; + if (mostWatchedSegments.length === 0) { + console.log("No segments found. Downloading full video..."); + const outputPath = join(outputDir, `${safeTitle}.%(ext)s`); + await downloadSection(url, outputPath, "*", format); + return; } - writeFileSync(chapterListPath, chapterList); - console.log(`Chapter list saved to: ${chapterListPath}`); + + // Download the most watched segment + const topSegment = mostWatchedSegments[0]; + const outputPath = join(outputDir, `${safeTitle}_most_watched.%(ext)s`); + const section = `*${formatTime(topSegment.start)}-${formatTime(topSegment.end)}`; + + console.log(`\nMost watched segment: ${formatTime(topSegment.start)} - ${formatTime(topSegment.end)}`); + console.log(`Duration: ${formatTime(topSegment.end - topSegment.start)}`); + console.log(`Intensity: ${((topSegment.intensity || 0) * 100).toFixed(1)}%`); + console.log(`\nDownloading most watched segment...`); + + await downloadSection(url, outputPath, section, format); + + // Save segment info + const segmentInfoPath = join(outputDir, `${safeTitle}_most_watched.txt`); + let segmentInfo = `# ${info.title}\n\n`; + segmentInfo += `Most watched segment:\n`; + segmentInfo += ` Start: ${formatTime(topSegment.start)}\n`; + segmentInfo += ` End: ${formatTime(topSegment.end)}\n`; + segmentInfo += ` Duration: ${formatTime(topSegment.end - topSegment.start)}\n`; + segmentInfo += ` Intensity: ${((topSegment.intensity || 0) * 100).toFixed(1)}%\n`; + + if (mostWatchedSegments.length > 1) { + segmentInfo += `\nOther top segments:\n`; + for (let i = 1; i < mostWatchedSegments.length; i++) { + const seg = mostWatchedSegments[i]; + segmentInfo += ` ${formatTime(seg.start)} - ${formatTime(seg.end)} (${((seg.intensity || 0) * 100).toFixed(1)}%)\n`; + } + } + + writeFileSync(segmentInfoPath, segmentInfo); + console.log(`Segment info saved to: ${segmentInfoPath}`); } diff --git a/src/cli/index.ts b/src/cli/index.ts index c2760e1..f8cdeae 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -12,15 +12,15 @@ async function main() { console.log("Options:"); console.log(" -o, --output Output directory (default: ./downloads)"); console.log(" -f, --format Video format (default: best)"); - console.log(" --no-chapters Skip chapter extraction"); + console.log(" -n, --segments Number of top segments (default: 1)"); console.log(" -h, --help Show help"); process.exit(1); } - console.log(`Downloading segments from: ${args.url}`); + console.log(`Downloading most watched segment(s) from: ${args.url}`); console.log(`Output directory: ${args.output}`); console.log(`Format: ${args.format}`); - console.log(`Extract chapters: ${!args.noChapters}`); + console.log(`Segments to download: ${args.segments}`); console.log(""); try { @@ -28,7 +28,7 @@ async function main() { url: args.url, outputDir: args.output, format: args.format, - extractChapters: !args.noChapters, + extractChapters: true, }); console.log("\nDownload complete!"); } catch (error) {