From 57419cc58f42d015b77dd06f589f314044611335 Mon Sep 17 00:00:00 2001 From: Kilo Code Cloud Date: Wed, 14 Jan 2026 19:45:14 +0000 Subject: [PATCH] fix: handle various heatmap data field names from YouTube API --- src/cli/downloader.ts | 89 ++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/src/cli/downloader.ts b/src/cli/downloader.ts index 0adde6d..4fd0a0a 100644 --- a/src/cli/downloader.ts +++ b/src/cli/downloader.ts @@ -8,16 +8,20 @@ export interface DownloadOptions { format: string; } -interface HeatmapSegment { - start_seconds: number; - end_seconds: number; - intensity: number; +interface RawHeatmapSegment { + start_seconds?: number; + start_time?: number; + end_seconds?: number; + end_time?: number; + intensity?: number; + heat?: number; + value?: number; } interface VideoInfo { title: string; duration: number; - heatmap?: HeatmapSegment[]; + heatmap?: RawHeatmapSegment[]; } async function getVideoInfo(url: string): Promise { @@ -49,11 +53,13 @@ async function getVideoInfo(url: string): Promise { try { const info = JSON.parse(stdout); - - // Extract heatmap data from YouTube's internal API - // The heatmap shows what segments were re-watched the most const heatmapData = info.heatmap; + // Debug: log the actual structure if needed + if (heatmapData && heatmapData.length > 0) { + console.log("[Debug] First heatmap entry:", JSON.stringify(heatmapData[0], null, 2)); + } + resolve({ title: info.title || "video", duration: info.duration || 0, @@ -115,11 +121,26 @@ function sanitizeFilename(filename: string): string { } function formatTime(seconds: number): string { + if (!Number.isFinite(seconds) || seconds < 0) { + return "0:00"; + } const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, "0")}`; } +function getStartTime(segment: RawHeatmapSegment): number { + return segment.start_seconds ?? segment.start_time ?? 0; +} + +function getEndTime(segment: RawHeatmapSegment): number { + return segment.end_seconds ?? segment.end_time ?? 0; +} + +function getIntensity(segment: RawHeatmapSegment): number { + return segment.intensity ?? segment.heat ?? segment.value ?? 0; +} + export async function downloadMostWatchedSegment(options: DownloadOptions): Promise { const { url, outputDir, format } = options; @@ -136,7 +157,7 @@ export async function downloadMostWatchedSegment(options: DownloadOptions): Prom console.log(`Video: ${info.title}`); console.log(`Duration: ${formatTime(info.duration)}`); - // Check for heatmap data - this shows what was re-watched the most + // Check for heatmap data if (!info.heatmap || info.heatmap.length === 0) { console.log("\nNo heatmap data available for this video."); console.log("The video may not have enough view data to determine most watched segments."); @@ -147,35 +168,51 @@ export async function downloadMostWatchedSegment(options: DownloadOptions): Prom return; } - // Find the most watched segment (highest intensity) - const mostWatched = info.heatmap.reduce((max, current) => { - return current.intensity > max.intensity ? current : max; + console.log(`\nHeatmap data found: ${info.heatmap.length} segments`); + + // Filter out invalid segments and find the most watched + const validSegments = info.heatmap.filter(seg => { + const start = getStartTime(seg); + const end = getEndTime(seg); + const intensity = getIntensity(seg); + return Number.isFinite(start) && Number.isFinite(end) && Number.isFinite(intensity); }); - console.log(`\nHeatmap data found: ${info.heatmap.length} segments`); - console.log(`Most watched segment intensity: ${(mostWatched.intensity * 100).toFixed(1)}%`); - console.log(`Segment: ${formatTime(mostWatched.start_seconds)} - ${formatTime(mostWatched.end_seconds)}`); + if (validSegments.length === 0) { + console.log("No valid heatmap segments found. Downloading full video..."); + const outputPath = join(outputDir, `${safeTitle}.%(ext)s`); + await downloadSegment(url, outputPath, 0, info.duration, format); + return; + } + + // Find the most watched segment (highest intensity) + const mostWatched = validSegments.reduce((max, current) => { + const currentIntensity = getIntensity(current); + const maxIntensity = getIntensity(max); + return currentIntensity > maxIntensity ? current : max; + }); + + const startTime = getStartTime(mostWatched); + const endTime = getEndTime(mostWatched); + const intensity = getIntensity(mostWatched); + + console.log(`Most watched segment intensity: ${(intensity * 100).toFixed(1)}%`); + console.log(`Segment: ${formatTime(startTime)} - ${formatTime(endTime)}`); // Download the most watched segment const outputPath = join(outputDir, `${safeTitle}_most_watched.%(ext)s`); console.log(`\nDownloading most watched segment...`); - await downloadSegment( - url, - outputPath, - mostWatched.start_seconds, - mostWatched.end_seconds, - format - ); + await downloadSegment(url, outputPath, startTime, endTime, format); // Save segment info const segmentInfoPath = join(outputDir, `${safeTitle}_segment_info.txt`); const segmentInfo = `# ${info.title}\n\n` + `Most watched segment (from YouTube heatmap):\n` + - ` Start: ${formatTime(mostWatched.start_seconds)} (${mostWatched.start_seconds}s)\n` + - ` End: ${formatTime(mostWatched.end_seconds)} (${mostWatched.end_seconds}s)\n` + - ` Duration: ${formatTime(mostWatched.end_seconds - mostWatched.start_seconds)}\n` + - ` Intensity: ${(mostWatched.intensity * 100).toFixed(1)}%\n\n` + + ` Start: ${formatTime(startTime)} (${startTime}s)\n` + + ` End: ${formatTime(endTime)} (${endTime}s)\n` + + ` Duration: ${formatTime(endTime - startTime)}\n` + + ` Intensity: ${(intensity * 100).toFixed(1)}%\n\n` + `Note: This segment had the highest re-watch rate according to YouTube's analytics.\n`; writeFileSync(segmentInfoPath, segmentInfo);