fix: handle various heatmap data field names from YouTube API

This commit is contained in:
Kilo Code Cloud
2026-01-14 19:45:14 +00:00
parent 128a81722c
commit 57419cc58f

View File

@@ -8,16 +8,20 @@ export interface DownloadOptions {
format: string; format: string;
} }
interface HeatmapSegment { interface RawHeatmapSegment {
start_seconds: number; start_seconds?: number;
end_seconds: number; start_time?: number;
intensity: number; end_seconds?: number;
end_time?: number;
intensity?: number;
heat?: number;
value?: number;
} }
interface VideoInfo { interface VideoInfo {
title: string; title: string;
duration: number; duration: number;
heatmap?: HeatmapSegment[]; heatmap?: RawHeatmapSegment[];
} }
async function getVideoInfo(url: string): Promise<VideoInfo> { async function getVideoInfo(url: string): Promise<VideoInfo> {
@@ -49,11 +53,13 @@ async function getVideoInfo(url: string): Promise<VideoInfo> {
try { try {
const info = JSON.parse(stdout); 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; 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({ resolve({
title: info.title || "video", title: info.title || "video",
duration: info.duration || 0, duration: info.duration || 0,
@@ -115,11 +121,26 @@ function sanitizeFilename(filename: string): string {
} }
function formatTime(seconds: number): string { function formatTime(seconds: number): string {
if (!Number.isFinite(seconds) || seconds < 0) {
return "0:00";
}
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60); const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, "0")}`; 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<void> { export async function downloadMostWatchedSegment(options: DownloadOptions): Promise<void> {
const { url, outputDir, format } = options; const { url, outputDir, format } = options;
@@ -136,7 +157,7 @@ export async function downloadMostWatchedSegment(options: DownloadOptions): Prom
console.log(`Video: ${info.title}`); console.log(`Video: ${info.title}`);
console.log(`Duration: ${formatTime(info.duration)}`); 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) { if (!info.heatmap || info.heatmap.length === 0) {
console.log("\nNo heatmap data available for this video."); console.log("\nNo heatmap data available for this video.");
console.log("The video may not have enough view data to determine most watched segments."); 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; return;
} }
// Find the most watched segment (highest intensity) console.log(`\nHeatmap data found: ${info.heatmap.length} segments`);
const mostWatched = info.heatmap.reduce((max, current) => {
return current.intensity > max.intensity ? current : max; // 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`); if (validSegments.length === 0) {
console.log(`Most watched segment intensity: ${(mostWatched.intensity * 100).toFixed(1)}%`); console.log("No valid heatmap segments found. Downloading full video...");
console.log(`Segment: ${formatTime(mostWatched.start_seconds)} - ${formatTime(mostWatched.end_seconds)}`); 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 // Download the most watched segment
const outputPath = join(outputDir, `${safeTitle}_most_watched.%(ext)s`); const outputPath = join(outputDir, `${safeTitle}_most_watched.%(ext)s`);
console.log(`\nDownloading most watched segment...`); console.log(`\nDownloading most watched segment...`);
await downloadSegment( await downloadSegment(url, outputPath, startTime, endTime, format);
url,
outputPath,
mostWatched.start_seconds,
mostWatched.end_seconds,
format
);
// Save segment info // Save segment info
const segmentInfoPath = join(outputDir, `${safeTitle}_segment_info.txt`); const segmentInfoPath = join(outputDir, `${safeTitle}_segment_info.txt`);
const segmentInfo = `# ${info.title}\n\n` + const segmentInfo = `# ${info.title}\n\n` +
`Most watched segment (from YouTube heatmap):\n` + `Most watched segment (from YouTube heatmap):\n` +
` Start: ${formatTime(mostWatched.start_seconds)} (${mostWatched.start_seconds}s)\n` + ` Start: ${formatTime(startTime)} (${startTime}s)\n` +
` End: ${formatTime(mostWatched.end_seconds)} (${mostWatched.end_seconds}s)\n` + ` End: ${formatTime(endTime)} (${endTime}s)\n` +
` Duration: ${formatTime(mostWatched.end_seconds - mostWatched.start_seconds)}\n` + ` Duration: ${formatTime(endTime - startTime)}\n` +
` Intensity: ${(mostWatched.intensity * 100).toFixed(1)}%\n\n` + ` Intensity: ${(intensity * 100).toFixed(1)}%\n\n` +
`Note: This segment had the highest re-watch rate according to YouTube's analytics.\n`; `Note: This segment had the highest re-watch rate according to YouTube's analytics.\n`;
writeFileSync(segmentInfoPath, segmentInfo); writeFileSync(segmentInfoPath, segmentInfo);