diff --git a/src/cli/args.ts b/src/cli/args.ts index 3b5dd06..67f7e0c 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -2,14 +2,12 @@ export interface CliArgs { url?: string; output: string; format: string; - skipStartSeconds: number; } export function parseArgs(): CliArgs { const args: CliArgs = { output: "./downloads", format: "best", - skipStartSeconds: 30, }; const rawArgs = Bun.argv; @@ -24,9 +22,6 @@ export function parseArgs(): CliArgs { } else if (arg === "-f" || arg === "--format") { args.format = nextArg || "best"; i++; - } else if (arg === "-s" || arg === "--skip-start") { - args.skipStartSeconds = parseFloat(nextArg || "30"); - i++; } else if (arg === "-h" || arg === "--help") { console.log(`YouTube Most Watched Segment Downloader @@ -36,16 +31,13 @@ Arguments: YouTube video URL (required) Options: - -o, --output Output directory (default: ./downloads) - -f, --format Video format (default: best) - -s, --skip-start Skip first N seconds (default: 30) - Higher values skip more of the intro - -h, --help Show this help message + -o, --output Output directory (default: ./downloads) + -f, --format Video format (default: best) + -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" -s 60 `); process.exit(0); } else if (!arg.startsWith("-") && !arg.includes("bun")) { diff --git a/src/cli/downloader.ts b/src/cli/downloader.ts index 983d865..d103091 100644 --- a/src/cli/downloader.ts +++ b/src/cli/downloader.ts @@ -6,7 +6,6 @@ export interface DownloadOptions { url: string; outputDir: string; format: string; - skipStartSeconds: number; } interface RawHeatmapSegment { @@ -23,7 +22,7 @@ interface ProcessedSegment { start: number; end: number; intensity: number; - integral: number; + integralJump: number; } interface VideoInfo { @@ -143,9 +142,8 @@ function getIntensity(segment: RawHeatmapSegment): number { return segment.intensity ?? segment.heat ?? segment.value ?? 0; } -function findSegmentByIntegral( - segments: RawHeatmapSegment[], - skipStartSeconds: number +function findHighestIntegralJump( + segments: RawHeatmapSegment[] ): ProcessedSegment | null { // Convert to processed format and filter valid segments const validSegments = segments @@ -166,32 +164,25 @@ function findSegmentByIntegral( return null; } - // Calculate the primitive (integral) for each segment - // The integral represents cumulative watch time contribution - let cumulativeIntegral = 0; + // Calculate integral jump for each segment (intensity × duration) const withIntegral = validSegments.map(seg => { const segmentDuration = seg.end - seg.start; - const segmentIntegral = seg.intensity * segmentDuration; - cumulativeIntegral += segmentIntegral; + const integralJump = seg.intensity * segmentDuration; return { ...seg, - integral: cumulativeIntegral, + integralJump, }; }); - // Sort by integral value (highest contribution first) - // The integral tells us which segment contributed most to total watch time - withIntegral.sort((a, b) => b.integral - a.integral); - - // Find the highest integral segment that starts after skipStartSeconds - const candidate = withIntegral.find(seg => seg.start >= skipStartSeconds); - - // If all segments are in the skipped region, return the highest one anyway - return candidate || withIntegral[0]; + // Find the segment with the highest integral jump (biggest bump in the integral) + // This is the segment that contributed most to the total integral + return withIntegral.reduce((max, current) => { + return current.integralJump > max.integralJump ? current : max; + }); } export async function downloadMostWatchedSegment(options: DownloadOptions): Promise { - const { url, outputDir, format, skipStartSeconds } = options; + const { url, outputDir, format } = options; // Create output directory if it doesn't exist if (!existsSync(outputDir)) { @@ -218,8 +209,8 @@ export async function downloadMostWatchedSegment(options: DownloadOptions): Prom console.log(`\nHeatmap data found: ${info.heatmap.length} segments`); - // Find the segment with highest integral (using primitive function) - const topSegment = findSegmentByIntegral(info.heatmap, skipStartSeconds); + // Find segment with highest integral jump (biggest bump) + const topSegment = findHighestIntegralJump(info.heatmap); if (!topSegment) { console.log("No valid segments found. Downloading full video..."); @@ -228,11 +219,11 @@ export async function downloadMostWatchedSegment(options: DownloadOptions): Prom return; } - console.log(`\nSegment with highest integral (primitive):`); + console.log(`\nSegment with highest integral jump:`); console.log(` Time: ${formatTime(topSegment.start)} - ${formatTime(topSegment.end)}`); console.log(` Duration: ${formatTime(topSegment.end - topSegment.start)}`); console.log(` Intensity: ${(topSegment.intensity * 100).toFixed(1)}%`); - console.log(` Integral: ${topSegment.integral.toFixed(4)}`); + console.log(` Integral Jump: ${topSegment.integralJump.toFixed(4)}`); // Download the segment const outputPath = join(outputDir, `${safeTitle}_most_watched.%(ext)s`); @@ -243,13 +234,12 @@ export async function downloadMostWatchedSegment(options: DownloadOptions): Prom // Save segment info const segmentInfoPath = join(outputDir, `${safeTitle}_segment_info.txt`); const segmentInfo = `# ${info.title}\n\n` + - `Most watched segment (highest integral from YouTube heatmap):\n` + + `Segment with highest integral jump (biggest bump in heatmap):\n` + ` Start: ${formatTime(topSegment.start)} (${topSegment.start.toFixed(1)}s)\n` + ` End: ${formatTime(topSegment.end)} (${topSegment.end.toFixed(1)}s)\n` + ` Duration: ${formatTime(topSegment.end - topSegment.start)}\n` + ` Intensity: ${(topSegment.intensity * 100).toFixed(1)}%\n` + - ` Integral: ${topSegment.integral.toFixed(4)}\n\n` + - `Note: This segment had the highest integral value (cumulative watch contribution).\n`; + ` Integral Jump: ${topSegment.integralJump.toFixed(4)}\n`; writeFileSync(segmentInfoPath, segmentInfo); console.log(`\nSegment info saved to: ${segmentInfoPath}`); diff --git a/src/cli/index.ts b/src/cli/index.ts index 97a216e..6567a65 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -10,17 +10,15 @@ async function main() { console.error("Error: YouTube URL is required"); console.log("Usage: yt-segments [options]"); console.log("Options:"); - console.log(" -o, --output Output directory (default: ./downloads)"); - console.log(" -f, --format Video format (default: best)"); - console.log(" -s, --skip-start Skip first N seconds (default: 30)"); - console.log(" -h, --help Show help"); + console.log(" -o, --output Output directory (default: ./downloads)"); + console.log(" -f, --format Video format (default: best)"); + console.log(" -h, --help Show help"); process.exit(1); } console.log(`Downloading most watched segment from: ${args.url}`); console.log(`Output directory: ${args.output}`); console.log(`Format: ${args.format}`); - console.log(`Skip first ${args.skipStartSeconds} seconds`); console.log(""); try { @@ -28,7 +26,6 @@ async function main() { url: args.url, outputDir: args.output, format: args.format, - skipStartSeconds: args.skipStartSeconds, }); } catch (error) { console.error("Error:", error instanceof Error ? error.message : error);