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] 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; } }