diff --git a/play-dl/YouTube/classes/Video.ts b/play-dl/YouTube/classes/Video.ts index 67682d0..c8c1ffb 100644 --- a/play-dl/YouTube/classes/Video.ts +++ b/play-dl/YouTube/classes/Video.ts @@ -82,7 +82,33 @@ interface VideoOptions { * Gives info about music content in that video. */ music?: VideoMusic[]; + /** + * The chapters for this video + * + * If the video doesn't have any chapters or if the video object wasn't created by {@link video_basic_info} or {@link video_info} this will be an empty array. + */ + chapters: VideoChapter[]; } + +export interface VideoChapter { + /** + * The title of the chapter + */ + title: string; + /** + * The timestamp of the start of the chapter + */ + timestamp: string; + /** + * The start of the chapter in seconds + */ + seconds: number; + /** + * Thumbnails of the frame at the start of this chapter + */ + thumbnails: YouTubeThumbnail[]; +} + /** * Class for YouTube Video url */ @@ -163,6 +189,12 @@ export class YouTubeVideo { * Gives info about music content in that video. */ music?: VideoMusic[]; + /** + * The chapters for this video + * + * If the video doesn't have any chapters or if the video object wasn't created by {@link video_basic_info} or {@link video_info} this will be an empty array. + */ + chapters: VideoChapter[]; /** * Constructor for YouTube Video Class * @param data JSON parsed data. @@ -193,6 +225,7 @@ export class YouTubeVideo { this.tags = data.tags || []; this.discretionAdvised = data.discretionAdvised ?? undefined; this.music = data.music || []; + this.chapters = data.chapters || []; } /** * Converts class to title name of video. @@ -222,7 +255,8 @@ export class YouTubeVideo { live: this.live, private: this.private, discretionAdvised: this.discretionAdvised, - music: this.music + music: this.music, + chapters: this.chapters }; } } diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index cfa21dd..ec245bd 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -1,6 +1,6 @@ import { request } from './../../Request/index'; import { format_decipher } from './cipher'; -import { YouTubeVideo } from '../classes/Video'; +import { VideoChapter, YouTubeVideo } from '../classes/Video'; import { YouTubePlayList } from '../classes/Playlist'; import { InfoData, StreamInfoData } from './constants'; import { URL, URLSearchParams } from 'node:url'; @@ -229,6 +229,21 @@ export async function video_basic_info(url: string, options: InfoOptions = {}): x.metadataRowRenderer.contents[0].simpleText ?? x.metadataRowRenderer.contents[0]?.runs?.[0]?.text; }); } + const rawChapters = + initial_response.playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer.decoratedPlayerBarRenderer.playerBar?.multiMarkersPlayerBarRenderer.markersMap.find( + (m: any) => m.key === 'DESCRIPTION_CHAPTERS' + ).value.chapters; + const chapters: VideoChapter[] = []; + if (rawChapters) { + for (const { chapterRenderer } of rawChapters) { + chapters.push({ + title: chapterRenderer.title.simpleText, + timestamp: parseSeconds(chapterRenderer.timeRangeStartMillis / 1000), + seconds: chapterRenderer.timeRangeStartMillis / 1000, + thumbnails: chapterRenderer.thumbnail.thumbnails + }); + } + } let upcomingDate; if (upcoming) { if (microformat.liveBroadcastDetails.startTimestamp) @@ -271,7 +286,8 @@ export async function video_basic_info(url: string, options: InfoOptions = {}): live: vid.isLiveContent, private: vid.isPrivate, discretionAdvised, - music + music, + chapters }); let format = []; if (!upcoming) {