feat(youtube-transcript): add end times to chapter data

Add 'end' field to Chapter interface and parseChapters output.
Each chapter's end is derived from the next chapter's start time,
with the last chapter ending at the video's total duration.

This makes chapter data complete and ready for downstream consumers
(e.g. video clipping with ffmpeg) without requiring them to compute
end times from adjacent chapters.

Before: { title: 'Overview', start: 0 }
After:  { title: 'Overview', start: 0, end: 21 }
This commit is contained in:
jzocb
2026-03-22 15:52:30 -04:00
parent ba20cf89f2
commit 8d973f2bc5
@@ -44,6 +44,7 @@ interface TranscriptInfo {
interface Chapter { interface Chapter {
title: string; title: string;
start: number; start: number;
end: number;
} }
interface VideoMeta { interface VideoMeta {
@@ -249,16 +250,21 @@ async function fetchTranscriptSnippets(info: TranscriptInfo, translateTo?: strin
// --- Metadata & chapters --- // --- Metadata & chapters ---
function parseChapters(description: string): Chapter[] { function parseChapters(description: string, duration: number = 0): Chapter[] {
const chapters: Chapter[] = []; const raw: { title: string; start: number }[] = [];
for (const line of description.split("\n")) { for (const line of description.split("\n")) {
const m = line.trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})\s+(.+)$/); const m = line.trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})\s+(.+)$/);
if (m) { if (m) {
const h = m[1] ? parseInt(m[1]) : 0; const h = m[1] ? parseInt(m[1]) : 0;
chapters.push({ title: m[4].trim(), start: h * 3600 + parseInt(m[2]) * 60 + parseInt(m[3]) }); raw.push({ title: m[4].trim(), start: h * 3600 + parseInt(m[2]) * 60 + parseInt(m[3]) });
} }
} }
return chapters.length >= 2 ? chapters : []; if (raw.length < 2) return [];
return raw.map((ch, i) => ({
title: ch.title,
start: ch.start,
end: i < raw.length - 1 ? raw[i + 1].start : duration,
}));
} }
function getThumbnailUrls(videoId: string, data: any): string[] { function getThumbnailUrls(videoId: string, data: any): string[] {
@@ -644,7 +650,8 @@ async function fetchAndCache(videoId: string, baseDir: string, opts: Options): P
const info = findTranscript(transcripts, opts.languages, opts.excludeGenerated, opts.excludeManual); const info = findTranscript(transcripts, opts.languages, opts.excludeGenerated, opts.excludeManual);
const result = await fetchTranscriptSnippets(info, opts.translate || undefined); const result = await fetchTranscriptSnippets(info, opts.translate || undefined);
const description = data?.videoDetails?.shortDescription || ""; const description = data?.videoDetails?.shortDescription || "";
const chapters = parseChapters(description); const duration = parseInt(data?.videoDetails?.lengthSeconds || "0");
const chapters = parseChapters(description, duration);
const langInfo = { code: result.languageCode, name: result.language, isGenerated: info.isGenerated }; const langInfo = { code: result.languageCode, name: result.language, isGenerated: info.isGenerated };
const meta = buildVideoMeta(data, videoId, langInfo, chapters); const meta = buildVideoMeta(data, videoId, langInfo, chapters);