diff --git a/play-dl/YouTube/utils/cipher.ts b/play-dl/YouTube/utils/cipher.ts index d7d9434..62d1c99 100644 --- a/play-dl/YouTube/utils/cipher.ts +++ b/play-dl/YouTube/utils/cipher.ts @@ -101,7 +101,7 @@ function deciper_signature(tokens: string[], signature: string) { switch (token.slice(0, 2)) { case 'sw': pos = parseInt(token.slice(2)); - sig = swappositions(sig, pos); + swappositions(sig, pos); break; case 'rv': sig.reverse(); @@ -122,13 +122,11 @@ function deciper_signature(tokens: string[], signature: string) { * Function to swap positions in a array * @param array array * @param position position to switch with first element - * @returns new array with swapped positions. */ function swappositions(array: string[], position: number) { const first = array[0]; array[0] = array[position]; array[position] = first; - return array; } /** * Sets Download url with some extra parameter diff --git a/play-dl/YouTube/utils/constants.ts b/play-dl/YouTube/utils/constants.ts index 23c746a..a8b660e 100644 --- a/play-dl/YouTube/utils/constants.ts +++ b/play-dl/YouTube/utils/constants.ts @@ -37,3 +37,10 @@ export interface InfoData { video_details: YouTubeVideo; related_videos: string[]; } + +export interface StreamInfoData { + LiveStreamData: LiveStreamData; + html5player: string; + format: Partial[]; + video_details: Pick; +} diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index 20b0421..2d11522 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -2,7 +2,7 @@ import { ProxyOptions as Proxy, request } from './../../Request/index'; import { format_decipher } from './cipher'; import { YouTubeVideo } from '../classes/Video'; import { YouTubePlayList } from '../classes/Playlist'; -import { InfoData } from './constants'; +import { InfoData, StreamInfoData } from './constants'; interface InfoOptions { proxy?: Proxy[]; @@ -219,6 +219,69 @@ export async function video_basic_info(url: string, options: InfoOptions = {}): related_videos: related }; } +/** + * Gets the data required for streaming from YouTube url, ID or html body data and deciphers it. + * + * Internal function used by {@link stream} instead of {@link video_info} + * because it only extracts the information required for streaming. + * + * @param url YouTube url or ID or html body data + * @param options Video Info Options + * - `Proxy[]` proxy : sends data through a proxy + * - `boolean` htmldata : given data is html data or not + * @returns Deciphered Video Info {@link StreamInfoData}. + */ +export async function video_stream_info(url: string, options: InfoOptions = {}): Promise { + let body: string; + if (options.htmldata) { + body = url; + } else { + if (yt_validate(url) !== 'video') throw new Error('This is not a YouTube Watch URL'); + const video_id: string = extractID(url); + const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`; + body = await request(new_url, { + proxies: options.proxy ?? [], + headers: { 'accept-language': 'en-US,en;q=0.9' }, + cookies: true + }); + } + if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1) + throw new Error('Captcha page: YouTube has detected that you are a bot!'); + const player_data = body + .split('var ytInitialPlayerResponse = ')?.[1] + ?.split(';')[0] + .split(/;\s*(var|const|let)/)[0]; + if (!player_data) throw new Error('Initial Player Response Data is undefined.'); + const player_response = JSON.parse(player_data); + if (player_response.playabilityStatus.status !== 'OK') + throw new Error( + `While getting info from url\n${ + player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ?? + player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText + }` + ); + const html5player = `https://www.youtube.com${body.split('"jsUrl":"')[1].split('"')[0]}`; + const duration = Number(player_response.videoDetails.lengthSeconds); + const video_details = { + url: `https://www.youtube.com/watch?v=${player_response.videoDetails.videoId}`, + durationInSec: (duration < 0 ? 0 : duration) || 0 + }; + const format = []; + format.push(...(player_response.streamingData.formats ?? [])); + format.push(...(player_response.streamingData.adaptiveFormats ?? [])); + + const LiveStreamData = { + isLive: player_response.videoDetails.isLiveContent, + dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null, + hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null + }; + return await decipher_info({ + LiveStreamData, + html5player, + format, + video_details + }); +} /** * Function to convert seconds to [hour : minutes : seconds] format * @param seconds seconds to convert @@ -276,7 +339,7 @@ export async function video_info(url: string, options: InfoOptions = {}): Promis * @param data Data - {@link InfoData} * @returns Deciphered Video Info {@link InfoData} */ -export async function decipher_info(data: InfoData) { +export async function decipher_info(data: T): Promise { if (data.LiveStreamData.isLive === true && data.LiveStreamData.dashManifestUrl !== null) { return data; } else if (data.format[0].signatureCipher || data.format[0].cipher) {