diff --git a/play-dl/YouTube/classes/Video.ts b/play-dl/YouTube/classes/Video.ts index 63f8587..8cdb9a1 100644 --- a/play-dl/YouTube/classes/Video.ts +++ b/play-dl/YouTube/classes/Video.ts @@ -38,6 +38,10 @@ interface VideoOptions { * YouTube Video Uploaded Date */ uploadedAt?: string; + /** + * If the video is upcoming or a premiere that isn't currently live, this will contain the premiere date, for watch page playlists this will be true, it defaults to undefined + */ + upcoming?: Date | true; /** * YouTube Views */ @@ -115,6 +119,10 @@ export class YouTubeVideo { * YouTube Video Uploaded Date */ uploadedAt?: string; + /** + * If the video is upcoming or a premiere that isn't currently live, this will contain the premiere date, for watch page playlists this will be true, it defaults to undefined + */ + upcoming?: Date | true; /** * YouTube Views */ @@ -166,6 +174,7 @@ export class YouTubeVideo { this.durationRaw = data.duration_raw || '0:00'; this.durationInSec = (data.duration < 0 ? 0 : data.duration) || 0; this.uploadedAt = data.uploadedAt || undefined; + this.upcoming = data.upcoming; this.views = parseInt(data.views) || 0; const thumbnails = []; for (const thumb of data.thumbnails) { diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index b9b7aef..d0b4e51 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -19,7 +19,7 @@ export interface StreamOptions { language?: string; htmldata?: boolean; precache?: number; - discordPlayerCompatibility?: boolean + discordPlayerCompatibility?: boolean; } /** @@ -63,6 +63,9 @@ export async function stream_from_info( info: InfoData | StreamInfoData, options: StreamOptions = {} ): Promise { + if (!info.format || info.format.length === 0) + throw new Error('Upcoming and premiere videos that are not currently live cannot be streamed.'); + const final: any[] = []; if ( info.LiveStreamData.isLive === true && @@ -87,8 +90,8 @@ export async function stream_from_info( final[0].codec === 'opus' && final[0].container === 'webm' ? StreamType.WebmOpus : StreamType.Arbitrary; await request_stream(`https://${new URL(final[0].url).host}/generate_204`); if (type === StreamType.WebmOpus) { - if(!options.discordPlayerCompatibility){ - options.seek ??= 0 + if (!options.discordPlayerCompatibility) { + options.seek ??= 0; if (options.seek >= info.video_details.durationInSec || options.seek < 0) throw new Error(`Seeking beyond limit. [ 0 - ${info.video_details.durationInSec - 1}]`); return new SeekStream( @@ -99,7 +102,7 @@ export async function stream_from_info( info.video_details.url, options ); - } else if(options.seek) throw new Error("Can not seek with discordPlayerCompatibility set to true.") + } else if (options.seek) throw new Error('Can not seek with discordPlayerCompatibility set to true.'); } return new Stream( final[0].url, diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index 8f45f27..8e5bf9d 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -159,6 +159,7 @@ export async function video_basic_info(url: string, options: InfoOptions = {}): const vid = player_response.videoDetails; let discretionAdvised = false; + let upcoming = false; if (player_response.playabilityStatus.status !== 'OK') { if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') { if (options.htmldata) @@ -179,7 +180,8 @@ export async function video_basic_info(url: string, options: InfoOptions = {}): const updatedValues = await acceptViewerDiscretion(vid.videoId, cookieJar, body, true); player_response.streamingData = updatedValues.streamingData; initial_response.contents.twoColumnWatchNextResults.secondaryResults = updatedValues.relatedVideos; - } else + } else if (player_response.playabilityStatus.status === 'LIVE_STREAM_OFFLINE') upcoming = true; + else throw new Error( `While getting info from url\n${ player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ?? @@ -223,6 +225,17 @@ export async function video_basic_info(url: string, options: InfoOptions = {}): x.metadataRowRenderer.contents[0].simpleText ?? x.metadataRowRenderer.contents[0]?.runs?.[0]?.text; }); } + let upcomingDate; + if (upcoming) { + if (microformat.liveBroadcastDetails.startTimestamp) + upcomingDate = new Date(microformat.liveBroadcastDetails.startTimestamp); + else { + const timestamp = + player_response.playabilityStatus.liveStreamability.liveStreamabilityRenderer.offlineSlate + .liveStreamOfflineSlateRenderer.scheduledStartTime; + upcomingDate = new Date(parseInt(timestamp) * 1000); + } + } const video_details = new YouTubeVideo({ id: vid.videoId, title: vid.title, @@ -230,6 +243,7 @@ export async function video_basic_info(url: string, options: InfoOptions = {}): duration: Number(vid.lengthSeconds), duration_raw: parseSeconds(vid.lengthSeconds), uploadedAt: microformat.publishDate, + upcoming: upcomingDate, thumbnails: vid.thumbnail.thumbnails, channel: { name: vid.author, @@ -254,8 +268,11 @@ export async function video_basic_info(url: string, options: InfoOptions = {}): discretionAdvised, music }); - const format = player_response.streamingData.formats ?? []; - format.push(...(player_response.streamingData.adaptiveFormats ?? [])); + let format = []; + if (!upcoming) { + format.push(...(player_response.streamingData.formats ?? [])); + format.push(...(player_response.streamingData.adaptiveFormats ?? [])); + } const LiveStreamData = { isLive: video_details.live, dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null, @@ -304,6 +321,7 @@ export async function video_stream_info(url: string, options: InfoOptions = {}): .split(/;\s*(var|const|let)\s/)[0]; if (!player_data) throw new Error('Initial Player Response Data is undefined.'); const player_response = JSON.parse(player_data); + let upcoming = false; if (player_response.playabilityStatus.status !== 'OK') { if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') { if (options.htmldata) @@ -334,7 +352,8 @@ export async function video_stream_info(url: string, options: InfoOptions = {}): false ); player_response.streamingData = updatedValues.streamingData; - } else + } else if (player_response.playabilityStatus.status === 'LIVE_STREAM_OFFLINE') upcoming = true; + else throw new Error( `While getting info from url\n${ player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ?? @@ -348,8 +367,11 @@ export async function video_stream_info(url: string, options: InfoOptions = {}): url: `https://www.youtube.com/watch?v=${player_response.videoDetails.videoId}`, durationInSec: (duration < 0 ? 0 : duration) || 0 }; - const format = player_response.streamingData.formats ?? []; - format.push(...(player_response.streamingData.adaptiveFormats ?? [])); + let format = []; + if (!upcoming) { + format.push(...(player_response.streamingData.formats ?? [])); + format.push(...(player_response.streamingData.adaptiveFormats ?? [])); + } const LiveStreamData = { isLive: player_response.videoDetails.isLiveContent, @@ -414,7 +436,7 @@ export async function decipher_info(data: T data.video_details.durationInSec === 0 ) { return data; - } else if (data.format[0].signatureCipher || data.format[0].cipher) { + } else if (data.format.length > 0 && (data.format[0].signatureCipher || data.format[0].cipher)) { data.format = await format_decipher(data.format, data.html5player); return data; } else { @@ -495,6 +517,9 @@ export function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] { duration_raw: info.lengthText?.simpleText ?? '0:00', thumbnails: info.thumbnail.thumbnails, title: info.title.runs[0].text, + upcoming: info.upcomingEventData?.startTime + ? new Date(parseInt(info.upcomingEventData.startTime) * 1000) + : undefined, channel: { id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined, name: info.shortBylineText.runs[0].text || undefined, @@ -723,6 +748,8 @@ function getWatchPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] { duration_raw: info.lengthText?.simpleText ?? '0:00', thumbnails: info.thumbnail.thumbnails, title: info.title.simpleText, + upcoming: + info.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.style === 'UPCOMING' || undefined, channel: { id: channel_info.navigationEndpoint.browseEndpoint.browseId || undefined, name: channel_info.text || undefined, diff --git a/play-dl/YouTube/utils/parser.ts b/play-dl/YouTube/utils/parser.ts index 647a52e..7135a7c 100644 --- a/play-dl/YouTube/utils/parser.ts +++ b/play-dl/YouTube/utils/parser.ts @@ -146,6 +146,9 @@ export function parseVideo(data?: any): YouTubeVideo { artist: Boolean(badge?.includes('artist')) }, uploadedAt: data.videoRenderer.publishedTimeText?.simpleText ?? null, + upcoming: data.videoRenderer.upcomingEventData?.startTime + ? new Date(parseInt(data.videoRenderer.upcomingEventData.startTime) * 1000) + : undefined, views: data.videoRenderer.viewCountText?.simpleText?.replace(/\D/g, '') ?? 0, live: durationText ? false : true });