From 70d177450899466d4cbaae6988d6dd08541eb8d9 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Mon, 27 Sep 2021 22:20:50 +0530 Subject: [PATCH 01/11] Types Improved --- play-dl/SoundCloud/index.ts | 7 ++-- play-dl/Spotify/index.ts | 4 ++- play-dl/YouTube/classes/Channel.ts | 9 ++--- play-dl/YouTube/classes/Playlist.ts | 34 ++++++++++--------- play-dl/YouTube/classes/Thumbnail.ts | 49 ---------------------------- play-dl/YouTube/classes/Video.ts | 17 ++++------ play-dl/YouTube/index.ts | 3 +- play-dl/YouTube/search.ts | 13 ++++---- play-dl/YouTube/stream.ts | 8 +++-- play-dl/YouTube/utils/extractor.ts | 18 +++++----- play-dl/YouTube/utils/parser.ts | 29 ++++++++-------- play-dl/index.ts | 44 ++++++++++++++++++------- 12 files changed, 104 insertions(+), 131 deletions(-) delete mode 100644 play-dl/YouTube/classes/Thumbnail.ts diff --git a/play-dl/SoundCloud/index.ts b/play-dl/SoundCloud/index.ts index 18de69b..50262b3 100644 --- a/play-dl/SoundCloud/index.ts +++ b/play-dl/SoundCloud/index.ts @@ -33,11 +33,12 @@ export async function soundcloud(url: string): Promise { +): Promise { const response = await request( `https://api-v2.soundcloud.com/search/${type}?q=${query}&client_id=${soundData.client_id}&limit=${limit}` ); @@ -66,8 +67,8 @@ export async function stream(url: string, quality?: number): Promise { : StreamType.Arbitrary; return new Stream(s_data.url, type); } - -export async function stream_from_info(data: SoundCloudTrack, quality?: number): Promise { +export type SoundCloudStream = Stream; +export async function stream_from_info(data: SoundCloudTrack, quality?: number): Promise { const HLSformats = parseHlsFormats(data.formats); if (typeof quality !== 'number') quality = HLSformats.length - 1; else if (quality <= 0) quality = 0; diff --git a/play-dl/Spotify/index.ts b/play-dl/Spotify/index.ts index df5b02a..fee1362 100644 --- a/play-dl/Spotify/index.ts +++ b/play-dl/Spotify/index.ts @@ -110,11 +110,13 @@ export function is_expired(): boolean { else return false; } +export type Spotify = SpotifyAlbum | SpotifyPlaylist | SpotifyVideo; + export async function sp_search( query: string, type: 'album' | 'playlist' | 'track', limit: number = 10 -): Promise<(SpotifyAlbum | SpotifyPlaylist | SpotifyVideo)[]> { +): Promise { const results: (SpotifyAlbum | SpotifyPlaylist | SpotifyVideo)[] = []; if (!spotifyData) throw new Error('Spotify Data is missing\nDid you forgot to do authorization ?'); if (query.length === 0) throw new Error('Pass some query to search.'); diff --git a/play-dl/YouTube/classes/Channel.ts b/play-dl/YouTube/classes/Channel.ts index f1d4653..2f9520a 100644 --- a/play-dl/YouTube/classes/Channel.ts +++ b/play-dl/YouTube/classes/Channel.ts @@ -4,17 +4,18 @@ export interface ChannelIconInterface { height: number; } -export class Channel { +export class YouTubeChannel { name?: string; verified?: boolean; id?: string; + type: 'video' | 'playlist' | 'channel'; url?: string; icon?: ChannelIconInterface; subscribers?: string; constructor(data: any) { if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); - + this.type = 'channel'; this._patch(data); } @@ -41,10 +42,6 @@ export class Channel { return this.icon.url.replace(`=s${def}-c`, `=s${options.size}-c`); } - get type(): 'channel' { - return 'channel'; - } - toString(): string { return this.name || ''; } diff --git a/play-dl/YouTube/classes/Playlist.ts b/play-dl/YouTube/classes/Playlist.ts index 3254c8c..b51c710 100644 --- a/play-dl/YouTube/classes/Playlist.ts +++ b/play-dl/YouTube/classes/Playlist.ts @@ -1,22 +1,27 @@ import { getPlaylistVideos, getContinuationToken } from '../utils/extractor'; import { request } from '../utils/request'; -import { Thumbnail } from './Thumbnail'; -import { Channel } from './Channel'; -import { Video } from './Video'; +import { YouTubeChannel } from './Channel'; +import { YouTubeVideo } from './Video'; const BASE_API = 'https://www.youtube.com/youtubei/v1/browse?key='; -export class PlayList { +export class YouTubePlayList { id?: string; title?: string; + type: 'video' | 'playlist' | 'channel'; videoCount?: number; lastUpdate?: string; views?: number; url?: string; link?: string; - channel?: Channel; - thumbnail?: Thumbnail; + channel?: YouTubeChannel; + thumbnail?: { + id: string | undefined; + width: number | undefined; + height: number | undefined; + url: string | undefined; + }; private videos?: []; - private fetched_videos: Map; + private fetched_videos: Map; private _continuation: { api?: string; token?: string; @@ -28,6 +33,7 @@ export class PlayList { if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); this.__count = 0; this.fetched_videos = new Map(); + this.type = 'playlist'; if (searchResult) this.__patchSearch(data); else this.__patch(data); } @@ -44,7 +50,7 @@ export class PlayList { this.thumbnail = data.thumbnail || undefined; this.videos = data.videos || []; this.__count++; - this.fetched_videos.set(`${this.__count}`, this.videos as Video[]); + this.fetched_videos.set(`${this.__count}`, this.videos as YouTubeVideo[]); this._continuation.api = data.continuation?.api ?? undefined; this._continuation.token = data.continuation?.token ?? undefined; this._continuation.clientVersion = data.continuation?.clientVersion ?? ''; @@ -63,7 +69,7 @@ export class PlayList { this.views = 0; } - async next(limit = Infinity): Promise { + async next(limit = Infinity): Promise { if (!this._continuation || !this._continuation.token) return []; const nextPage = await request(`${BASE_API}${this._continuation.api}`, { @@ -109,14 +115,10 @@ export class PlayList { return this; } - get type(): 'playlist' { - return 'playlist'; - } - - page(number: number): Video[] { + page(number: number): YouTubeVideo[] { if (!number) throw new Error('Page number is not provided'); if (!this.fetched_videos.has(`${number}`)) throw new Error('Given Page number is invalid'); - return this.fetched_videos.get(`${number}`) as Video[]; + return this.fetched_videos.get(`${number}`) as YouTubeVideo[]; } get total_pages() { @@ -125,7 +127,7 @@ export class PlayList { get total_videos() { const page_number: number = this.total_pages; - return (page_number - 1) * 100 + (this.fetched_videos.get(`${page_number}`) as Video[]).length; + return (page_number - 1) * 100 + (this.fetched_videos.get(`${page_number}`) as YouTubeVideo[]).length; } toJSON() { diff --git a/play-dl/YouTube/classes/Thumbnail.ts b/play-dl/YouTube/classes/Thumbnail.ts deleted file mode 100644 index fce1644..0000000 --- a/play-dl/YouTube/classes/Thumbnail.ts +++ /dev/null @@ -1,49 +0,0 @@ -type ThumbnailType = 'default' | 'hqdefault' | 'mqdefault' | 'sddefault' | 'maxresdefault' | 'ultrares'; - -export class Thumbnail { - id?: string; - width?: number; - height?: number; - url?: string; - - constructor(data: any) { - if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); - - this._patch(data); - } - - private _patch(data: any) { - if (!data) data = {}; - - this.id = data.id || undefined; - this.width = data.width || 0; - this.height = data.height || 0; - this.url = data.url || undefined; - } - - displayThumbnailURL(thumbnailType: ThumbnailType = 'maxresdefault'): string { - if (!['default', 'hqdefault', 'mqdefault', 'sddefault', 'maxresdefault', 'ultrares'].includes(thumbnailType)) - throw new Error(`Invalid thumbnail type "${thumbnailType}"!`); - if (thumbnailType === 'ultrares') return this.url as string; - return `https://i3.ytimg.com/vi/${this.id}/${thumbnailType}.jpg`; - } - - defaultThumbnailURL(id: '0' | '1' | '2' | '3' | '4'): string { - if (!id) id = '0'; - if (!['0', '1', '2', '3', '4'].includes(id)) throw new Error(`Invalid thumbnail id "${id}"!`); - return `https://i3.ytimg.com/vi/${this.id}/${id}.jpg`; - } - - toString(): string { - return this.url ? `${this.url}` : ''; - } - - toJSON() { - return { - id: this.id, - width: this.width, - height: this.height, - url: this.url - }; - } -} diff --git a/play-dl/YouTube/classes/Video.ts b/play-dl/YouTube/classes/Video.ts index eaf0dc0..59f30a5 100644 --- a/play-dl/YouTube/classes/Video.ts +++ b/play-dl/YouTube/classes/Video.ts @@ -1,5 +1,4 @@ -import { Channel } from './Channel'; -import { Thumbnail } from './Thumbnail'; +import { YouTubeChannel } from './Channel'; interface VideoOptions { id?: string; @@ -21,7 +20,6 @@ interface VideoOptions { id: string; icon: string; }; - videos?: Video[]; type: string; ratings: { likes: number; @@ -32,9 +30,10 @@ interface VideoOptions { tags: string[]; } -export class Video { +export class YouTubeVideo { id?: string; - url?: string; + url: string; + type: 'video' | 'playlist' | 'channel'; title?: string; description?: string; durationRaw: string; @@ -47,8 +46,7 @@ export class Video { height: number | undefined; url: string | undefined; }; - channel?: Channel; - videos?: Video[]; + channel?: YouTubeChannel; likes: number; dislikes: number; live: boolean; @@ -60,6 +58,7 @@ export class Video { this.id = data.id || undefined; this.url = `https://www.youtube.com/watch?v=${this.id}`; + this.type = 'video'; this.title = data.title || undefined; this.description = data.description || undefined; this.durationRaw = data.duration_raw || '0:00'; @@ -75,10 +74,6 @@ export class Video { this.tags = data.tags || []; } - get type(): 'video' { - return 'video'; - } - get toString(): string { return this.url || ''; } diff --git a/play-dl/YouTube/index.ts b/play-dl/YouTube/index.ts index 8ca7478..a5b2754 100644 --- a/play-dl/YouTube/index.ts +++ b/play-dl/YouTube/index.ts @@ -1,2 +1,3 @@ -export { stream, stream_from_info } from './stream'; +export { stream, stream_from_info, YouTubeStream } from './stream'; export * from './utils'; +export { YouTube } from './search'; diff --git a/play-dl/YouTube/search.ts b/play-dl/YouTube/search.ts index 45cc9c4..adc9128 100644 --- a/play-dl/YouTube/search.ts +++ b/play-dl/YouTube/search.ts @@ -1,8 +1,8 @@ import { request } from './utils/request'; import { ParseSearchInterface, ParseSearchResult } from './utils/parser'; -import { Video } from './classes/Video'; -import { Channel } from './classes/Channel'; -import { PlayList } from './classes/Playlist'; +import { YouTubeVideo } from './classes/Video'; +import { YouTubeChannel } from './classes/Channel'; +import { YouTubePlayList } from './classes/Playlist'; enum SearchType { Video = 'EgIQAQ%253D%253D', @@ -10,10 +10,9 @@ enum SearchType { Channel = 'EgIQAg%253D%253D' } -export async function yt_search( - search: string, - options: ParseSearchInterface = {} -): Promise<(Video | Channel | PlayList)[]> { +export type YouTube = YouTubeVideo | YouTubeChannel | YouTubePlayList; + +export async function yt_search(search: string, options: ParseSearchInterface = {}): Promise { let url = 'https://www.youtube.com/results?search_query=' + search.replaceAll(' ', '+'); options.type ??= 'video'; if (!url.match('&sp=')) { diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index 02332d0..47ba5a8 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -38,13 +38,15 @@ function parseAudioFormats(formats: any[]) { return result; } -export async function stream(url: string, options: StreamOptions = {}): Promise { +export type YouTubeStream = Stream | LiveStreaming; + +export async function stream(url: string, options: StreamOptions = {}): Promise { const info = await video_info(url, options.cookie); const final: any[] = []; if ( info.LiveStreamData.isLive === true && info.LiveStreamData.hlsManifestUrl !== null && - info.video_details.durationInSec === '0' + info.video_details.durationInSec === 0 ) { return new LiveStreaming( info.LiveStreamData.dashManifestUrl, @@ -72,7 +74,7 @@ export async function stream(url: string, options: StreamOptions = {}): Promise< ); } -export async function stream_from_info(info: InfoData, options: StreamOptions = {}): Promise { +export async function stream_from_info(info: InfoData, options: StreamOptions = {}): Promise { const final: any[] = []; if ( info.LiveStreamData.isLive === true && diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index e16d307..c427243 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -1,7 +1,7 @@ import { request } from './request'; import { format_decipher } from './cipher'; -import { Video } from '../classes/Video'; -import { PlayList } from '../classes/Playlist'; +import { YouTubeVideo } from '../classes/Video'; +import { YouTubePlayList } from '../classes/Playlist'; const DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; const video_pattern = @@ -66,7 +66,7 @@ export async function video_basic_info(url: string, cookie?: string) { initial_response.contents.twoColumnWatchNextResults.results.results.contents[1]?.videoSecondaryInfoRenderer ?.owner?.videoOwnerRenderer?.badges[0]; const html5player = `https://www.youtube.com${body.split('"jsUrl":"')[1].split('"')[0]}`; - const related: any[] = []; + const related: string[] = []; initial_response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach( (res: any) => { if (res.compactVideoRenderer) @@ -76,7 +76,7 @@ export async function video_basic_info(url: string, cookie?: string) { const format = []; const vid = player_response.videoDetails; const microformat = player_response.microformat.playerMicroformatRenderer; - const video_details = { + const video_details = new YouTubeVideo({ id: vid.videoId, url: `https://www.youtube.com/watch?v=${vid.videoId}`, title: vid.title, @@ -96,7 +96,7 @@ export async function video_basic_info(url: string, cookie?: string) { averageRating: vid.averageRating, live: vid.isLiveContent, private: vid.isPrivate - }; + }); format.push(...(player_response.streamingData.formats ?? [])); format.push(...(player_response.streamingData.adaptiveFormats ?? [])); const LiveStreamData = { @@ -182,7 +182,7 @@ export async function playlist_info(url: string, parseIncomplete = false) { ?.runs.pop()?.text ?? null; const videosCount = data.stats[0].runs[0].text.replace(/[^0-9]/g, '') || 0; - const res = new PlayList({ + const res = new YouTubePlayList({ continuation: { api: API_KEY, token: getContinuationToken(parsed), @@ -217,13 +217,13 @@ export async function playlist_info(url: string, parseIncomplete = false) { thumbnail: data.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length ? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[ data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length - 1 - ].url + ] : null }); return res; } -export function getPlaylistVideos(data: any, limit = Infinity): Video[] { +export function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] { const videos = []; for (let i = 0; i < data.length; i++) { @@ -232,7 +232,7 @@ export function getPlaylistVideos(data: any, limit = Infinity): Video[] { if (!info || !info.shortBylineText) continue; videos.push( - new Video({ + new YouTubeVideo({ id: info.videoId, index: parseInt(info.index?.simpleText) || 0, duration: parseDuration(info.lengthText?.simpleText) || 0, diff --git a/play-dl/YouTube/utils/parser.ts b/play-dl/YouTube/utils/parser.ts index bd8d619..e02797f 100644 --- a/play-dl/YouTube/utils/parser.ts +++ b/play-dl/YouTube/utils/parser.ts @@ -1,6 +1,6 @@ -import { Video } from '../classes/Video'; -import { PlayList } from '../classes/Playlist'; -import { Channel } from '../classes/Channel'; +import { YouTubeVideo } from '../classes/Video'; +import { YouTubePlayList } from '../classes/Playlist'; +import { YouTubeChannel } from '../classes/Channel'; export interface ParseSearchInterface { type?: 'video' | 'playlist' | 'channel'; @@ -13,7 +13,10 @@ export interface thumbnail { url: string; } -export function ParseSearchResult(html: string, options?: ParseSearchInterface): (Video | PlayList | Channel)[] { +export function ParseSearchResult( + html: string, + options?: ParseSearchInterface +): (YouTubeVideo | YouTubePlayList | YouTubeChannel)[] { if (!html) throw new Error("Can't parse Search result without data"); if (!options) options = { type: 'video', limit: 0 }; if (!options.type) options.type = 'video'; @@ -62,14 +65,14 @@ function parseDuration(duration: string): number { return dur; } -export function parseChannel(data?: any): Channel | void { - if (!data || !data.channelRenderer) return; +export function parseChannel(data?: any): YouTubeChannel { + if (!data || !data.channelRenderer) throw new Error('Failed to Parse YouTube Channel'); const badge = data.channelRenderer.ownerBadges && data.channelRenderer.ownerBadges[0]; const url = `https://www.youtube.com${ data.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl || data.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url }`; - const res = new Channel({ + const res = new YouTubeChannel({ id: data.channelRenderer.channelId, name: data.channelRenderer.title.simpleText, icon: { @@ -91,11 +94,11 @@ export function parseChannel(data?: any): Channel | void { return res; } -export function parseVideo(data?: any): Video | void { - if (!data || !data.videoRenderer) return; +export function parseVideo(data?: any): YouTubeVideo { + if (!data || !data.videoRenderer) throw new Error('Failed to Parse YouTube Video'); const badge = data.videoRenderer.ownerBadges && data.videoRenderer.ownerBadges[0]; - const res = new Video({ + const res = new YouTubeVideo({ id: data.videoRenderer.videoId, url: `https://www.youtube.com/watch?v=${data.videoRenderer.videoId}`, title: data.videoRenderer.title.runs[0].text, @@ -131,10 +134,10 @@ export function parseVideo(data?: any): Video | void { return res; } -export function parsePlaylist(data?: any): PlayList | void { - if (!data.playlistRenderer) return; +export function parsePlaylist(data?: any): YouTubePlayList { + if (!data.playlistRenderer) throw new Error('Failed to Parse YouTube Playlist'); - const res = new PlayList( + const res = new YouTubePlayList( { id: data.playlistRenderer.playlistId, title: data.playlistRenderer.title.simpleText, diff --git a/play-dl/index.ts b/play-dl/index.ts index a985656..57e4d4a 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -1,6 +1,6 @@ -export { playlist_info, video_basic_info, video_info, yt_validate, extractID } from './YouTube'; -export { spotify, sp_validate, refreshToken, is_expired } from './Spotify'; -export { soundcloud, so_validate } from './SoundCloud'; +export { playlist_info, video_basic_info, video_info, yt_validate, extractID, YouTube, YouTubeStream } from './YouTube'; +export { spotify, sp_validate, refreshToken, is_expired, Spotify } from './Spotify'; +export { soundcloud, so_validate, SoundCloud, SoundCloudStream } from './SoundCloud'; interface SearchOptions { limit?: number; @@ -13,20 +13,32 @@ interface SearchOptions { import readline from 'readline'; import fs from 'fs'; -import { sp_validate, yt_validate, so_validate } from '.'; +import { sp_validate, yt_validate, so_validate, YouTubeStream, SoundCloudStream } from '.'; import { SpotifyAuthorize, sp_search } from './Spotify'; import { check_id, so_search, stream as so_stream, stream_from_info as so_stream_info } from './SoundCloud'; import { InfoData, stream as yt_stream, StreamOptions, stream_from_info as yt_stream_info } from './YouTube/stream'; -import { SoundCloudTrack, Stream as SoStream } from './SoundCloud/classes'; -import { LiveStreaming, Stream as YTStream } from './YouTube/classes/LiveStream'; +import { SoundCloudTrack } from './SoundCloud/classes'; import { yt_search } from './YouTube/search'; -export async function stream(url: string, options: StreamOptions = {}): Promise { +/** + * Main stream Command for streaming through various sources + * @param url The video / track url to make stream of + * @param options contains quality and cookie to set for stream + * @returns YouTube / SoundCloud Stream to play + */ + +export async function stream(url: string, options: StreamOptions = {}): Promise { if (url.length === 0) throw new Error('Stream URL has a length of 0. Check your url again.'); if (url.indexOf('soundcloud') !== -1) return await so_stream(url, options.quality); else return await yt_stream(url, { cookie: options.cookie }); } +/** + * Main Search Command for searching through various sources + * @param query string to search. + * @param options contains limit and source to choose. + * @returns + */ export async function search(query: string, options: SearchOptions = {}) { if (!options.source) options.source = { youtube: 'video' }; @@ -35,25 +47,33 @@ export async function search(query: string, options: SearchOptions = {}) { else if (options.source.soundcloud) return await so_search(query, options.source.soundcloud, options.limit); } +/** + * + * @param info + * @param options + * @returns + */ export async function stream_from_info( info: InfoData | SoundCloudTrack, options: StreamOptions = {} -): Promise { +): Promise { if (info instanceof SoundCloudTrack) return await so_stream_info(info); else return await yt_stream_info(info, { cookie: options.cookie }); } -export async function validate(url: string): Promise<"so_playlist" | "so_track" | "sp_track" | "sp_album" | "sp_playlist" | "yt_video" | "yt_playlist" | false> { +export async function validate( + url: string +): Promise<'so_playlist' | 'so_track' | 'sp_track' | 'sp_album' | 'sp_playlist' | 'yt_video' | 'yt_playlist' | false> { let check; if (url.indexOf('spotify') !== -1) { check = sp_validate(url); - return check !== false ? 'sp_' + check as "sp_track" | "sp_album" | "sp_playlist" : false; + return check !== false ? (('sp_' + check) as 'sp_track' | 'sp_album' | 'sp_playlist') : false; } else if (url.indexOf('soundcloud') !== -1) { check = await so_validate(url); - return check !== false ? 'so_' + check as "so_playlist" | "so_track" : false; + return check !== false ? (('so_' + check) as 'so_playlist' | 'so_track') : false; } else { check = yt_validate(url); - return check !== false ? 'yt_' + check as "yt_video" | "yt_playlist" : false; + return check !== false ? (('yt_' + check) as 'yt_video' | 'yt_playlist') : false; } } From f21f34ea315986220636d2481aface215c3c1e4e Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Mon, 27 Sep 2021 23:12:22 +0530 Subject: [PATCH 02/11] Added Proxy Options --- play-dl/YouTube/utils/request.ts | 10 ++++++++++ play-dl/index.ts | 13 +++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/play-dl/YouTube/utils/request.ts b/play-dl/YouTube/utils/request.ts index c502b73..ea972d6 100644 --- a/play-dl/YouTube/utils/request.ts +++ b/play-dl/YouTube/utils/request.ts @@ -2,9 +2,19 @@ import https, { RequestOptions } from 'https'; import { IncomingMessage } from 'http'; import { URL } from 'url'; +interface ProxyOpts { + host : string, + port : number, + authentication? : { + username : string; + password : string; + } +} + interface RequestOpts extends RequestOptions { body?: string; method?: 'GET' | 'POST'; + proxies? : string[] | ProxyOpts[] } function https_getter(req_url: string, options: RequestOpts = {}): Promise { diff --git a/play-dl/index.ts b/play-dl/index.ts index 57e4d4a..e7ffa77 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -13,7 +13,7 @@ interface SearchOptions { import readline from 'readline'; import fs from 'fs'; -import { sp_validate, yt_validate, so_validate, YouTubeStream, SoundCloudStream } from '.'; +import { sp_validate, yt_validate, so_validate, YouTubeStream, SoundCloudStream, YouTube, SoundCloud, Spotify } from '.'; import { SpotifyAuthorize, sp_search } from './Spotify'; import { check_id, so_search, stream as so_stream, stream_from_info as so_stream_info } from './SoundCloud'; import { InfoData, stream as yt_stream, StreamOptions, stream_from_info as yt_stream_info } from './YouTube/stream'; @@ -30,7 +30,7 @@ import { yt_search } from './YouTube/search'; export async function stream(url: string, options: StreamOptions = {}): Promise { if (url.length === 0) throw new Error('Stream URL has a length of 0. Check your url again.'); if (url.indexOf('soundcloud') !== -1) return await so_stream(url, options.quality); - else return await yt_stream(url, { cookie: options.cookie }); + else return await yt_stream(url, options); } /** @@ -39,16 +39,17 @@ export async function stream(url: string, options: StreamOptions = {}): Promise< * @param options contains limit and source to choose. * @returns */ -export async function search(query: string, options: SearchOptions = {}) { +export async function search(query: string, options: SearchOptions = {}): Promise { if (!options.source) options.source = { youtube: 'video' }; if (options.source.youtube) return await yt_search(query, { limit: options.limit, type: options.source.youtube }); else if (options.source.spotify) return await sp_search(query, options.source.spotify, options.limit); else if (options.source.soundcloud) return await so_search(query, options.source.soundcloud, options.limit); + else throw new Error('Not possible to reach Here LOL. Easter Egg of play-dl if someone get this.') } /** - * + * Command to be used * @param info * @param options * @returns @@ -57,8 +58,8 @@ export async function stream_from_info( info: InfoData | SoundCloudTrack, options: StreamOptions = {} ): Promise { - if (info instanceof SoundCloudTrack) return await so_stream_info(info); - else return await yt_stream_info(info, { cookie: options.cookie }); + if (info instanceof SoundCloudTrack) return await so_stream_info(info, options.quality); + else return await yt_stream_info(info, options); } export async function validate( From 9d946a84a6c081d7504d04a75669e5944e6e5c16 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Tue, 28 Sep 2021 19:20:58 +0530 Subject: [PATCH 03/11] Proxy support added completely --- play-dl/YouTube/classes/LiveStream.ts | 2 +- play-dl/YouTube/utils/request.ts | 129 +++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 15 deletions(-) diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index 0c32f9a..0dec6ff 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -47,7 +47,7 @@ export class LiveStreaming { if ( info.LiveStreamData.isLive === true && info.LiveStreamData.hlsManifestUrl !== null && - info.video_details.durationInSec === '0' + info.video_details.durationInSec === 0 ) { this.url = info.LiveStreamData.dashManifestUrl; } diff --git a/play-dl/YouTube/utils/request.ts b/play-dl/YouTube/utils/request.ts index ea972d6..1d065b3 100644 --- a/play-dl/YouTube/utils/request.ts +++ b/play-dl/YouTube/utils/request.ts @@ -1,7 +1,10 @@ import https, { RequestOptions } from 'https'; -import { IncomingMessage } from 'http'; +import tls from 'tls'; +import http , { ClientRequest, IncomingMessage } from 'http'; import { URL } from 'url'; +export type Proxy = ProxyOpts | string + interface ProxyOpts { host : string, port : number, @@ -11,10 +14,16 @@ interface ProxyOpts { } } +interface ProxyOutput { + statusCode : number; + head : string; + body : string; +} + interface RequestOpts extends RequestOptions { body?: string; method?: 'GET' | 'POST'; - proxies? : string[] | ProxyOpts[] + proxies? : Proxy[] } function https_getter(req_url: string, options: RequestOpts = {}): Promise { @@ -37,22 +46,114 @@ function https_getter(req_url: string, options: RequestOpts = {}): Promise{ + return new Promise((resolve, reject) => { + const proxy : string | ProxyOpts = req_proxy[randomIntFromInterval(0, req_proxy.length)] + const parsed_url = new URL(req_url) + let opts : ProxyOpts + if(typeof proxy === 'string'){ + const parsed = new URL(proxy) + opts = { + host : parsed.hostname, + port : Number(parsed.port), + authentication : { + username : parsed.username, + password : parsed.password + } + } + } + else opts = proxy + let req : ClientRequest + if(opts.authentication?.username.length === 0){ + req = http.request({ + host: opts.host, + port: opts.port, + method: 'CONNECT', + path: `${parsed_url.host}:443`, + }); + } + else { + req = http.request({ + host: opts.host, + port: opts.port, + method: 'CONNECT', + path: `${parsed_url.host}:443`, + headers : { + "Proxy-Authorization" : `Basic ${Buffer.from(`${opts.authentication?.username}:${opts.authentication?.password}`).toString('base64')}` + } + }); + } + + req.on('connect', function (res, socket, head) { + console.log('Connected') + const tlsConnection = tls.connect({ + host : parsed_url.hostname, + port : 443, + socket : socket, + rejectUnauthorized : false + }, function() { + tlsConnection.write(`GET ${parsed_url.pathname}${parsed_url.search} HTTP/1.1\r\n` + + `Host : ${parsed_url.hostname}\r\n` + + 'Connection: close\r\n' + + '\r\n') + }) + + tlsConnection.setEncoding('utf-8') + let data = '' + tlsConnection.once('error', (e) => reject(e)) + tlsConnection.on('data', (c) => data+=c) + tlsConnection.on('end', () => { + const y = data.split('\r\n\r\n') + const head = y.shift() as string + resolve({ + statusCode : Number(head.split('\n')[0].split(' ')[1]), + head : head, + body : y.join('\n') + }) + }) + }) + req.on('error', (e : Error) => reject(e)) + req.end() + }) +} + export async function request(url: string, options?: RequestOpts): Promise { return new Promise(async (resolve, reject) => { - let data = ''; - let res = await https_getter(url, options).catch((err: Error) => err); - if (res instanceof Error) { - reject(res); - return; + if(!options?.proxies){ + let data = ''; + let res = await https_getter(url, options).catch((err: Error) => err); + if (res instanceof Error) { + reject(res); + return; + } + if (Number(res.statusCode) >= 300 && Number(res.statusCode) < 400) { + res = await https_getter(res.headers.location as string, options); + } else if (Number(res.statusCode) > 400) { + reject(new Error(`Got ${res.statusCode} from the request`)); + } + res.setEncoding('utf-8'); + res.on('data', (c) => (data += c)); + res.on('end', () => resolve(data)); } - if (Number(res.statusCode) >= 300 && Number(res.statusCode) < 400) { - res = await https_getter(res.headers.location as string, options); - } else if (Number(res.statusCode) > 400) { - reject(new Error(`Got ${res.statusCode} from the request`)); + else { + let res = await proxy_getter(url, options.proxies).catch((e : Error) => e) + if (res instanceof Error) { + reject(res); + return; + } + if(res.statusCode >= 300 && res.statusCode < 400){ + res = await proxy_getter(res.head.split('Location: ')[1].split('\n')[0], options.proxies) + } else if (res.statusCode > 400){ + reject(new Error(`GOT ${res.statusCode} from proxy request`)) + } + resolve(res.body) } - res.setEncoding('utf-8'); - res.on('data', (c) => (data += c)); - res.on('end', () => resolve(data)); }); } From 80d1ae27501b1701c0cf0f98180b1a30d0965859 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Tue, 28 Sep 2021 20:45:45 +0530 Subject: [PATCH 04/11] Proxy codes completed --- play-dl/YouTube/utils/extractor.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index c427243..4293009 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -1,8 +1,18 @@ -import { request } from './request'; +import { Proxy, request } from './request'; import { format_decipher } from './cipher'; import { YouTubeVideo } from '../classes/Video'; import { YouTubePlayList } from '../classes/Playlist'; +interface InfoOptions{ + cookie? : string; + proxy? : Proxy[] +} + +interface PlaylistOptions { + incomplete? : boolean; + proxy? : Proxy[] +} + const DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; const video_pattern = /^((?:https?:)?\/\/)?(?:(?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; @@ -36,7 +46,7 @@ export function extractID(url: string): string { } else return url; } -export async function video_basic_info(url: string, cookie?: string) { +export async function video_basic_info(url: string, options: InfoOptions = {}) { let video_id: string; if (url.startsWith('https')) { if (yt_validate(url) !== 'video') throw new Error('This is not a YouTube Watch URL'); @@ -44,9 +54,10 @@ export async function video_basic_info(url: string, cookie?: string) { } else video_id = url; const new_url = `https://www.youtube.com/watch?v=${video_id}`; const body = await request(new_url, { - headers: cookie + proxies : options.proxy ?? [], + headers: options.cookie ? { - 'cookie': cookie, + 'cookie': options.cookie, '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' } @@ -125,8 +136,8 @@ function parseSeconds(seconds: number): string { return hDisplay + mDisplay + sDisplay; } -export async function video_info(url: string, cookie?: string) { - const data = await video_basic_info(url, cookie); +export async function video_info(url: string, options: InfoOptions = {}) { + const data = await video_basic_info(url, options); if (data.LiveStreamData.isLive === true && data.LiveStreamData.hlsManifestUrl !== null) { return data; } else if (data.format[0].signatureCipher || data.format[0].cipher) { @@ -137,7 +148,7 @@ export async function video_info(url: string, cookie?: string) { } } -export async function playlist_info(url: string, parseIncomplete = false) { +export async function playlist_info(url: string, options : PlaylistOptions = {}) { if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`); let Playlist_id: string; if (url.startsWith('https')) { @@ -147,12 +158,13 @@ export async function playlist_info(url: string, parseIncomplete = false) { const new_url = `https://www.youtube.com/playlist?list=${Playlist_id}`; const body = await request(new_url, { + proxies : options.proxy ?? [], headers: { 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7' } }); const response = JSON.parse(body.split('var ytInitialData = ')[1].split(';')[0]); if (response.alerts) { if (response.alerts[0].alertWithButtonRenderer?.type === 'INFO') { - if (!parseIncomplete) + if (!options.incomplete) throw new Error( `While parsing playlist url\n${response.alerts[0].alertWithButtonRenderer.text.simpleText}` ); From 9ab641d1f08f8ddb83b34b665e566ed75c990a3f Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Tue, 28 Sep 2021 20:56:10 +0530 Subject: [PATCH 05/11] LiveStream Error fixes --- play-dl/YouTube/classes/LiveStream.ts | 13 ++++++++++--- play-dl/YouTube/stream.ts | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index 0dec6ff..914bd7d 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -174,7 +174,7 @@ export class Stream { } private async retry() { - const info = await video_info(this.video_url, this.cookie); + const info = await video_info(this.video_url, { cookie : this.cookie }); this.url = info.format[info.format.length - 1].url; } @@ -220,8 +220,15 @@ export class Stream { this.request = stream; stream.pipe(this.stream, { end: false }); - stream.once('error', (err) => { - this.stream.emit('error', err); + stream.once('error', async(err) => { + this.cleanup() + await this.retry(); + this.loop(); + if (!this.timer) { + this.timer = setInterval(() => { + this.retry(); + }, 7200 * 1000); + } }); stream.on('data', (chunk: any) => { diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index 47ba5a8..96a0d7a 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -41,7 +41,7 @@ function parseAudioFormats(formats: any[]) { export type YouTubeStream = Stream | LiveStreaming; export async function stream(url: string, options: StreamOptions = {}): Promise { - const info = await video_info(url, options.cookie); + const info = await video_info(url, { cookie : options.cookie }); const final: any[] = []; if ( info.LiveStreamData.isLive === true && From 20984ce9e97f48b2197513b152567f92a7581cb5 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Tue, 28 Sep 2021 21:05:43 +0530 Subject: [PATCH 06/11] pretty codes and LiveStream issues fixed 100% --- play-dl/YouTube/classes/LiveStream.ts | 16 ++- play-dl/YouTube/stream.ts | 12 +- play-dl/YouTube/utils/extractor.ts | 16 +-- play-dl/YouTube/utils/request.ts | 152 +++++++++++++------------- play-dl/index.ts | 20 +++- 5 files changed, 120 insertions(+), 96 deletions(-) diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index 914bd7d..534182e 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -1,6 +1,6 @@ import { PassThrough } from 'stream'; import { IncomingMessage } from 'http'; -import { StreamType } from '../stream'; +import { parseAudioFormats, StreamType } from '../stream'; import { request, request_stream } from '../utils/request'; import { video_info } from '..'; @@ -132,6 +132,7 @@ export class Stream { private cookie: string; private data_ended: boolean; private playing_count: number; + private quality: number; private request: IncomingMessage | null; constructor( url: string, @@ -139,10 +140,12 @@ export class Stream { duration: number, contentLength: number, video_url: string, - cookie: string + cookie: string, + quality: number ) { this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 }); this.url = url; + this.quality = quality; this.type = type; this.bytes_count = 0; this.video_url = video_url; @@ -174,8 +177,9 @@ export class Stream { } private async retry() { - const info = await video_info(this.video_url, { cookie : this.cookie }); - this.url = info.format[info.format.length - 1].url; + const info = await video_info(this.video_url, { cookie: this.cookie }); + const audioFormat = parseAudioFormats(info.format); + this.url = audioFormat[this.quality].url; } private cleanup() { @@ -220,8 +224,8 @@ export class Stream { this.request = stream; stream.pipe(this.stream, { end: false }); - stream.once('error', async(err) => { - this.cleanup() + stream.once('error', async (err) => { + this.cleanup(); await this.retry(); this.loop(); if (!this.timer) { diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index 96a0d7a..6953d26 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -1,5 +1,6 @@ import { video_info } from '.'; import { LiveStreaming, Stream } from './classes/LiveStream'; +import { Proxy } from './utils/request'; export enum StreamType { Arbitrary = 'arbitrary', @@ -12,6 +13,7 @@ export enum StreamType { export interface StreamOptions { quality?: number; cookie?: string; + proxy?: Proxy[]; } export interface InfoData { @@ -25,7 +27,7 @@ export interface InfoData { video_details: any; } -function parseAudioFormats(formats: any[]) { +export function parseAudioFormats(formats: any[]) { const result: any[] = []; formats.forEach((format) => { const type = format.mimeType as string; @@ -41,7 +43,7 @@ function parseAudioFormats(formats: any[]) { export type YouTubeStream = Stream | LiveStreaming; export async function stream(url: string, options: StreamOptions = {}): Promise { - const info = await video_info(url, { cookie : options.cookie }); + const info = await video_info(url, { cookie: options.cookie, proxy: options.proxy }); const final: any[] = []; if ( info.LiveStreamData.isLive === true && @@ -70,7 +72,8 @@ export async function stream(url: string, options: StreamOptions = {}): Promise< info.video_details.durationInSec, Number(final[0].contentLength), info.video_details.url, - options.cookie as string + options.cookie as string, + options.quality ); } @@ -103,6 +106,7 @@ export async function stream_from_info(info: InfoData, options: StreamOptions = info.video_details.durationInSec, Number(final[0].contentLength), info.video_details.url, - options.cookie as string + options.cookie as string, + options.quality ); } diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index 4293009..ed72797 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -3,14 +3,14 @@ import { format_decipher } from './cipher'; import { YouTubeVideo } from '../classes/Video'; import { YouTubePlayList } from '../classes/Playlist'; -interface InfoOptions{ - cookie? : string; - proxy? : Proxy[] +interface InfoOptions { + cookie?: string; + proxy?: Proxy[]; } interface PlaylistOptions { - incomplete? : boolean; - proxy? : Proxy[] + incomplete?: boolean; + proxy?: Proxy[]; } const DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; @@ -54,7 +54,7 @@ export async function video_basic_info(url: string, options: InfoOptions = {}) { } else video_id = url; const new_url = `https://www.youtube.com/watch?v=${video_id}`; const body = await request(new_url, { - proxies : options.proxy ?? [], + proxies: options.proxy ?? [], headers: options.cookie ? { 'cookie': options.cookie, @@ -148,7 +148,7 @@ export async function video_info(url: string, options: InfoOptions = {}) { } } -export async function playlist_info(url: string, options : PlaylistOptions = {}) { +export async function playlist_info(url: string, options: PlaylistOptions = {}) { if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`); let Playlist_id: string; if (url.startsWith('https')) { @@ -158,7 +158,7 @@ export async function playlist_info(url: string, options : PlaylistOptions = {}) const new_url = `https://www.youtube.com/playlist?list=${Playlist_id}`; const body = await request(new_url, { - proxies : options.proxy ?? [], + proxies: options.proxy ?? [], headers: { 'accept-language': 'en-US,en-IN;q=0.9,en;q=0.8,hi;q=0.7' } }); const response = JSON.parse(body.split('var ytInitialData = ')[1].split(';')[0]); diff --git a/play-dl/YouTube/utils/request.ts b/play-dl/YouTube/utils/request.ts index 1d065b3..8a17852 100644 --- a/play-dl/YouTube/utils/request.ts +++ b/play-dl/YouTube/utils/request.ts @@ -1,29 +1,29 @@ import https, { RequestOptions } from 'https'; import tls from 'tls'; -import http , { ClientRequest, IncomingMessage } from 'http'; +import http, { ClientRequest, IncomingMessage } from 'http'; import { URL } from 'url'; -export type Proxy = ProxyOpts | string +export type Proxy = ProxyOpts | string; interface ProxyOpts { - host : string, - port : number, - authentication? : { - username : string; - password : string; - } + host: string; + port: number; + authentication?: { + username: string; + password: string; + }; } interface ProxyOutput { - statusCode : number; - head : string; - body : string; + statusCode: number; + head: string; + body: string; } interface RequestOpts extends RequestOptions { body?: string; method?: 'GET' | 'POST'; - proxies? : Proxy[] + proxies?: Proxy[]; } function https_getter(req_url: string, options: RequestOpts = {}): Promise { @@ -46,86 +46,91 @@ function https_getter(req_url: string, options: RequestOpts = {}): Promise{ +async function proxy_getter(req_url: string, req_proxy: Proxy[]): Promise { return new Promise((resolve, reject) => { - const proxy : string | ProxyOpts = req_proxy[randomIntFromInterval(0, req_proxy.length)] - const parsed_url = new URL(req_url) - let opts : ProxyOpts - if(typeof proxy === 'string'){ - const parsed = new URL(proxy) + const proxy: string | ProxyOpts = req_proxy[randomIntFromInterval(0, req_proxy.length)]; + const parsed_url = new URL(req_url); + let opts: ProxyOpts; + if (typeof proxy === 'string') { + const parsed = new URL(proxy); opts = { - host : parsed.hostname, - port : Number(parsed.port), - authentication : { - username : parsed.username, - password : parsed.password + host: parsed.hostname, + port: Number(parsed.port), + authentication: { + username: parsed.username, + password: parsed.password } - } - } - else opts = proxy - let req : ClientRequest - if(opts.authentication?.username.length === 0){ + }; + } else opts = proxy; + let req: ClientRequest; + if (opts.authentication?.username.length === 0) { req = http.request({ host: opts.host, port: opts.port, method: 'CONNECT', - path: `${parsed_url.host}:443`, + path: `${parsed_url.host}:443` }); - } - else { + } else { req = http.request({ host: opts.host, port: opts.port, method: 'CONNECT', path: `${parsed_url.host}:443`, - headers : { - "Proxy-Authorization" : `Basic ${Buffer.from(`${opts.authentication?.username}:${opts.authentication?.password}`).toString('base64')}` + headers: { + 'Proxy-Authorization': `Basic ${Buffer.from( + `${opts.authentication?.username}:${opts.authentication?.password}` + ).toString('base64')}` } }); } req.on('connect', function (res, socket, head) { - console.log('Connected') - const tlsConnection = tls.connect({ - host : parsed_url.hostname, - port : 443, - socket : socket, - rejectUnauthorized : false - }, function() { - tlsConnection.write(`GET ${parsed_url.pathname}${parsed_url.search} HTTP/1.1\r\n` - + `Host : ${parsed_url.hostname}\r\n` - + 'Connection: close\r\n' - + '\r\n') - }) + console.log('Connected'); + const tlsConnection = tls.connect( + { + host: parsed_url.hostname, + port: 443, + socket: socket, + rejectUnauthorized: false + }, + function () { + tlsConnection.write( + `GET ${parsed_url.pathname}${parsed_url.search} HTTP/1.1\r\n` + + `Host : ${parsed_url.hostname}\r\n` + + 'Connection: close\r\n' + + '\r\n' + ); + } + ); - tlsConnection.setEncoding('utf-8') - let data = '' - tlsConnection.once('error', (e) => reject(e)) - tlsConnection.on('data', (c) => data+=c) + tlsConnection.setEncoding('utf-8'); + let data = ''; + tlsConnection.once('error', (e) => reject(e)); + tlsConnection.on('data', (c) => (data += c)); tlsConnection.on('end', () => { - const y = data.split('\r\n\r\n') - const head = y.shift() as string + const y = data.split('\r\n\r\n'); + const head = y.shift() as string; resolve({ - statusCode : Number(head.split('\n')[0].split(' ')[1]), - head : head, - body : y.join('\n') - }) - }) - }) - req.on('error', (e : Error) => reject(e)) - req.end() - }) + statusCode: Number(head.split('\n')[0].split(' ')[1]), + head: head, + body: y.join('\n') + }); + }); + }); + req.on('error', (e: Error) => reject(e)); + req.end(); + }); } export async function request(url: string, options?: RequestOpts): Promise { return new Promise(async (resolve, reject) => { - if(!options?.proxies){ + if (!options?.proxies) { let data = ''; let res = await https_getter(url, options).catch((err: Error) => err); if (res instanceof Error) { @@ -140,19 +145,18 @@ export async function request(url: string, options?: RequestOpts): Promise (data += c)); res.on('end', () => resolve(data)); - } - else { - let res = await proxy_getter(url, options.proxies).catch((e : Error) => e) + } else { + let res = await proxy_getter(url, options.proxies).catch((e: Error) => e); if (res instanceof Error) { reject(res); return; } - if(res.statusCode >= 300 && res.statusCode < 400){ - res = await proxy_getter(res.head.split('Location: ')[1].split('\n')[0], options.proxies) - } else if (res.statusCode > 400){ - reject(new Error(`GOT ${res.statusCode} from proxy request`)) + if (res.statusCode >= 300 && res.statusCode < 400) { + res = await proxy_getter(res.head.split('Location: ')[1].split('\n')[0], options.proxies); + } else if (res.statusCode > 400) { + reject(new Error(`GOT ${res.statusCode} from proxy request`)); } - resolve(res.body) + resolve(res.body); } }); } diff --git a/play-dl/index.ts b/play-dl/index.ts index e7ffa77..32f7fa5 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -13,7 +13,16 @@ interface SearchOptions { import readline from 'readline'; import fs from 'fs'; -import { sp_validate, yt_validate, so_validate, YouTubeStream, SoundCloudStream, YouTube, SoundCloud, Spotify } from '.'; +import { + sp_validate, + yt_validate, + so_validate, + YouTubeStream, + SoundCloudStream, + YouTube, + SoundCloud, + Spotify +} from '.'; import { SpotifyAuthorize, sp_search } from './Spotify'; import { check_id, so_search, stream as so_stream, stream_from_info as so_stream_info } from './SoundCloud'; import { InfoData, stream as yt_stream, StreamOptions, stream_from_info as yt_stream_info } from './YouTube/stream'; @@ -39,17 +48,20 @@ export async function stream(url: string, options: StreamOptions = {}): Promise< * @param options contains limit and source to choose. * @returns */ -export async function search(query: string, options: SearchOptions = {}): Promise { +export async function search( + query: string, + options: SearchOptions = {} +): Promise { if (!options.source) options.source = { youtube: 'video' }; if (options.source.youtube) return await yt_search(query, { limit: options.limit, type: options.source.youtube }); else if (options.source.spotify) return await sp_search(query, options.source.spotify, options.limit); else if (options.source.soundcloud) return await so_search(query, options.source.soundcloud, options.limit); - else throw new Error('Not possible to reach Here LOL. Easter Egg of play-dl if someone get this.') + else throw new Error('Not possible to reach Here LOL. Easter Egg of play-dl if someone get this.'); } /** - * Command to be used + * Command to be used * @param info * @param options * @returns From 8e96e24aeb69d7fab052b6694997e5165174d582 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Tue, 28 Sep 2021 21:25:41 +0530 Subject: [PATCH 07/11] Updated Docs --- docs/README.md | 21 +++++++++++++++++++-- docs/YouTube/README.md | 35 +++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/docs/README.md b/docs/README.md index 1ea6ed9..1fa5b92 100644 --- a/docs/README.md +++ b/docs/README.md @@ -77,6 +77,7 @@ let data = await search('Rick Roll', { limit: 1, source : { soundcloud : "tracks - quality : `number` :- Sets quality of stream [ 0 = Lowest, 1 = Medium ]. Leave this empty to get highest audio quality. - cookie : `string` :- **[Cookies](https://github.com/play-dl/play-dl/discussions/34)** are optional and are required for playing age restricted videos. +- proxy : `Proxy` :- Optional parameter to add support of proxies. As of now, HTTPS proxies are only supported. So make sure to get HTTPS proxies only. #### stream(url : `string`, options? : [`StreamOptions`](https://github.com/play-dl/play-dl/tree/main/docs#streamoptions-)) @@ -91,6 +92,14 @@ let source = await stream("url", { quality : 1 }) // Next to Lowest quality. let source = await stream("url", { cookie: COOKIE }) //This will create a stream Class and also give cookies. +let source = await stream(url, { proxy : ['url'] }) // Accepts a url which has port in in it. + +let source = await stream(url. {proxy : [{ + host : "IP or hostname", + port : 8080 + }] +}) // Or add a json containing hostname and port. + let resource = createAudioResource(source.stream, { inputType : source.type }) // This creates resource for playing @@ -104,7 +113,7 @@ _This is basic to create a stream from a info [ from [video_info](https://github ```js let source = await stream_from_info(info) // This will create a stream Class from video_info or SoundCoudTrack Class. Highest Quality - + let source = await stream_from_info(info, { quality : 0 }) // Lowest quality let source = await stream_from_info(info, { quality : 1 }) // Next to Lowest quality. @@ -112,7 +121,15 @@ let source = await stream_from_info(info, { quality : 1 }) // Next to Lowest qua let source = await stream_from_info(info, { cookie: COOKIE }) //This will create a stream Class and also give cookies if retrying. - let resource = createAudioResource(source.stream, { +let source = await stream_from_info(url, { proxy : ['url'] }) // Accepts a url which has port in in it. + +let source = await stream_from_info(url. {proxy : [{ + host : "IP or hostname", + port : 8080 + }] +}) // Or add a json containing hostname and port. + +let resource = createAudioResource(source.stream, { inputType : source.type }) // This creates resource for playing ``` diff --git a/docs/YouTube/README.md b/docs/YouTube/README.md index d054fd3..0267d44 100644 --- a/docs/YouTube/README.md +++ b/docs/YouTube/README.md @@ -43,22 +43,37 @@ let id = extractID(url) ## Video -### video_basic_info(url : `string`, cookie? : `string`) +### InfoOptions + +_This are the info options that can be passed as a parameter in `video_info` and `video_basic_info`_ + +- cookie : **[Cookies](https://github.com/play-dl/play-dl/discussions/34) are optional and are required for playing age restricted videos.** +- proxy : Optional parameter to add support of proxies. As of now, HTTPS proxies are only supported. So make sure to get HTTPS proxies only. + +```js +const video = await video_basic_info(url, { cookie : "cookies" }) // Gives cookies support + +const video = await video_basic_info(url, { proxy : ['url'] }) // Accepts a url which has port in in it. + +const video = await video_basic_info(url. {proxy : [{ + host : "IP or hostname", + port : 8080 + }] +}) // Or add a json containing hostname and port. +``` + +### video_basic_info(url : `string`, options? : [`InfoOptions`]()) _The basic video details `play-dl` fetches at first from url or videoID._ -**[Cookies](https://github.com/play-dl/play-dl/discussions/34) are optional and are required for playing age restricted videos.** - ```js const video = await video_basic_info(url) ``` -### video_info(url : `string`, cookie? : `string`) +### video_info(url : `string`, , options? : [`InfoOptions`]()) _This contains everything with deciphered formats along with `video_details`. It can fetech data from url or videoID._ -**[Cookies](https://github.com/play-dl/play-dl/discussions/34) are optional and are required for playing age restricted videos.** - ```js const video = await video_info(url) ``` @@ -74,18 +89,18 @@ const video = await video_info(url) ## Playlist -### playlist_info(url : `string`, parseIncomplete : `boolean`) +### playlist_info(url : `string`, options : `PlaylistOptions`) _This fetches all details about a playlist from a url or playlistID._ -**parseIncomplete** is optional parameter if you want to parse playlist with hidden videos. - ```js const playlist = await playlist_info(url) //This only fetches first 100 videos from a playlist -const playlist = await playlist_info(url, true) +const playlist = await playlist_info(url, { incomplete : true }) //This only fetches first 100 videos from a playlist and also parses playlist with hidden videos + +const playlist = await playlist_info(url, { proxy : [''] }) // Same 2 options as mentioned in InfoOptions ``` - #### fetch() `method` From 5d6b9167ed9b6d30a127cb2a7567704ecc960ed0 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Wed, 29 Sep 2021 20:23:16 +0530 Subject: [PATCH 08/11] Comments are now added in functions --- play-dl/SoundCloud/classes.ts | 12 ++- play-dl/SoundCloud/index.ts | 53 +++++++++++-- play-dl/Spotify/classes.ts | 106 +++++++++----------------- play-dl/Spotify/index.ts | 56 ++++++++++---- play-dl/YouTube/classes/Channel.ts | 4 +- play-dl/YouTube/classes/LiveStream.ts | 16 ++-- play-dl/YouTube/classes/Playlist.ts | 4 +- play-dl/YouTube/classes/Video.ts | 4 +- play-dl/YouTube/search.ts | 10 ++- play-dl/YouTube/stream.ts | 28 +++++-- play-dl/YouTube/utils/cipher.ts | 37 +++++++-- play-dl/YouTube/utils/extractor.ts | 62 ++++++++++++--- play-dl/YouTube/utils/parser.ts | 37 ++++++--- play-dl/YouTube/utils/request.ts | 39 ++++++++-- play-dl/index.ts | 23 ++++-- 15 files changed, 342 insertions(+), 149 deletions(-) diff --git a/play-dl/SoundCloud/classes.ts b/play-dl/SoundCloud/classes.ts index 4296021..9674af3 100644 --- a/play-dl/SoundCloud/classes.ts +++ b/play-dl/SoundCloud/classes.ts @@ -32,7 +32,9 @@ export interface SoundCloudTrackFormat { }; quality: string; } - +/** + * SoundCloud Track + */ export class SoundCloudTrack { name: string; id: number; @@ -100,7 +102,9 @@ export class SoundCloudTrack { }; } } - +/** + * SoundCloud Playlist + */ export class SoundCloudPlaylist { name: string; id: number; @@ -193,7 +197,9 @@ export class SoundCloudPlaylist { }; } } - +/** + * SoundCloud Stream class + */ export class Stream { stream: PassThrough; type: StreamType; diff --git a/play-dl/SoundCloud/index.ts b/play-dl/SoundCloud/index.ts index 50262b3..1d9c923 100644 --- a/play-dl/SoundCloud/index.ts +++ b/play-dl/SoundCloud/index.ts @@ -13,8 +13,12 @@ interface SoundDataOptions { } const pattern = /^(?:(https?):\/\/)?(?:(?:www|m)\.)?(soundcloud\.com|snd\.sc)\/(.*)$/; - -export async function soundcloud(url: string): Promise { +/** + * Function to get info from a soundcloud url + * @param url soundcloud url + * @returns SoundCloud Track or SoundCloud Playlist + */ +export async function soundcloud(url: string): Promise { if (!soundData) throw new Error('SoundCloud Data is missing\nDid you forgot to do authorization ?'); if (!url.match(pattern)) throw new Error('This is not a SoundCloud URL'); @@ -32,8 +36,17 @@ export async function soundcloud(url: string): Promise { const data = await soundcloud(url); @@ -67,7 +85,16 @@ export async function stream(url: string, quality?: number): Promise { : StreamType.Arbitrary; return new Stream(s_data.url, type); } +/** + * Type for SoundCloud Stream + */ export type SoundCloudStream = Stream; +/** + * Function for creating a Stream of soundcloud using a SoundCloud Track Class + * @param data SoundCloud Track Class + * @param quality Quality to select from + * @returns SoundCloud Stream + */ export async function stream_from_info(data: SoundCloudTrack, quality?: number): Promise { const HLSformats = parseHlsFormats(data.formats); if (typeof quality !== 'number') quality = HLSformats.length - 1; @@ -80,7 +107,11 @@ export async function stream_from_info(data: SoundCloudTrack, quality?: number): : StreamType.Arbitrary; return new Stream(s_data.url, type); } - +/** + * Function to check client ID + * @param id Client ID + * @returns boolean + */ export async function check_id(id: string): Promise { const response = await request(`https://api-v2.soundcloud.com/search?client_id=${id}&q=Rick+Roll&limit=0`).catch( (err: Error) => { @@ -90,7 +121,11 @@ export async function check_id(id: string): Promise { if (response instanceof Error) return false; else return true; } - +/** + * Function to validate for a soundcloud url + * @param url soundcloud url + * @returns "false" | 'track' | 'playlist' + */ export async function so_validate(url: string): Promise { const data = await request( `https://api-v2.soundcloud.com/resolve?url=${url}&client_id=${soundData.client_id}` @@ -103,7 +138,11 @@ export async function so_validate(url: string): Promise { diff --git a/play-dl/Spotify/classes.ts b/play-dl/Spotify/classes.ts index 52540a8..807497a 100644 --- a/play-dl/Spotify/classes.ts +++ b/play-dl/Spotify/classes.ts @@ -26,8 +26,10 @@ interface SpotifyCopyright { text: string; type: string; } - -export class SpotifyVideo { +/** + * Class for Spotify Track + */ +export class SpotifyTrack { name: string; type: 'track' | 'playlist' | 'album'; id: string; @@ -36,8 +38,8 @@ export class SpotifyVideo { durationInSec: number; durationInMs: number; artists: SpotifyArtists[]; - album: SpotifyTrackAlbum; - thumbnail: SpotifyThumbnail; + album: SpotifyTrackAlbum | undefined; + thumbnail: SpotifyThumbnail | undefined; constructor(data: any) { this.name = data.name; this.id = data.id; @@ -55,15 +57,19 @@ export class SpotifyVideo { }); }); this.artists = artists; - this.album = { - name: data.album.name, - url: data.external_urls.spotify, - id: data.album.id, - release_date: data.album.release_date, - release_date_precision: data.album.release_date_precision, - total_tracks: data.album.total_tracks - }; - this.thumbnail = data.album.images[0]; + if (!data.album?.name) this.album = undefined; + else { + this.album = { + name: data.album.name, + url: data.external_urls.spotify, + id: data.album.id, + release_date: data.album.release_date, + release_date_precision: data.album.release_date_precision, + total_tracks: data.album.total_tracks + }; + } + if (!data.album?.images?.[0]) this.thumbnail = undefined; + else this.thumbnail = data.album.images[0]; } toJSON() { @@ -81,7 +87,9 @@ export class SpotifyVideo { }; } } - +/** + * Class for Spotify Playlist + */ export class SpotifyPlaylist { name: string; type: 'track' | 'playlist' | 'album'; @@ -93,7 +101,7 @@ export class SpotifyPlaylist { owner: SpotifyArtists; tracksCount: number; private spotifyData: SpotifyDataOptions; - private fetched_tracks: Map; + private fetched_tracks: Map; constructor(data: any, spotifyData: SpotifyDataOptions) { this.name = data.name; this.type = 'playlist'; @@ -108,9 +116,9 @@ export class SpotifyPlaylist { id: data.owner.id }; this.tracksCount = Number(data.tracks.total); - const videos: SpotifyVideo[] = []; + const videos: SpotifyTrack[] = []; data.tracks.items.forEach((v: any) => { - videos.push(new SpotifyVideo(v.track)); + videos.push(new SpotifyTrack(v.track)); }); this.fetched_tracks = new Map(); this.fetched_tracks.set('1', videos); @@ -136,11 +144,11 @@ export class SpotifyPlaylist { } } ).catch((err) => reject(`Response Error : \n${err}`)); - const videos: SpotifyVideo[] = []; + const videos: SpotifyTrack[] = []; if (typeof response !== 'string') return; const json_data = JSON.parse(response); json_data.items.forEach((v: any) => { - videos.push(new SpotifyVideo(v.track)); + videos.push(new SpotifyTrack(v.track)); }); this.fetched_tracks.set(`${i}`, videos); resolve('Success'); @@ -163,7 +171,7 @@ export class SpotifyPlaylist { get total_tracks() { const page_number: number = this.total_pages; - return (page_number - 1) * 100 + (this.fetched_tracks.get(`${page_number}`) as SpotifyVideo[]).length; + return (page_number - 1) * 100 + (this.fetched_tracks.get(`${page_number}`) as SpotifyTrack[]).length; } toJSON() { @@ -179,7 +187,9 @@ export class SpotifyPlaylist { }; } } - +/** + * Class for Spotify Album + */ export class SpotifyAlbum { name: string; type: 'track' | 'playlist' | 'album'; @@ -192,7 +202,7 @@ export class SpotifyAlbum { release_date_precision: string; trackCount: number; private spotifyData: SpotifyDataOptions; - private fetched_tracks: Map; + private fetched_tracks: Map; constructor(data: any, spotifyData: SpotifyDataOptions) { this.name = data.name; this.type = 'album'; @@ -212,9 +222,9 @@ export class SpotifyAlbum { this.release_date = data.release_date; this.release_date_precision = data.release_date_precision; this.trackCount = data.total_tracks; - const videos: SpotifyTracks[] = []; + const videos: SpotifyTrack[] = []; data.tracks.items.forEach((v: any) => { - videos.push(new SpotifyTracks(v)); + videos.push(new SpotifyTrack(v)); }); this.fetched_tracks = new Map(); this.fetched_tracks.set('1', videos); @@ -240,11 +250,11 @@ export class SpotifyAlbum { } } ).catch((err) => reject(`Response Error : \n${err}`)); - const videos: SpotifyTracks[] = []; + const videos: SpotifyTrack[] = []; if (typeof response !== 'string') return; const json_data = JSON.parse(response); json_data.items.forEach((v: any) => { - videos.push(new SpotifyTracks(v)); + videos.push(new SpotifyTrack(v)); }); this.fetched_tracks.set(`${i}`, videos); resolve('Success'); @@ -267,7 +277,7 @@ export class SpotifyAlbum { get total_tracks() { const page_number: number = this.total_pages; - return (page_number - 1) * 100 + (this.fetched_tracks.get(`${page_number}`) as SpotifyVideo[]).length; + return (page_number - 1) * 100 + (this.fetched_tracks.get(`${page_number}`) as SpotifyTrack[]).length; } toJSON() { @@ -284,45 +294,3 @@ export class SpotifyAlbum { }; } } - -class SpotifyTracks { - name: string; - type: 'track' | 'playlist' | 'album'; - id: string; - url: string; - explicit: boolean; - durationInSec: number; - durationInMs: number; - artists: SpotifyArtists[]; - constructor(data: any) { - this.name = data.name; - this.id = data.id; - this.type = 'track'; - this.url = data.external_urls.spotify; - this.explicit = data.explicit; - this.durationInMs = data.duration_ms; - this.durationInSec = Math.round(this.durationInMs / 1000); - const artists: SpotifyArtists[] = []; - data.artists.forEach((v: any) => { - artists.push({ - name: v.name, - id: v.id, - url: v.external_urls.spotify - }); - }); - this.artists = artists; - } - - toJSON() { - return { - name: this.name, - id: this.id, - type: this.type, - url: this.url, - explicit: this.explicit, - durationInMs: this.durationInMs, - durationInSec: this.durationInSec, - artists: this.artists - }; - } -} diff --git a/play-dl/Spotify/index.ts b/play-dl/Spotify/index.ts index fee1362..cd2f7d5 100644 --- a/play-dl/Spotify/index.ts +++ b/play-dl/Spotify/index.ts @@ -1,12 +1,14 @@ import { request } from '../YouTube/utils/request'; -import { SpotifyAlbum, SpotifyPlaylist, SpotifyVideo } from './classes'; +import { SpotifyAlbum, SpotifyPlaylist, SpotifyTrack } from './classes'; import fs from 'fs'; let spotifyData: SpotifyDataOptions; if (fs.existsSync('.data/spotify.data')) { spotifyData = JSON.parse(fs.readFileSync('.data/spotify.data').toString()); } - +/** + * Spotify Data options that are stored in spotify.data file. + */ export interface SpotifyDataOptions { client_id: string; client_secret: string; @@ -21,8 +23,12 @@ export interface SpotifyDataOptions { } const pattern = /^((https:)?\/\/)?open.spotify.com\/(track|album|playlist)\//; - -export async function spotify(url: string): Promise { +/** + * Function to get Playlist | Album | Track + * @param url url of spotify from which you want info + * @returns Spotify type. + */ +export async function spotify(url: string): Promise { if (!spotifyData) throw new Error('Spotify Data is missing\nDid you forgot to do authorization ?'); if (!url.match(pattern)) throw new Error('This is not a Spotify URL'); if (url.indexOf('track/') !== -1) { @@ -35,7 +41,7 @@ export async function spotify(url: string): Promise { const response = await request(`https://accounts.spotify.com/api/token`, { headers: { @@ -104,20 +118,31 @@ export async function SpotifyAuthorize(data: SpotifyDataOptions): Promise= (spotifyData.expiry as number)) return true; else return false; } - -export type Spotify = SpotifyAlbum | SpotifyPlaylist | SpotifyVideo; - +/** + * type for Spotify Class + */ +export type Spotify = SpotifyAlbum | SpotifyPlaylist | SpotifyTrack; +/** + * Function for searching songs on Spotify + * @param query searching query + * @param type "album" | "playlist" | "track" + * @param limit max no of results + * @returns Spotify type. + */ export async function sp_search( query: string, type: 'album' | 'playlist' | 'track', limit: number = 10 ): Promise { - const results: (SpotifyAlbum | SpotifyPlaylist | SpotifyVideo)[] = []; + const results: Spotify[] = []; if (!spotifyData) throw new Error('Spotify Data is missing\nDid you forgot to do authorization ?'); if (query.length === 0) throw new Error('Pass some query to search.'); if (limit > 50 || limit < 0) throw new Error(`You crossed limit range of Spotify [ 0 - 50 ]`); @@ -137,7 +162,7 @@ export async function sp_search( const json_data = JSON.parse(response); if (type === 'track') { json_data.tracks.items.forEach((track: any) => { - results.push(new SpotifyVideo(track)); + results.push(new SpotifyTrack(track)); }); } else if (type === 'album') { json_data.albums.items.forEach((album: any) => { @@ -150,7 +175,10 @@ export async function sp_search( } return results; } - +/** + * Function to refresh Token + * @returns boolean to check whether token is refreshed or not + */ export async function refreshToken(): Promise { const response = await request(`https://accounts.spotify.com/api/token`, { headers: { diff --git a/play-dl/YouTube/classes/Channel.ts b/play-dl/YouTube/classes/Channel.ts index 2f9520a..5891a69 100644 --- a/play-dl/YouTube/classes/Channel.ts +++ b/play-dl/YouTube/classes/Channel.ts @@ -3,7 +3,9 @@ export interface ChannelIconInterface { width: number; height: number; } - +/** + * Class for YouTube Channel url + */ export class YouTubeChannel { name?: string; verified?: boolean; diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index 534182e..202295a 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -1,7 +1,7 @@ import { PassThrough } from 'stream'; import { IncomingMessage } from 'http'; -import { parseAudioFormats, StreamType } from '../stream'; -import { request, request_stream } from '../utils/request'; +import { parseAudioFormats, StreamOptions, StreamType } from '../stream'; +import { Proxy, request, request_stream } from '../utils/request'; import { video_info } from '..'; export interface FormatInterface { @@ -119,7 +119,9 @@ export class LiveStreaming { }, this.interval); } } - +/** + * Class for YouTube Stream + */ export class Stream { stream: PassThrough; type: StreamType; @@ -133,6 +135,7 @@ export class Stream { private data_ended: boolean; private playing_count: number; private quality: number; + private proxy: Proxy[]; private request: IncomingMessage | null; constructor( url: string, @@ -141,11 +144,12 @@ export class Stream { contentLength: number, video_url: string, cookie: string, - quality: number + options: StreamOptions ) { this.stream = new PassThrough({ highWaterMark: 10 * 1000 * 1000 }); this.url = url; - this.quality = quality; + this.quality = options.quality as number; + this.proxy = options.proxy || []; this.type = type; this.bytes_count = 0; this.video_url = video_url; @@ -177,7 +181,7 @@ export class Stream { } private async retry() { - const info = await video_info(this.video_url, { cookie: this.cookie }); + const info = await video_info(this.video_url, { cookie: this.cookie, proxy: this.proxy }); const audioFormat = parseAudioFormats(info.format); this.url = audioFormat[this.quality].url; } diff --git a/play-dl/YouTube/classes/Playlist.ts b/play-dl/YouTube/classes/Playlist.ts index b51c710..ee07455 100644 --- a/play-dl/YouTube/classes/Playlist.ts +++ b/play-dl/YouTube/classes/Playlist.ts @@ -3,7 +3,9 @@ import { request } from '../utils/request'; import { YouTubeChannel } from './Channel'; import { YouTubeVideo } from './Video'; const BASE_API = 'https://www.youtube.com/youtubei/v1/browse?key='; - +/** + * Class for YouTube Playlist url + */ export class YouTubePlayList { id?: string; title?: string; diff --git a/play-dl/YouTube/classes/Video.ts b/play-dl/YouTube/classes/Video.ts index 59f30a5..d52b9f5 100644 --- a/play-dl/YouTube/classes/Video.ts +++ b/play-dl/YouTube/classes/Video.ts @@ -29,7 +29,9 @@ interface VideoOptions { private: boolean; tags: string[]; } - +/** + * Class for YouTube Video url + */ export class YouTubeVideo { id?: string; url: string; diff --git a/play-dl/YouTube/search.ts b/play-dl/YouTube/search.ts index adc9128..7446ba9 100644 --- a/play-dl/YouTube/search.ts +++ b/play-dl/YouTube/search.ts @@ -10,8 +10,16 @@ enum SearchType { Channel = 'EgIQAg%253D%253D' } +/** + * Type for YouTube returns + */ export type YouTube = YouTubeVideo | YouTubeChannel | YouTubePlayList; - +/** + * Command to search from YouTube + * @param search The query to search + * @param options limit & type of YouTube search you want. + * @returns YouTube type. + */ export async function yt_search(search: string, options: ParseSearchInterface = {}): Promise { let url = 'https://www.youtube.com/results?search_query=' + search.replaceAll(' ', '+'); options.type ??= 'video'; diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index 6953d26..c87f683 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -26,7 +26,11 @@ export interface InfoData { format: any[]; video_details: any; } - +/** + * Command to find audio formats from given format array + * @param formats Formats to search from + * @returns Audio Formats array + */ export function parseAudioFormats(formats: any[]) { const result: any[] = []; formats.forEach((format) => { @@ -39,9 +43,16 @@ export function parseAudioFormats(formats: any[]) { }); return result; } - +/** + * Type for YouTube Stream + */ export type YouTubeStream = Stream | LiveStreaming; - +/** + * Stream command for YouTube + * @param url YouTube URL + * @param options lets you add quality, cookie, proxy support for stream + * @returns Stream class with type and stream for playing. + */ export async function stream(url: string, options: StreamOptions = {}): Promise { const info = await video_info(url, { cookie: options.cookie, proxy: options.proxy }); const final: any[] = []; @@ -73,10 +84,15 @@ export async function stream(url: string, options: StreamOptions = {}): Promise< Number(final[0].contentLength), info.video_details.url, options.cookie as string, - options.quality + options ); } - +/** + * Stream command for YouTube using info from video_info function. + * @param info video_info data + * @param options lets you add quality, cookie, proxy support for stream + * @returns Stream class with type and stream for playing. + */ export async function stream_from_info(info: InfoData, options: StreamOptions = {}): Promise { const final: any[] = []; if ( @@ -107,6 +123,6 @@ export async function stream_from_info(info: InfoData, options: StreamOptions = Number(final[0].contentLength), info.video_details.url, options.cookie as string, - options.quality + options ); } diff --git a/play-dl/YouTube/utils/cipher.ts b/play-dl/YouTube/utils/cipher.ts index b9f63cb..dcb6880 100644 --- a/play-dl/YouTube/utils/cipher.ts +++ b/play-dl/YouTube/utils/cipher.ts @@ -9,6 +9,7 @@ interface formatOptions { cipher?: string; s?: string; } +// RegExp for various js functions const var_js = '[a-zA-Z_\\$][a-zA-Z_0-9]*'; const singlequote_js = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`; const duoblequote_js = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`; @@ -37,7 +38,11 @@ const reverse_regexp = new RegExp(`(?:^|,)(${key_js})${reverse_function}`, 'm'); const slice_regexp = new RegExp(`(?:^|,)(${key_js})${slice_function}`, 'm'); const splice_regexp = new RegExp(`(?:^|,)(${key_js})${splice_function}`, 'm'); const swap_regexp = new RegExp(`(?:^|,)(${key_js})${swap_function}`, 'm'); - +/** + * Function to get tokens from html5player body data. + * @param body body data of html5player. + * @returns Array of tokens. + */ export function js_tokens(body: string) { const function_action = function_regexp.exec(body); const object_action = obj_regexp.exec(body); @@ -82,7 +87,12 @@ export function js_tokens(body: string) { } return tokens; } - +/** + * Function to decipher signature + * @param tokens Tokens from js_tokens function + * @param signature Signatured format url + * @returns deciphered signature + */ function deciper_signature(tokens: string[], signature: string) { let sig = signature.split(''); const len = tokens.length; @@ -109,14 +119,24 @@ function deciper_signature(tokens: string[], signature: string) { } return sig.join(''); } - +/** + * 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 + * @param format video fomat + * @param sig deciphered signature + * @returns void + */ function download_url(format: formatOptions, sig: string) { let decoded_url; if (!format.url) return; @@ -132,8 +152,13 @@ function download_url(format: formatOptions, sig: string) { } format.url = parsed_url.toString(); } - -export async function format_decipher(formats: formatOptions[], html5player: string) { +/** + * Main function which handles all queries related to video format deciphering + * @param formats video formats + * @param html5player url of html5player + * @returns array of format. + */ +export async function format_decipher(formats: formatOptions[], html5player: string): Promise { const body = await request(html5player); const tokens = js_tokens(body); formats.forEach((format) => { diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index ed72797..e8eb971 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -17,7 +17,11 @@ const DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'; const video_pattern = /^((?:https?:)?\/\/)?(?:(?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; const playlist_pattern = /^((?:https?:)?\/\/)?(?:(?:www|m)\.)?(youtube\.com)\/(?:(playlist|watch))(.*)?((\?|\&)list=)/; - +/** + * Command to validate a YouTube url + * @param url Url for validation + * @returns type of url or false. + */ export function yt_validate(url: string): 'playlist' | 'video' | false { if (url.indexOf('list=') === -1) { if (!url.match(video_pattern)) return false; @@ -31,7 +35,11 @@ export function yt_validate(url: string): 'playlist' | 'video' | false { return 'playlist'; } } - +/** + * Function to extract ID of YouTube url. + * @param url ID or url of YouTube + * @returns ID of video or playlist. + */ export function extractID(url: string): string { if (url.startsWith('https')) { if (url.indexOf('list=') === -1) { @@ -45,7 +53,12 @@ export function extractID(url: string): string { } } else return url; } - +/** + * Basic function to get data from a YouTube url or ID. + * @param url YouTube url or ID + * @param options cookie and proxy parameters to add + * @returns Data containing video_details, LiveStreamData and formats of video url. + */ export async function video_basic_info(url: string, options: InfoOptions = {}) { let video_id: string; if (url.startsWith('https')) { @@ -123,7 +136,11 @@ export async function video_basic_info(url: string, options: InfoOptions = {}) { related_videos: related }; } - +/** + * Function to convert seconds to [hour : minutes : seconds] format + * @param seconds seconds to convert + * @returns [hour : minutes : seconds] format + */ function parseSeconds(seconds: number): string { const d = Number(seconds); const h = Math.floor(d / 3600); @@ -135,7 +152,12 @@ function parseSeconds(seconds: number): string { const sDisplay = s > 0 ? (s < 10 ? `0${s}` : s) : '00'; return hDisplay + mDisplay + sDisplay; } - +/** + * Function which gets data from video_basic_info and deciphers it if it contains signatures. + * @param url YouTube Video URL + * @param options cookie and proxy parameters to add + * @returns Data containing video_details, LiveStreamData and formats of video url. + */ export async function video_info(url: string, options: InfoOptions = {}) { const data = await video_basic_info(url, options); if (data.LiveStreamData.isLive === true && data.LiveStreamData.hlsManifestUrl !== null) { @@ -147,8 +169,13 @@ export async function video_info(url: string, options: InfoOptions = {}) { return data; } } - -export async function playlist_info(url: string, options: PlaylistOptions = {}) { +/** + * Function to get YouTube playlist info from a playlist url. + * @param url Playlist URL + * @param options incomplete and proxy to add. + * @returns YouTube Playlist + */ +export async function playlist_info(url: string, options: PlaylistOptions = {}): Promise { if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`); let Playlist_id: string; if (url.startsWith('https')) { @@ -184,7 +211,7 @@ export async function playlist_info(url: string, options: PlaylistOptions = {}) const videos = getPlaylistVideos(parsed, 100); const data = playlistDetails[0].playlistSidebarPrimaryInfoRenderer; - if (!data.title.runs || !data.title.runs.length) return undefined; + 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; @@ -234,7 +261,12 @@ export async function playlist_info(url: string, options: PlaylistOptions = {}) }); return res; } - +/** + * 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 = []; @@ -270,7 +302,11 @@ export function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] { } return videos; } - +/** + * Function to convert [hour : minutes : seconds] format to seconds + * @param duration hour : minutes : seconds format + * @returns seconds + */ function parseDuration(duration: string): number { duration ??= '0:00'; const args = duration.split(':'); @@ -289,7 +325,11 @@ function parseDuration(duration: string): number { return dur; } - +/** + * Function to get Continuation Token + * @param data html data of playlist url + * @returns token + */ export function getContinuationToken(data: any): string { const continuationToken = data.find((x: any) => Object.keys(x)[0] === 'continuationItemRenderer') ?.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token; diff --git a/play-dl/YouTube/utils/parser.ts b/play-dl/YouTube/utils/parser.ts index e02797f..b1499f5 100644 --- a/play-dl/YouTube/utils/parser.ts +++ b/play-dl/YouTube/utils/parser.ts @@ -1,6 +1,7 @@ import { YouTubeVideo } from '../classes/Video'; import { YouTubePlayList } from '../classes/Playlist'; import { YouTubeChannel } from '../classes/Channel'; +import { YouTube } from '..'; export interface ParseSearchInterface { type?: 'video' | 'playlist' | 'channel'; @@ -12,11 +13,13 @@ export interface thumbnail { height: string; url: string; } - -export function ParseSearchResult( - html: string, - options?: ParseSearchInterface -): (YouTubeVideo | YouTubePlayList | YouTubeChannel)[] { +/** + * Main command which converts html body data and returns the type of data requested. + * @param html body of that request + * @param options limit & type of YouTube search you want. + * @returns Array of one of YouTube type. + */ +export function ParseSearchResult(html: string, options?: ParseSearchInterface): YouTube[] { if (!html) throw new Error("Can't parse Search result without data"); if (!options) options = { type: 'video', limit: 0 }; if (!options.type) options.type = 'video'; @@ -45,7 +48,11 @@ export function ParseSearchResult( } return results; } - +/** + * Function to convert [hour : minutes : seconds] format to seconds + * @param duration hour : minutes : seconds format + * @returns seconds + */ function parseDuration(duration: string): number { duration ??= '0:00'; const args = duration.split(':'); @@ -64,7 +71,11 @@ function parseDuration(duration: string): number { return dur; } - +/** + * Function to parse Channel searches + * @param data body of that channel request. + * @returns YouTubeChannel class + */ export function parseChannel(data?: any): YouTubeChannel { if (!data || !data.channelRenderer) throw new Error('Failed to Parse YouTube Channel'); const badge = data.channelRenderer.ownerBadges && data.channelRenderer.ownerBadges[0]; @@ -93,7 +104,11 @@ export function parseChannel(data?: any): YouTubeChannel { return res; } - +/** + * Function to parse Video searches + * @param data body of that video request. + * @returns YouTubeVideo class + */ export function parseVideo(data?: any): YouTubeVideo { if (!data || !data.videoRenderer) throw new Error('Failed to Parse YouTube Video'); @@ -133,7 +148,11 @@ export function parseVideo(data?: any): YouTubeVideo { return res; } - +/** + * Function to parse Playlist searches + * @param data body of that playlist request. + * @returns YouTubePlaylist class + */ export function parsePlaylist(data?: any): YouTubePlayList { if (!data.playlistRenderer) throw new Error('Failed to Parse YouTube Playlist'); diff --git a/play-dl/YouTube/utils/request.ts b/play-dl/YouTube/utils/request.ts index 8a17852..f2735f2 100644 --- a/play-dl/YouTube/utils/request.ts +++ b/play-dl/YouTube/utils/request.ts @@ -2,7 +2,9 @@ import https, { RequestOptions } from 'https'; import tls from 'tls'; import http, { ClientRequest, IncomingMessage } from 'http'; import { URL } from 'url'; - +/** + * Types for Proxy + */ export type Proxy = ProxyOpts | string; interface ProxyOpts { @@ -25,7 +27,12 @@ interface RequestOpts extends RequestOptions { method?: 'GET' | 'POST'; proxies?: Proxy[]; } - +/** + * Main module that play-dl uses for making a https request + * @param req_url URL to make https request to + * @param options Request options for https request + * @returns Incoming Message from the https request + */ function https_getter(req_url: string, options: RequestOpts = {}): Promise { return new Promise((resolve, reject) => { const s = new URL(req_url); @@ -45,13 +52,23 @@ function https_getter(req_url: string, options: RequestOpts = {}): Promise { return new Promise((resolve, reject) => { const proxy: string | ProxyOpts = req_proxy[randomIntFromInterval(0, req_proxy.length)]; @@ -127,7 +144,12 @@ async function proxy_getter(req_url: string, req_proxy: Proxy[]): Promise { return new Promise(async (resolve, reject) => { if (!options?.proxies) { @@ -160,7 +182,12 @@ export async function request(url: string, options?: RequestOpts): Promise { return new Promise(async (resolve, reject) => { let res = await https_getter(url, options).catch((err: Error) => err); diff --git a/play-dl/index.ts b/play-dl/index.ts index 32f7fa5..16ff171 100644 --- a/play-dl/index.ts +++ b/play-dl/index.ts @@ -32,7 +32,7 @@ import { yt_search } from './YouTube/search'; /** * Main stream Command for streaming through various sources * @param url The video / track url to make stream of - * @param options contains quality and cookie to set for stream + * @param options contains quality, cookie and proxy to set for stream * @returns YouTube / SoundCloud Stream to play */ @@ -46,7 +46,7 @@ export async function stream(url: string, options: StreamOptions = {}): Promise< * Main Search Command for searching through various sources * @param query string to search. * @param options contains limit and source to choose. - * @returns + * @returns Array of YouTube or Spotify or SoundCloud */ export async function search( query: string, @@ -61,10 +61,11 @@ export async function search( } /** - * Command to be used - * @param info - * @param options - * @returns + * stream Command for streaming through various sources using data from video_info or soundcloud + * SoundCloud Track is only supported + * @param info video_info data or SoundCloud Track data. + * @param options contains quality, cookie and proxy to set for stream + * @returns YouTube / SoundCloud Stream to play */ export async function stream_from_info( info: InfoData | SoundCloudTrack, @@ -73,7 +74,11 @@ export async function stream_from_info( if (info instanceof SoundCloudTrack) return await so_stream_info(info, options.quality); else return await yt_stream_info(info, options); } - +/** + * Command to validate the provided url. It checks whether it supports play-dl or not. + * @param url url to validate + * @returns On failure, returns false else type of url. + */ export async function validate( url: string ): Promise<'so_playlist' | 'so_track' | 'sp_track' | 'sp_album' | 'sp_playlist' | 'yt_video' | 'yt_playlist' | false> { @@ -89,7 +94,9 @@ export async function validate( return check !== false ? (('yt_' + check) as 'yt_video' | 'yt_playlist') : false; } } - +/** + * Authorization interface for Spotify and SoundCloud. + */ export function authorization(): void { const ask = readline.createInterface({ input: process.stdin, From 704ddd303ad37a2db7fb1dedbddb6ad74dba4a8c Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Fri, 1 Oct 2021 10:30:39 +0530 Subject: [PATCH 09/11] has verified parameter added --- play-dl/YouTube/utils/extractor.ts | 2 +- play-dl/YouTube/utils/parser.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index e8eb971..cda92e7 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -65,7 +65,7 @@ export async function video_basic_info(url: string, options: InfoOptions = {}) { if (yt_validate(url) !== 'video') throw new Error('This is not a YouTube Watch URL'); video_id = extractID(url); } else video_id = url; - const new_url = `https://www.youtube.com/watch?v=${video_id}`; + const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`; const body = await request(new_url, { proxies: options.proxy ?? [], headers: options.cookie diff --git a/play-dl/YouTube/utils/parser.ts b/play-dl/YouTube/utils/parser.ts index b1499f5..39e0b17 100644 --- a/play-dl/YouTube/utils/parser.ts +++ b/play-dl/YouTube/utils/parser.ts @@ -173,9 +173,9 @@ export function parsePlaylist(data?: any): YouTubePlayList { ].width }, channel: { - id: data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId, - name: data.playlistRenderer.shortBylineText.runs[0].text, - url: `https://www.youtube.com${data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` + id: data.playlistRenderer.shortBylineText.runs?.[0].navigationEndpoint.browseEndpoint.browseId, + name: data.playlistRenderer.shortBylineText.runs?.[0].text, + url: `https://www.youtube.com${data.playlistRenderer.shortBylineText.runs?.[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}` }, videos: parseInt(data.playlistRenderer.videoCount.replace(/[^0-9]/g, '')) }, From 4f49d16b16fdb0cac1cbfbd030fe87da9750a7a0 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Fri, 1 Oct 2021 11:14:25 +0530 Subject: [PATCH 10/11] SoundCloud API Url fixed --- play-dl/SoundCloud/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-dl/SoundCloud/index.ts b/play-dl/SoundCloud/index.ts index 1d9c923..7c59028 100644 --- a/play-dl/SoundCloud/index.ts +++ b/play-dl/SoundCloud/index.ts @@ -12,7 +12,7 @@ interface SoundDataOptions { client_id: string; } -const pattern = /^(?:(https?):\/\/)?(?:(?:www|m)\.)?(soundcloud\.com|snd\.sc)\/(.*)$/; +const pattern = /^(?:(https?):\/\/)?(?:(?:www|m)\.)?(api\.soundcloud\.com|soundcloud\.com|snd\.sc)\/(.*)$/; /** * Function to get info from a soundcloud url * @param url soundcloud url From 144b247cc93dc0bb4edbc2e607c9673392157cc2 Mon Sep 17 00:00:00 2001 From: killer069 <65385476+killer069@users.noreply.github.com> Date: Fri, 1 Oct 2021 11:25:53 +0530 Subject: [PATCH 11/11] Removed 2 hour timer from YT Stream --- play-dl/YouTube/classes/LiveStream.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index 202295a..805613f 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -130,7 +130,6 @@ export class Stream { private per_sec_bytes: number; private content_length: number; private video_url: string; - private timer: NodeJS.Timer | null; private cookie: string; private data_ended: boolean; private playing_count: number; @@ -154,9 +153,6 @@ export class Stream { this.bytes_count = 0; this.video_url = video_url; this.cookie = cookie; - this.timer = setInterval(() => { - this.retry(); - }, 7200 * 1000); this.per_sec_bytes = Math.ceil(contentLength / duration); this.content_length = contentLength; this.request = null; @@ -187,10 +183,8 @@ export class Stream { } private cleanup() { - clearInterval(this.timer as NodeJS.Timer); this.request?.unpipe(this.stream); this.request?.destroy(); - this.timer = null; this.request = null; this.url = ''; } @@ -218,11 +212,6 @@ export class Stream { this.cleanup(); await this.retry(); this.loop(); - if (!this.timer) { - this.timer = setInterval(() => { - this.retry(); - }, 7200 * 1000); - } return; } this.request = stream; @@ -232,11 +221,6 @@ export class Stream { this.cleanup(); await this.retry(); this.loop(); - if (!this.timer) { - this.timer = setInterval(() => { - this.retry(); - }, 7200 * 1000); - } }); stream.on('data', (chunk: any) => {