Files
play-dl-test/play-dl/YouTube/utils/extractor.ts

731 lines
30 KiB
TypeScript
Raw Normal View History

2021-12-07 10:43:23 +05:30
import { request } from './../../Request/index';
2021-09-20 09:40:15 +05:30
import { format_decipher } from './cipher';
2021-09-27 22:20:50 +05:30
import { YouTubeVideo } from '../classes/Video';
import { YouTubePlayList } from '../classes/Playlist';
2021-12-06 17:28:37 +01:00
import { InfoData, StreamInfoData } from './constants';
import { URL, URLSearchParams } from 'node:url';
2021-09-17 14:36:32 +05:30
interface InfoOptions {
2021-11-01 15:32:51 +05:30
htmldata?: boolean;
2021-12-26 15:34:31 +05:30
language?: string;
2021-09-28 20:45:45 +05:30
}
interface PlaylistOptions {
incomplete?: boolean;
2021-12-26 15:34:31 +05:30
language?: string;
2021-09-28 20:45:45 +05:30
}
2021-10-02 13:26:50 +05:30
const video_id_pattern = /^[a-zA-Z\d_-]{11,12}$/;
2021-12-14 11:31:14 +05:30
const playlist_id_pattern = /^(PL|UU|LL|RD|OL)[a-zA-Z\d_-]{10,}$/;
2021-09-17 14:36:32 +05:30
const DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
const video_pattern =
2021-12-24 12:41:28 +05:30
/^((?:https?:)?\/\/)?(?:(?:www|m|music)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|shorts\/|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
2021-12-09 15:03:47 +05:30
const playlist_pattern =
2021-12-27 12:09:08 +05:30
/^((?:https?:)?\/\/)?(?:(?:www|m|music)\.)?(youtube\.com)\/(?:(playlist|watch))(.*)?((\?|\&)list=)(PL|UU|LL|RD|OL)[a-zA-Z\d_-]{10,}(.*)?$/;
2021-09-29 20:23:16 +05:30
/**
2021-11-22 13:13:00 +05:30
* Validate YouTube URL or ID.
2021-11-23 09:56:08 +05:30
*
* **CAUTION :** If your search word is 11 or 12 characters long, you might get it validated as video ID.
2021-11-23 09:56:08 +05:30
*
2021-11-22 13:13:00 +05:30
* To avoid above, add one more condition to yt_validate
* ```ts
* if (url.startsWith('https') && yt_validate(url) === 'video') {
* // YouTube Video Url.
* }
* ```
* @param url YouTube URL OR ID
2021-11-23 09:56:08 +05:30
* @returns
2021-11-22 13:13:00 +05:30
* ```
* 'playlist' | 'video' | 'search' | false
* ```
2021-09-29 20:23:16 +05:30
*/
2021-10-09 16:18:05 +05:30
export function yt_validate(url: string): 'playlist' | 'video' | 'search' | false {
2021-09-17 14:36:32 +05:30
if (url.indexOf('list=') === -1) {
2021-10-02 13:26:50 +05:30
if (url.startsWith('https')) {
2021-10-09 16:18:05 +05:30
if (url.match(video_pattern)) {
let id: string;
if (url.includes('youtu.be/')) id = url.split('youtu.be/')[1].split(/(\?|\/|&)/)[0];
2021-10-12 14:09:14 +05:30
else if (url.includes('youtube.com/embed/'))
id = url.split('youtube.com/embed/')[1].split(/(\?|\/|&)/)[0];
2021-10-26 14:57:19 +05:30
else if (url.includes('youtube.com/shorts/'))
id = url.split('youtube.com/shorts/')[1].split(/(\?|\/|&)/)[0];
else id = url.split('watch?v=')[1].split(/(\?|\/|&)/)[0];
2021-10-09 18:59:16 +05:30
if (id.match(video_id_pattern)) return 'video';
else return false;
} else return false;
2021-10-02 13:26:50 +05:30
} else {
if (url.match(video_id_pattern)) return 'video';
else if (url.match(playlist_id_pattern)) return 'playlist';
2021-10-09 16:18:05 +05:30
else return 'search';
2021-10-02 13:26:50 +05:30
}
2021-12-09 15:03:47 +05:30
} else {
if (!url.match(playlist_pattern)) return false;
else return 'playlist';
}
2021-08-31 10:16:14 +05:30
}
/**
* Extracts the video ID from a YouTube URL.
*
* Will return the value of `urlOrId` if it looks like a video ID.
* @param urlOrId A YouTube URL or video ID
* @returns the video ID or `false` if it can't find a video ID.
*/
function extractVideoId(urlOrId: string): string | false {
if (urlOrId.startsWith('https://') && urlOrId.match(video_pattern)) {
let id: string;
if (urlOrId.includes('youtu.be/')) {
id = urlOrId.split('youtu.be/')[1].split(/(\?|\/|&)/)[0];
} else if (urlOrId.includes('youtube.com/embed/')) {
id = urlOrId.split('youtube.com/embed/')[1].split(/(\?|\/|&)/)[0];
} else if (urlOrId.includes('youtube.com/shorts/')) {
id = urlOrId.split('youtube.com/shorts/')[1].split(/(\?|\/|&)/)[0];
} else {
id = (urlOrId.split('watch?v=')[1] ?? urlOrId.split('&v=')[1]).split(/(\?|\/|&)/)[0];
}
if (id.match(video_id_pattern)) return id;
} else if (urlOrId.match(video_id_pattern)) {
return urlOrId;
}
return false;
}
2021-09-29 20:23:16 +05:30
/**
2021-11-22 13:13:00 +05:30
* Extract ID of YouTube url.
2021-09-29 20:23:16 +05:30
* @param url ID or url of YouTube
* @returns ID of video or playlist.
*/
2021-09-17 14:36:32 +05:30
export function extractID(url: string): string {
2021-10-09 16:18:05 +05:30
const check = yt_validate(url);
if (!check || check === 'search') throw new Error('This is not a YouTube url or videoId or PlaylistID');
2021-09-17 14:36:32 +05:30
if (url.startsWith('https')) {
if (url.indexOf('list=') === -1) {
const video_id = extractVideoId(url);
if (!video_id) throw new Error('This is not a YouTube url or videoId or PlaylistID');
2021-09-17 14:36:32 +05:30
return video_id;
} else {
return url.split('list=')[1].split('&')[0];
2021-09-06 10:33:37 +05:30
}
2021-09-17 14:36:32 +05:30
} else return url;
2021-09-06 10:33:37 +05:30
}
2021-09-29 20:23:16 +05:30
/**
* Basic function to get data from a YouTube url or ID.
2021-12-08 09:47:26 +05:30
*
2021-11-22 13:13:00 +05:30
* Example
* ```ts
* const video = await play.video_basic_info('youtube video url')
2021-12-08 09:47:26 +05:30
*
2021-11-22 13:13:00 +05:30
* const res = ... // Any https package get function.
2021-12-08 09:47:26 +05:30
*
2021-12-07 10:50:42 +05:30
* const video = await play.video_basic_info(res.body, { htmldata : true })
2021-11-22 13:13:00 +05:30
* ```
* @param url YouTube url or ID or html body data
* @param options Video Info Options
* - `boolean` htmldata : given data is html data or not
* @returns Video Basic Info {@link InfoData}.
2021-09-29 20:23:16 +05:30
*/
2021-11-23 09:56:08 +05:30
export async function video_basic_info(url: string, options: InfoOptions = {}): Promise<InfoData> {
if (typeof url !== 'string') throw new Error('url parameter is not a URL string or a string of HTML');
2021-11-01 15:32:51 +05:30
let body: string;
const cookieJar = {};
2021-11-01 15:32:51 +05:30
if (options.htmldata) {
body = url;
2021-11-01 15:32:51 +05:30
} else {
const video_id = extractVideoId(url);
if (!video_id) throw new Error('This is not a YouTube Watch URL');
const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`;
body = await request(new_url, {
2021-12-07 10:43:23 +05:30
headers: {
2021-12-26 15:34:31 +05:30
'accept-language': options.language || 'en-US;q=0.9'
2021-12-07 10:43:23 +05:30
},
cookies: true,
cookieJar
});
}
2021-11-29 09:43:25 +05:30
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!');
2021-10-26 14:31:30 +05:30
const player_data = body
.split('var ytInitialPlayerResponse = ')?.[1]
?.split(';</script>')[0]
2021-12-26 21:17:39 +05:30
.split(/;\s*(var|const|let)\s/)[0];
2021-10-26 14:31:30 +05:30
if (!player_data) throw new Error('Initial Player Response Data is undefined.');
2021-10-26 14:40:50 +05:30
const initial_data = body
.split('var ytInitialData = ')?.[1]
?.split(';</script>')[0]
2021-12-26 21:17:39 +05:30
.split(/;\s*(var|const|let)\s/)[0];
2021-10-26 14:40:50 +05:30
if (!initial_data) throw new Error('Initial Response Data is undefined.');
2021-10-26 14:31:30 +05:30
const player_response = JSON.parse(player_data);
2021-10-26 14:40:50 +05:30
const initial_response = JSON.parse(initial_data);
const vid = player_response.videoDetails;
let discretionAdvised = false;
if (player_response.playabilityStatus.status !== 'OK') {
if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') {
if (options.htmldata)
throw new Error(
`Accepting the viewer discretion is not supported when using htmldata, video: ${vid.videoId}`
);
discretionAdvised = true;
const cookies =
initial_response.topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton
.buttonRenderer.command.saveConsentAction;
if (cookies) {
Object.assign(cookieJar, {
VISITOR_INFO1_LIVE: cookies.visitorCookie,
CONSENT: cookies.consentCookie
});
}
const updatedValues = await acceptViewerDiscretion(vid.videoId, cookieJar, body, true);
player_response.streamingData = updatedValues.streamingData;
initial_response.contents.twoColumnWatchNextResults.secondaryResults = updatedValues.relatedVideos;
} else
throw new Error(
`While getting info from url\n${
player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ??
player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText
}`
);
}
2021-11-23 09:56:08 +05:30
const ownerInfo =
initial_response.contents.twoColumnWatchNextResults.results?.results?.contents[1]?.videoSecondaryInfoRenderer
?.owner?.videoOwnerRenderer;
2021-12-15 23:00:35 +01:00
const badge = ownerInfo?.badges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase();
2021-09-17 14:36:32 +05:30
const html5player = `https://www.youtube.com${body.split('"jsUrl":"')[1].split('"')[0]}`;
2021-09-27 22:20:50 +05:30
const related: string[] = [];
2021-09-24 12:49:39 +05:30
initial_response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach(
(res: any) => {
if (res.compactVideoRenderer)
related.push(`https://www.youtube.com/watch?v=${res.compactVideoRenderer.videoId}`);
}
);
2021-09-17 14:36:32 +05:30
const microformat = player_response.microformat.playerMicroformatRenderer;
2021-09-27 22:20:50 +05:30
const video_details = new YouTubeVideo({
2021-09-17 14:36:32 +05:30
id: vid.videoId,
title: vid.title,
description: vid.shortDescription,
2021-10-05 18:47:09 +05:30
duration: Number(vid.lengthSeconds),
2021-12-15 23:00:35 +01:00
duration_raw: parseSeconds(vid.lengthSeconds),
2021-10-05 11:09:41 +05:30
uploadedAt: microformat.publishDate,
thumbnails: vid.thumbnail.thumbnails,
2021-09-17 14:36:32 +05:30
channel: {
name: vid.author,
id: vid.channelId,
url: `https://www.youtube.com/channel/${vid.channelId}`,
2021-12-15 23:00:35 +01:00
verified: Boolean(badge?.includes('verified')),
artist: Boolean(badge?.includes('artist')),
2021-11-23 09:56:08 +05:30
icons: ownerInfo?.thumbnail?.thumbnails || undefined
2021-09-17 14:36:32 +05:30
},
views: vid.viewCount,
tags: vid.keywords,
2021-11-27 14:32:43 +01:00
likes: parseInt(
2021-12-15 23:00:35 +01:00
initial_response.contents.twoColumnWatchNextResults.results.results.contents
.find((content: any) => content.videoPrimaryInfoRenderer)
?.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons?.find(
(button: any) => button.toggleButtonRenderer.defaultIcon.iconType === 'LIKE'
)
2021-11-27 14:32:43 +01:00
?.toggleButtonRenderer.defaultText.accessibility?.accessibilityData.label.replace(/\D+/g, '') ?? 0
),
2021-09-17 14:36:32 +05:30
live: vid.isLiveContent,
private: vid.isPrivate,
discretionAdvised
2021-09-27 22:20:50 +05:30
});
2021-12-15 23:00:35 +01:00
const format = player_response.streamingData.formats ?? [];
2021-09-20 17:20:15 +05:30
format.push(...(player_response.streamingData.adaptiveFormats ?? []));
2021-09-17 14:36:32 +05:30
const LiveStreamData = {
isLive: video_details.live,
dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null,
hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null
};
return {
LiveStreamData,
html5player,
format,
2021-09-22 15:33:56 +05:30
video_details,
2021-09-24 12:49:39 +05:30
related_videos: related
2021-09-17 14:36:32 +05:30
};
2021-08-07 15:53:18 +05:30
}
2021-12-06 17:28:37 +01:00
/**
* 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
* - `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<StreamInfoData> {
if (typeof url !== 'string') throw new Error('url parameter is not a URL string or a string of HTML');
2021-12-06 17:28:37 +01:00
let body: string;
const cookieJar = {};
2021-12-06 17:28:37 +01:00
if (options.htmldata) {
body = url;
} else {
const video_id = extractVideoId(url);
if (!video_id) throw new Error('This is not a YouTube Watch URL');
2021-12-06 17:28:37 +01:00
const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`;
body = await request(new_url, {
headers: { 'accept-language': 'en-US,en;q=0.9' },
cookies: true,
cookieJar
2021-12-06 17:28:37 +01:00
});
}
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(';</script>')[0]
2021-12-26 21:17:39 +05:30
.split(/;\s*(var|const|let)\s/)[0];
2021-12-06 17:28:37 +01:00
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') {
if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') {
if (options.htmldata)
throw new Error(
`Accepting the viewer discretion is not supported when using htmldata, video: ${player_response.videoDetails.videoId}`
);
const initial_data = body
.split('var ytInitialData = ')?.[1]
?.split(';</script>')[0]
.split(/;\s*(var|const|let)\s/)[0];
if (!initial_data) throw new Error('Initial Response Data is undefined.');
const cookies =
JSON.parse(initial_data).topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton
.buttonRenderer.command.saveConsentAction;
if (cookies) {
Object.assign(cookieJar, {
VISITOR_INFO1_LIVE: cookies.visitorCookie,
CONSENT: cookies.consentCookie
});
}
const updatedValues = await acceptViewerDiscretion(
player_response.videoDetails.videoId,
cookieJar,
body,
false
);
player_response.streamingData = updatedValues.streamingData;
} else
throw new Error(
`While getting info from url\n${
player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ??
player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText
}`
);
}
2021-12-06 17:28:37 +01:00
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
};
2021-12-15 23:00:35 +01:00
const format = player_response.streamingData.formats ?? [];
2021-12-06 17:28:37 +01:00
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
});
}
2021-09-29 20:23:16 +05:30
/**
* Function to convert seconds to [hour : minutes : seconds] format
* @param seconds seconds to convert
* @returns [hour : minutes : seconds] format
*/
2021-09-17 14:36:32 +05:30
function parseSeconds(seconds: number): string {
const d = Number(seconds);
const h = Math.floor(d / 3600);
const m = Math.floor((d % 3600) / 60);
const s = Math.floor((d % 3600) % 60);
2021-08-23 13:05:40 +05:30
2021-09-17 14:36:32 +05:30
const hDisplay = h > 0 ? (h < 10 ? `0${h}` : h) + ':' : '';
const mDisplay = m > 0 ? (m < 10 ? `0${m}` : m) + ':' : '00:';
const sDisplay = s > 0 ? (s < 10 ? `0${s}` : s) : '00';
return hDisplay + mDisplay + sDisplay;
2021-08-23 13:05:40 +05:30
}
2021-09-29 20:23:16 +05:30
/**
2021-11-22 13:13:00 +05:30
* Gets data from YouTube url or ID or html body data and deciphers it.
* ```
* video_basic_info + decipher_info = video_info
* ```
2021-12-08 09:47:26 +05:30
*
2021-11-22 13:13:00 +05:30
* Example
* ```ts
* const video = await play.video_info('youtube video url')
2021-12-08 09:47:26 +05:30
*
2021-11-22 13:13:00 +05:30
* const res = ... // Any https package get function.
2021-12-08 09:47:26 +05:30
*
2021-12-07 10:50:42 +05:30
* const video = await play.video_info(res.body, { htmldata : true })
2021-11-22 13:13:00 +05:30
* ```
* @param url YouTube url or ID or html body data
* @param options Video Info Options
* - `boolean` htmldata : given data is html data or not
* @returns Deciphered Video Info {@link InfoData}.
2021-09-29 20:23:16 +05:30
*/
2021-11-18 15:38:25 +05:30
export async function video_info(url: string, options: InfoOptions = {}): Promise<InfoData> {
2021-09-28 20:45:45 +05:30
const data = await video_basic_info(url, options);
return await decipher_info(data);
2021-08-07 15:53:18 +05:30
}
/**
* Function uses data from video_basic_info and deciphers it if it contains signatures.
2021-11-22 13:13:00 +05:30
* @param data Data - {@link InfoData}
* @returns Deciphered Video Info {@link InfoData}
*/
2021-12-06 17:28:37 +01:00
export async function decipher_info<T extends InfoData | StreamInfoData>(data: T): Promise<T> {
2021-12-17 15:24:12 +05:30
if (
data.LiveStreamData.isLive === true &&
data.LiveStreamData.dashManifestUrl !== null &&
data.video_details.durationInSec === 0
) {
return data;
} else if (data.format[0].signatureCipher || data.format[0].cipher) {
data.format = await format_decipher(data.format, data.html5player);
return data;
} else {
return data;
}
}
2021-09-29 20:23:16 +05:30
/**
2021-11-22 13:13:00 +05:30
* Gets YouTube playlist info from a playlist url.
2021-12-08 09:47:26 +05:30
*
2021-11-22 13:13:00 +05:30
* Example
* ```ts
* const playlist = await play.playlist_info('youtube playlist url')
2021-12-08 09:47:26 +05:30
*
2021-11-22 13:13:00 +05:30
* const playlist = await play.playlist_info('youtube playlist url', { incomplete : true })
* ```
2021-09-29 20:23:16 +05:30
* @param url Playlist URL
2021-11-22 13:13:00 +05:30
* @param options Playlist Info Options
* - `boolean` incomplete : If set to true, parses playlist with hidden videos.
2021-12-08 09:47:26 +05:30
*
2021-09-29 20:23:16 +05:30
* @returns YouTube Playlist
*/
export async function playlist_info(url: string, options: PlaylistOptions = {}): Promise<YouTubePlayList> {
2021-09-17 14:36:32 +05:30
if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`);
2021-12-14 15:01:10 +05:30
if (!url.startsWith('https')) url = `https://www.youtube.com/playlist?list=${url}`;
if (url.indexOf('list=') === -1) throw new Error('This is not a Playlist URL');
2021-12-09 14:33:49 +05:30
if (url.includes('music.youtube.com')) {
const urlObj = new URL(url);
urlObj.hostname = 'www.youtube.com';
url = urlObj.toString();
}
2021-12-09 14:33:49 +05:30
const body = await request(url, {
2021-12-07 10:43:23 +05:30
headers: {
2021-12-26 15:34:31 +05:30
'accept-language': options.language || 'en-US;q=0.9'
2021-12-02 12:14:34 +05:30
}
2021-09-17 14:36:32 +05:30
});
2021-12-01 20:50:02 +05:30
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!');
2021-12-26 15:34:31 +05:30
const response = JSON.parse(
body
.split('var ytInitialData = ')[1]
.split(';</script>')[0]
2021-12-26 21:17:39 +05:30
.split(/;\s*(var|const|let)\s/)[0]
2021-12-26 15:34:31 +05:30
);
2021-09-17 14:36:32 +05:30
if (response.alerts) {
if (response.alerts[0].alertWithButtonRenderer?.type === 'INFO') {
2021-09-28 20:45:45 +05:30
if (!options.incomplete)
2021-09-17 14:36:32 +05:30
throw new Error(
`While parsing playlist url\n${response.alerts[0].alertWithButtonRenderer.text.simpleText}`
);
} else if (response.alerts[0].alertRenderer?.type === 'ERROR')
throw new Error(`While parsing playlist url\n${response.alerts[0].alertRenderer.text.runs[0].text}`);
else throw new Error('While parsing playlist url\nUnknown Playlist Error');
2021-08-23 23:34:10 +05:30
}
2021-12-14 15:01:10 +05:30
if (url.indexOf('watch?v=') !== -1) {
2021-12-27 12:09:08 +05:30
return getWatchPlaylist(response, body, url);
2021-12-14 15:01:10 +05:30
} else return getNormalPlaylist(response, body);
2021-12-09 14:33:49 +05:30
}
/**
* Function to parse Playlist from YouTube search
* @param data html data of that request
* @param limit No. of videos to parse
* @returns Array of YouTubeVideo.
*/
export function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {
const videos = [];
2021-08-13 13:16:34 +05:30
2021-12-09 14:33:49 +05:30
for (let i = 0; i < data.length; i++) {
if (limit === videos.length) break;
const info = data[i].playlistVideoRenderer;
if (!info || !info.shortBylineText) continue;
2021-08-13 13:16:34 +05:30
2021-12-09 14:33:49 +05:30
videos.push(
new YouTubeVideo({
id: info.videoId,
duration: parseInt(info.lengthSeconds) || 0,
duration_raw: info.lengthText?.simpleText ?? '0:00',
2021-12-14 15:01:10 +05:30
thumbnails: info.thumbnail.thumbnails,
2021-12-09 14:33:49 +05:30
title: info.title.runs[0].text,
channel: {
id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined,
name: info.shortBylineText.runs[0].text || undefined,
url: `https://www.youtube.com${
info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl ||
info.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url
}`,
icon: undefined
}
})
);
}
return videos;
}
/**
* Function to get Continuation Token
* @param data html data of playlist url
* @returns token
*/
export function getContinuationToken(data: any): string {
return data.find((x: any) => Object.keys(x)[0] === 'continuationItemRenderer')?.continuationItemRenderer
.continuationEndpoint?.continuationCommand?.token;
}
async function acceptViewerDiscretion(
videoId: string,
cookieJar: { [key: string]: string },
body: string,
extractRelated: boolean
): Promise<{ streamingData: any; relatedVideos?: any }> {
const apiKey =
body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ??
body.split('innertubeApiKey":"')[1]?.split('"')[0] ??
DEFAULT_API_KEY;
const sessionToken =
body.split('"XSRF_TOKEN":"')[1]?.split('"')[0].replaceAll('\\u003d', '=') ??
body.split('"xsrf_token":"')[1]?.split('"')[0].replaceAll('\\u003d', '=');
if (!sessionToken)
throw new Error(`Unable to extract XSRF_TOKEN to accept the viewer discretion popup for video: ${videoId}.`);
const verificationResponse = await request(`https://www.youtube.com/youtubei/v1/verify_age?key=${apiKey}`, {
method: 'POST',
body: JSON.stringify({
context: {
client: {
utcOffsetMinutes: 0,
gl: 'US',
hl: 'en',
clientName: 'WEB',
clientVersion:
body.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0] ??
body.split('"innertube_context_client_version":"')[1]?.split('"')[0] ??
'<some version>'
},
user: {},
request: {}
},
nextEndpoint: {
urlEndpoint: {
url: `watch?v=${videoId}`
}
},
setControvercy: true
}),
cookieJar
});
const endpoint = JSON.parse(verificationResponse).actions[0].navigateAction.endpoint;
const videoPage = await request(`https://www.youtube.com/${endpoint.urlEndpoint.url}&pbj=1`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams([
['command', JSON.stringify(endpoint)],
['session_token', sessionToken]
]).toString(),
cookieJar
});
if (videoPage.includes('<h1>Something went wrong</h1>'))
throw new Error(`Unable to accept the viewer discretion popup for video: ${videoId}`);
const videoPageData = JSON.parse(videoPage);
if (videoPageData[2].playerResponse.playabilityStatus.status !== 'OK')
throw new Error(
`While getting info from url after trying to accept the discretion popup for video ${videoId}\n${
videoPageData[2].playerResponse.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason
.simpleText ??
videoPageData[2].playerResponse.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText
}`
);
const streamingData = videoPageData[2].playerResponse.streamingData;
if (extractRelated)
return {
streamingData,
relatedVideos: videoPageData[3].response.contents.twoColumnWatchNextResults.secondaryResults
};
return { streamingData };
}
2021-12-28 08:38:39 +05:30
function getWatchPlaylist(response: any, body: any, url: string): YouTubePlayList {
2021-12-14 15:01:10 +05:30
const playlist_details = response.contents.twoColumnWatchNextResults.playlist.playlist;
2021-12-09 14:33:49 +05:30
2021-12-14 15:01:10 +05:30
const videos = getWatchPlaylistVideos(playlist_details.contents);
2021-09-17 14:36:32 +05:30
const API_KEY =
body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ??
body.split('innertubeApiKey":"')[1]?.split('"')[0] ??
DEFAULT_API_KEY;
2021-12-14 15:01:10 +05:30
const videoCount = playlist_details.totalVideos;
const channel = playlist_details.shortBylineText?.runs?.[0];
const badge = playlist_details.badges?.[0]?.metadataBadgeRenderer?.style.toLowerCase();
2021-08-13 13:16:34 +05:30
2021-12-09 14:33:49 +05:30
return new YouTubePlayList({
continuation: {
api: API_KEY,
token: getContinuationToken(playlist_details.contents),
clientVersion:
body.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0] ??
body.split('"innertube_context_client_version":"')[1]?.split('"')[0] ??
'<some version>'
},
2021-12-14 15:01:10 +05:30
id: playlist_details.playlistId || '',
title: playlist_details.title || '',
videoCount: parseInt(videoCount) || 0,
videos: videos,
2021-12-27 12:09:08 +05:30
url: url,
2021-12-14 15:01:10 +05:30
channel: {
2021-12-09 14:33:49 +05:30
id: channel?.navigationEndpoint?.browseEndpoint?.browseId || null,
name: channel?.text || null,
url: `https://www.youtube.com${
channel?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl ||
channel?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url
}`,
verified: Boolean(badge?.includes('verified')),
artist: Boolean(badge?.includes('artist'))
}
2021-12-14 15:01:10 +05:30
});
2021-12-09 14:33:49 +05:30
}
2021-12-14 15:01:10 +05:30
function getNormalPlaylist(response: any, body: any): YouTubePlayList {
const json_data =
response.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0]
.itemSectionRenderer.contents[0].playlistVideoListRenderer.contents;
2021-12-09 14:33:49 +05:30
const playlist_details = response.sidebar.playlistSidebarRenderer.items;
const API_KEY =
body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ??
body.split('innertubeApiKey":"')[1]?.split('"')[0] ??
DEFAULT_API_KEY;
const videos = getPlaylistVideos(json_data, 100);
const data = playlist_details[0].playlistSidebarPrimaryInfoRenderer;
2021-09-29 20:23:16 +05:30
if (!data.title.runs || !data.title.runs.length) throw new Error('Failed to Parse Playlist info.');
2021-08-13 13:16:34 +05:30
2021-12-09 14:33:49 +05:30
const author = playlist_details[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner;
2021-12-26 16:55:10 +01:00
const views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/\D/g, '') : 0;
2021-09-17 14:36:32 +05:30
const lastUpdate =
data.stats
.find((x: any) => 'runs' in x && x['runs'].find((y: any) => y.text.toLowerCase().includes('last update')))
?.runs.pop()?.text ?? null;
2021-12-26 16:55:10 +01:00
const videosCount = data.stats[0].runs[0].text.replace(/\D/g, '') || 0;
2021-08-13 13:16:34 +05:30
2021-09-27 22:20:50 +05:30
const res = new YouTubePlayList({
2021-08-13 13:16:34 +05:30
continuation: {
api: API_KEY,
2021-12-09 14:33:49 +05:30
token: getContinuationToken(json_data),
2021-09-17 14:36:32 +05:30
clientVersion:
body.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0] ??
body.split('"innertube_context_client_version":"')[1]?.split('"')[0] ??
'<some version>'
2021-08-13 13:16:34 +05:30
},
id: data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId,
title: data.title.runs[0].text,
videoCount: parseInt(videosCount) || 0,
lastUpdate: lastUpdate,
views: parseInt(views) || 0,
videos: videos,
url: `https://www.youtube.com/playlist?list=${data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId}`,
link: `https://www.youtube.com${data.title.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,
2021-12-09 14:33:49 +05:30
channel: author
2021-08-13 13:16:34 +05:30
? {
name: author.videoOwnerRenderer.title.runs[0].text,
id: author.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId,
2021-09-17 14:36:32 +05:30
url: `https://www.youtube.com${
author.videoOwnerRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url ||
author.videoOwnerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl
}`,
icon: author.videoOwnerRenderer.thumbnail.thumbnails.length
? author.videoOwnerRenderer.thumbnail.thumbnails[
author.videoOwnerRenderer.thumbnail.thumbnails.length - 1
].url
: null
2021-08-13 13:16:34 +05:30
}
: {},
2021-09-17 14:36:32 +05:30
thumbnail: data.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length
? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[
data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length - 1
2021-09-27 22:20:50 +05:30
]
2021-09-17 14:36:32 +05:30
: null
2021-08-13 13:16:34 +05:30
});
return res;
}
2021-12-14 15:01:10 +05:30
function getWatchPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {
const videos: YouTubeVideo[] = [];
2021-12-09 14:33:49 +05:30
2021-12-14 15:01:10 +05:30
for (let i = 0; i < data.length; i++) {
if (limit === videos.length) break;
2021-12-09 14:33:49 +05:30
const info = data[i].playlistPanelVideoRenderer;
2021-12-14 15:01:10 +05:30
if (!info || !info.shortBylineText) continue;
const channel_info = info.shortBylineText.runs[0];
2021-08-13 13:16:34 +05:30
videos.push(
2021-09-27 22:20:50 +05:30
new YouTubeVideo({
2021-08-13 13:16:34 +05:30
id: info.videoId,
2021-12-09 14:33:49 +05:30
duration: parseDuration(info.lengthText?.simpleText) || 0,
2021-09-17 14:36:32 +05:30
duration_raw: info.lengthText?.simpleText ?? '0:00',
2021-12-14 15:01:10 +05:30
thumbnails: info.thumbnail.thumbnails,
2021-12-09 14:33:49 +05:30
title: info.title.simpleText,
2021-08-13 13:16:34 +05:30
channel: {
2021-12-09 14:33:49 +05:30
id: channel_info.navigationEndpoint.browseEndpoint.browseId || undefined,
name: channel_info.text || undefined,
2021-09-17 14:36:32 +05:30
url: `https://www.youtube.com${
2021-12-09 14:33:49 +05:30
channel_info.navigationEndpoint.browseEndpoint.canonicalBaseUrl ||
channel_info.navigationEndpoint.commandMetadata.webCommandMetadata.url
2021-09-17 14:36:32 +05:30
}`,
2021-08-13 13:16:34 +05:30
icon: undefined
}
})
);
}
2021-12-09 14:33:49 +05:30
2021-12-14 15:01:10 +05:30
return videos;
2021-09-07 22:05:01 +09:00
}
2021-12-09 14:33:49 +05:30
2021-12-14 15:01:10 +05:30
function parseDuration(text: string): number {
if (!text) return 0;
const split = text.split(':');
2021-12-09 14:33:49 +05:30
2021-12-14 15:01:10 +05:30
switch (split.length) {
2021-12-09 14:33:49 +05:30
case 2:
2021-12-14 15:01:10 +05:30
return parseInt(split[0]) * 60 + parseInt(split[1]);
2021-12-09 14:33:49 +05:30
case 3:
2021-12-14 15:01:10 +05:30
return parseInt(split[0]) * 60 * 60 + parseInt(split[1]) * 60 + parseInt(split[2]);
2021-12-09 14:33:49 +05:30
2021-12-14 15:01:10 +05:30
default:
return 0;
2021-12-09 14:33:49 +05:30
}
2021-12-14 15:01:10 +05:30
}