commit
f93ae02cf6
@ -93,8 +93,8 @@ export class YouTubePlayList {
|
|||||||
this.lastUpdate = data.lastUpdate || undefined;
|
this.lastUpdate = data.lastUpdate || undefined;
|
||||||
this.views = data.views || 0;
|
this.views = data.views || 0;
|
||||||
this.link = data.link || undefined;
|
this.link = data.link || undefined;
|
||||||
this.channel = data.author || undefined;
|
this.channel = new YouTubeChannel(data.channel) || undefined;
|
||||||
this.thumbnail = new YouTubeThumbnail(data.thumbnail) || undefined;
|
this.thumbnail = (data.thumbnail) ? new YouTubeThumbnail(data.thumbnail) : undefined;
|
||||||
this.videos = data.videos || [];
|
this.videos = data.videos || [];
|
||||||
this.__count++;
|
this.__count++;
|
||||||
this.fetched_videos.set(`${this.__count}`, this.videos as YouTubeVideo[]);
|
this.fetched_videos.set(`${this.__count}`, this.videos as YouTubeVideo[]);
|
||||||
@ -281,7 +281,6 @@ interface PlaylistJSON {
|
|||||||
* YouTube Playlist thumbnail Data
|
* YouTube Playlist thumbnail Data
|
||||||
*/
|
*/
|
||||||
thumbnail?: {
|
thumbnail?: {
|
||||||
id: string | undefined;
|
|
||||||
width: number | undefined;
|
width: number | undefined;
|
||||||
height: number | undefined;
|
height: number | undefined;
|
||||||
url: string | undefined;
|
url: string | undefined;
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
export class YouTubeThumbnail {
|
export class YouTubeThumbnail {
|
||||||
id: string;
|
|
||||||
url: string;
|
url: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
|
||||||
constructor(data: any) {
|
constructor(data: any) {
|
||||||
this.id = data.id;
|
|
||||||
this.url = data.url;
|
this.url = data.url;
|
||||||
this.width = data.width;
|
this.width = data.width;
|
||||||
this.height = data.height;
|
this.height = data.height;
|
||||||
@ -13,7 +11,6 @@ export class YouTubeThumbnail {
|
|||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
|
||||||
url: this.url,
|
url: this.url,
|
||||||
width: this.width,
|
width: this.width,
|
||||||
height: this.height
|
height: this.height
|
||||||
|
|||||||
@ -38,7 +38,6 @@ interface VideoOptions {
|
|||||||
* YouTube Thumbnail Data
|
* YouTube Thumbnail Data
|
||||||
*/
|
*/
|
||||||
thumbnail?: {
|
thumbnail?: {
|
||||||
id: string | undefined;
|
|
||||||
width: number | undefined;
|
width: number | undefined;
|
||||||
height: number | undefined;
|
height: number | undefined;
|
||||||
url: string | undefined;
|
url: string | undefined;
|
||||||
|
|||||||
@ -17,8 +17,6 @@ const playlist_id_pattern = /^(PL|UU|LL|RD|OL)[a-zA-Z\d_-]{16,41}$/;
|
|||||||
const DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
const DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
||||||
const video_pattern =
|
const video_pattern =
|
||||||
/^((?:https?:)?\/\/)?(?:(?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|shorts\/|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
|
/^((?:https?:)?\/\/)?(?:(?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|shorts\/|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
|
||||||
const playlist_pattern =
|
|
||||||
/^((?:https?:)?\/\/)?(?:(?:www|m)\.)?(youtube\.com)\/(?:(playlist|watch))(.*)?((\?|\&)list=)(PL|UU|LL|RD|OL)[a-zA-Z\d_-]{16,41}(.*)?$/;
|
|
||||||
/**
|
/**
|
||||||
* Validate YouTube URL or ID.
|
* Validate YouTube URL or ID.
|
||||||
*
|
*
|
||||||
@ -55,10 +53,7 @@ export function yt_validate(url: string): 'playlist' | 'video' | 'search' | fals
|
|||||||
else if (url.match(playlist_id_pattern)) return 'playlist';
|
else if (url.match(playlist_id_pattern)) return 'playlist';
|
||||||
else return 'search';
|
else return 'search';
|
||||||
}
|
}
|
||||||
} else {
|
} else return "playlist"
|
||||||
if (!url.match(playlist_pattern)) return false;
|
|
||||||
else return 'playlist';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Extract ID of YouTube url.
|
* Extract ID of YouTube url.
|
||||||
@ -328,14 +323,15 @@ export async function decipher_info<T extends InfoData | StreamInfoData>(data: T
|
|||||||
*/
|
*/
|
||||||
export async function playlist_info(url: string, options: PlaylistOptions = {}): Promise<YouTubePlayList> {
|
export async function playlist_info(url: string, options: PlaylistOptions = {}): Promise<YouTubePlayList> {
|
||||||
if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`);
|
if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`);
|
||||||
let Playlist_id: string;
|
if (!url.startsWith('https')) url = `https://www.youtube.com/playlist?list=${url}`
|
||||||
if (url.startsWith('https')) {
|
if (url.indexOf('list=') === -1 ) throw new Error('This is not a Playlist URL');
|
||||||
if (yt_validate(url) !== 'playlist') throw new Error('This is not a Playlist URL');
|
|
||||||
Playlist_id = extractID(url);
|
|
||||||
} else Playlist_id = url;
|
|
||||||
const new_url = `https://www.youtube.com/playlist?list=${Playlist_id}`;
|
|
||||||
|
|
||||||
const body = await request(new_url, {
|
if(yt_validate(url) === 'playlist') {
|
||||||
|
const id = extractID(url)
|
||||||
|
url = `https://www.youtube.com/playlist?list=${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7'
|
'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7'
|
||||||
}
|
}
|
||||||
@ -353,67 +349,10 @@ export async function playlist_info(url: string, options: PlaylistOptions = {}):
|
|||||||
throw new Error(`While parsing playlist url\n${response.alerts[0].alertRenderer.text.runs[0].text}`);
|
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');
|
else throw new Error('While parsing playlist url\nUnknown Playlist Error');
|
||||||
}
|
}
|
||||||
|
if(url.indexOf('watch?v=') !== -1){
|
||||||
const rawJSON = `${body.split('{"playlistVideoListRenderer":{"contents":')[1].split('}],"playlistId"')[0]}}]`;
|
return getWatchPlaylist(response, body)
|
||||||
const parsed = JSON.parse(rawJSON);
|
|
||||||
const playlistDetails = JSON.parse(body.split('{"playlistSidebarRenderer":')[1].split('}};</script>')[0]).items;
|
|
||||||
|
|
||||||
const API_KEY =
|
|
||||||
body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ??
|
|
||||||
body.split('innertubeApiKey":"')[1]?.split('"')[0] ??
|
|
||||||
DEFAULT_API_KEY;
|
|
||||||
const videos = getPlaylistVideos(parsed, 100);
|
|
||||||
|
|
||||||
const data = playlistDetails[0].playlistSidebarPrimaryInfoRenderer;
|
|
||||||
if (!data.title.runs || !data.title.runs.length) throw new Error('Failed to Parse Playlist info.');
|
|
||||||
|
|
||||||
const author = playlistDetails[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner;
|
|
||||||
const views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/[^0-9]/g, '') : 0;
|
|
||||||
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;
|
|
||||||
const videosCount = data.stats[0].runs[0].text.replace(/[^0-9]/g, '') || 0;
|
|
||||||
|
|
||||||
const res = new YouTubePlayList({
|
|
||||||
continuation: {
|
|
||||||
api: API_KEY,
|
|
||||||
token: getContinuationToken(parsed),
|
|
||||||
clientVersion:
|
|
||||||
body.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0] ??
|
|
||||||
body.split('"innertube_context_client_version":"')[1]?.split('"')[0] ??
|
|
||||||
'<some version>'
|
|
||||||
},
|
|
||||||
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}`,
|
|
||||||
author: author
|
|
||||||
? {
|
|
||||||
name: author.videoOwnerRenderer.title.runs[0].text,
|
|
||||||
id: author.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId,
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
: {},
|
else return getNormalPlaylist(response, body)
|
||||||
thumbnail: data.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length
|
|
||||||
? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[
|
|
||||||
data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length - 1
|
|
||||||
]
|
|
||||||
: null
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Function to parse Playlist from YouTube search
|
* Function to parse Playlist from YouTube search
|
||||||
@ -432,15 +371,9 @@ export function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {
|
|||||||
videos.push(
|
videos.push(
|
||||||
new YouTubeVideo({
|
new YouTubeVideo({
|
||||||
id: info.videoId,
|
id: info.videoId,
|
||||||
index: parseInt(info.index?.simpleText) || 0,
|
|
||||||
duration: parseInt(info.lengthSeconds) || 0,
|
duration: parseInt(info.lengthSeconds) || 0,
|
||||||
duration_raw: info.lengthText?.simpleText ?? '0:00',
|
duration_raw: info.lengthText?.simpleText ?? '0:00',
|
||||||
thumbnail: {
|
thumbnails : info.thumbnail.thumbnails,
|
||||||
id: info.videoId,
|
|
||||||
url: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].url,
|
|
||||||
height: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].height,
|
|
||||||
width: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].width
|
|
||||||
},
|
|
||||||
title: info.title.runs[0].text,
|
title: info.title.runs[0].text,
|
||||||
channel: {
|
channel: {
|
||||||
id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined,
|
id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined,
|
||||||
@ -465,3 +398,155 @@ export function getContinuationToken(data: any): string {
|
|||||||
return data.find((x: any) => Object.keys(x)[0] === 'continuationItemRenderer')?.continuationItemRenderer
|
return data.find((x: any) => Object.keys(x)[0] === 'continuationItemRenderer')?.continuationItemRenderer
|
||||||
.continuationEndpoint?.continuationCommand?.token;
|
.continuationEndpoint?.continuationCommand?.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getWatchPlaylist(response : any, body : any) : YouTubePlayList{
|
||||||
|
const playlist_details = response.contents.twoColumnWatchNextResults.playlist.playlist
|
||||||
|
|
||||||
|
const videos = getWatchPlaylistVideos(playlist_details.contents)
|
||||||
|
const API_KEY =
|
||||||
|
body.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0] ??
|
||||||
|
body.split('innertubeApiKey":"')[1]?.split('"')[0] ??
|
||||||
|
DEFAULT_API_KEY;
|
||||||
|
|
||||||
|
const videoCount = playlist_details.totalVideos
|
||||||
|
const channel = playlist_details.shortBylineText?.runs?.[0]
|
||||||
|
const badge = playlist_details.badges?.[0]?.metadataBadgeRenderer?.style.toLowerCase()
|
||||||
|
|
||||||
|
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>'
|
||||||
|
},
|
||||||
|
id : playlist_details.playlistId || '',
|
||||||
|
title : playlist_details.title || '',
|
||||||
|
videoCount : parseInt(videoCount) || 0,
|
||||||
|
videos : videos,
|
||||||
|
url : `https://www.youtube.com/playlist?list=${playlist_details.playlistId}`,
|
||||||
|
channel : {
|
||||||
|
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'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
if (!data.title.runs || !data.title.runs.length) throw new Error('Failed to Parse Playlist info.');
|
||||||
|
|
||||||
|
const author = playlist_details[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner;
|
||||||
|
const views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/[^0-9]/g, '') : 0;
|
||||||
|
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;
|
||||||
|
const videosCount = data.stats[0].runs[0].text.replace(/[^0-9]/g, '') || 0;
|
||||||
|
|
||||||
|
const res = new YouTubePlayList({
|
||||||
|
continuation: {
|
||||||
|
api: API_KEY,
|
||||||
|
token: getContinuationToken(json_data),
|
||||||
|
clientVersion:
|
||||||
|
body.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0] ??
|
||||||
|
body.split('"innertube_context_client_version":"')[1]?.split('"')[0] ??
|
||||||
|
'<some version>'
|
||||||
|
},
|
||||||
|
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}`,
|
||||||
|
channel: author
|
||||||
|
? {
|
||||||
|
name: author.videoOwnerRenderer.title.runs[0].text,
|
||||||
|
id: author.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
thumbnail: data.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length
|
||||||
|
? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[
|
||||||
|
data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length - 1
|
||||||
|
]
|
||||||
|
: null
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWatchPlaylistVideos(data : any, limit = Infinity): YouTubeVideo[] {
|
||||||
|
const videos: YouTubeVideo[] = []
|
||||||
|
|
||||||
|
for(let i = 0; i < data.length ; i++) {
|
||||||
|
if(limit === videos.length) break;
|
||||||
|
const info = data[i].playlistPanelVideoRenderer;
|
||||||
|
if(!info || !info.shortBylineText) continue;
|
||||||
|
const channel_info = info.shortBylineText.runs[0]
|
||||||
|
|
||||||
|
videos.push(
|
||||||
|
new YouTubeVideo({
|
||||||
|
id: info.videoId,
|
||||||
|
duration: parseDuration(info.lengthText?.simpleText) || 0,
|
||||||
|
duration_raw: info.lengthText?.simpleText ?? '0:00',
|
||||||
|
thumbnails : info.thumbnail.thumbnails,
|
||||||
|
title: info.title.simpleText,
|
||||||
|
channel: {
|
||||||
|
id: channel_info.navigationEndpoint.browseEndpoint.browseId || undefined,
|
||||||
|
name: channel_info.text || undefined,
|
||||||
|
url: `https://www.youtube.com${
|
||||||
|
channel_info.navigationEndpoint.browseEndpoint.canonicalBaseUrl ||
|
||||||
|
channel_info.navigationEndpoint.commandMetadata.webCommandMetadata.url
|
||||||
|
}`,
|
||||||
|
icon: undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDuration(text : string): number{
|
||||||
|
if(!text) return 0
|
||||||
|
const split = text.split(':')
|
||||||
|
|
||||||
|
switch (split.length){
|
||||||
|
case 2:
|
||||||
|
return (parseInt(split[0]) * 60) + (parseInt(split[1]))
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return (parseInt(split[0]) * 60 * 60) + (parseInt(split[1]) * 60) + (parseInt(split[2]))
|
||||||
|
|
||||||
|
default :
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user