From 89840784432f5cfe4ee7d0045955a68d0ce1cae3 Mon Sep 17 00:00:00 2001 From: Histmy Date: Tue, 20 Aug 2024 20:58:30 +0200 Subject: [PATCH] =?UTF-8?q?Vylep=C5=A1en=C3=AD=C4=8Dko?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- dist/index.d.mts | 2869 ++++++++++++++++++++++++++++++++++++++++++++ dist/index.d.ts | 2869 ++++++++++++++++++++++++++++++++++++++++++++ dist/index.js | 22 + dist/index.js.map | 1 + dist/index.mjs | 22 + dist/index.mjs.map | 1 + package-lock.json | 32 +- package.json | 3 +- 9 files changed, 5804 insertions(+), 18 deletions(-) create mode 100644 dist/index.d.mts create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/index.js.map create mode 100644 dist/index.mjs create mode 100644 dist/index.mjs.map diff --git a/.gitignore b/.gitignore index 2c8dbd3..f0e2ae0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ node_modules/ -dist/ .npmrc examples/node_modules examples/package-lock.json examples/package.json -docs/ \ No newline at end of file +docs/ diff --git a/dist/index.d.mts b/dist/index.d.mts new file mode 100644 index 0000000..246967d --- /dev/null +++ b/dist/index.d.mts @@ -0,0 +1,2869 @@ +import { Readable, Duplex, DuplexOptions } from 'node:stream'; +import { WebmHeader } from 'play-audio'; +import { EventEmitter } from 'stream'; + +/** + * YouTube Live Stream class for playing audio from Live Stream videos. + */ +declare class LiveStream { + /** + * Readable Stream through which data passes + */ + stream: Readable; + /** + * Type of audio data that we recieved from live stream youtube url. + */ + type: StreamType; + /** + * Incoming message that we recieve. + * + * Storing this is essential. + * This helps to destroy the TCP connection completely if you stopped player in between the stream + */ + private request?; + /** + * Timer that creates loop from interval time provided. + */ + private normal_timer?; + /** + * Timer used to update dash url so as to avoid 404 errors after long hours of streaming. + * + * It updates dash_url every 30 minutes. + */ + private dash_timer; + /** + * Given Dash URL. + */ + private dash_url; + /** + * Base URL in dash manifest file. + */ + private base_url; + /** + * Interval to fetch data again to dash url. + */ + private interval; + /** + * Timer used to update dash url so as to avoid 404 errors after long hours of streaming. + * + * It updates dash_url every 30 minutes. + */ + private video_url; + /** + * No of segments of data to add in stream before starting to loop + */ + private precache; + /** + * Segment sequence number + */ + private sequence; + /** + * Live Stream Class Constructor + * @param dash_url dash manifest URL + * @param target_interval interval time for fetching dash data again + * @param video_url Live Stream video url. + */ + constructor(dash_url: string, interval: number, video_url: string, precache?: number); + /** + * This cleans every used variable in class. + * + * This is used to prevent re-use of this class and helping garbage collector to collect it. + */ + private cleanup; + /** + * Updates dash url. + * + * Used by dash_timer for updating dash_url every 30 minutes. + */ + private dash_updater; + /** + * Initializes dash after getting dash url. + * + * Start if it is first time of initialishing dash function. + */ + private initialize_dash; + /** + * Used only after initializing dash function first time. + * @param len Length of data that you want to + */ + private first_data; + /** + * This loops function in Live Stream Class. + * + * Gets next segment and push it. + */ + private loop; + /** + * Deprecated Functions + */ + pause(): void; + /** + * Deprecated Functions + */ + resume(): void; +} +/** + * YouTube Stream Class for playing audio from normal videos. + */ +declare class Stream { + /** + * Readable Stream through which data passes + */ + stream: Readable; + /** + * Type of audio data that we recieved from normal youtube url. + */ + type: StreamType; + /** + * Audio Endpoint Format Url to get data from. + */ + private url; + /** + * Used to calculate no of bytes data that we have recieved + */ + private bytes_count; + /** + * Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds) + */ + private per_sec_bytes; + /** + * Total length of audio file in bytes + */ + private content_length; + /** + * YouTube video url. [ Used only for retrying purposes only. ] + */ + private video_url; + /** + * Timer for looping data every 265 seconds. + */ + private timer; + /** + * Quality given by user. [ Used only for retrying purposes only. ] + */ + private quality; + /** + * Incoming message that we recieve. + * + * Storing this is essential. + * This helps to destroy the TCP connection completely if you stopped player in between the stream + */ + private request; + /** + * YouTube Stream Class constructor + * @param url Audio Endpoint url. + * @param type Type of Stream + * @param duration Duration of audio playback [ in seconds ] + * @param contentLength Total length of Audio file in bytes. + * @param video_url YouTube video url. + * @param options Options provided to stream function. + */ + constructor(url: string, type: StreamType, duration: number, contentLength: number, video_url: string, options: StreamOptions); + /** + * Retry if we get 404 or 403 Errors. + */ + private retry; + /** + * This cleans every used variable in class. + * + * This is used to prevent re-use of this class and helping garbage collector to collect it. + */ + private cleanup; + /** + * Getting data from audio endpoint url and passing it to stream. + * + * If 404 or 403 occurs, it will retry again. + */ + private loop; + /** + * Pauses timer. + * Stops running of loop. + * + * Useful if you don't want to get excess data to be stored in stream. + */ + pause(): void; + /** + * Resumes timer. + * Starts running of loop. + */ + resume(): void; +} + +declare enum WebmSeekerState { + READING_HEAD = "READING_HEAD", + READING_DATA = "READING_DATA" +} +interface WebmSeekerOptions extends DuplexOptions { + mode?: 'precise' | 'granular'; +} +declare class WebmSeeker extends Duplex { + remaining?: Buffer; + state: WebmSeekerState; + chunk?: Buffer; + cursor: number; + header: WebmHeader; + headfound: boolean; + headerparsed: boolean; + seekfound: boolean; + private data_size; + private offset; + private data_length; + private sec; + private time; + constructor(sec: number, options: WebmSeekerOptions); + private get vint_length(); + private vint_value; + cleanup(): void; + _read(): void; + seek(content_length: number): Error | number; + _write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void; + private readHead; + private readTag; + private getClosestBlock; + private parseEbmlID; + _destroy(error: Error | null, callback: (error: Error | null) => void): void; + _final(callback: (error?: Error | null) => void): void; +} + +/** + * YouTube Stream Class for seeking audio to a timeStamp. + */ +declare class SeekStream { + /** + * WebmSeeker Stream through which data passes + */ + stream: WebmSeeker; + /** + * Type of audio data that we recieved from normal youtube url. + */ + type: StreamType; + /** + * Audio Endpoint Format Url to get data from. + */ + private url; + /** + * Used to calculate no of bytes data that we have recieved + */ + private bytes_count; + /** + * Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds) + */ + private per_sec_bytes; + /** + * Length of the header in bytes + */ + private header_length; + /** + * Total length of audio file in bytes + */ + private content_length; + /** + * YouTube video url. [ Used only for retrying purposes only. ] + */ + private video_url; + /** + * Timer for looping data every 265 seconds. + */ + private timer; + /** + * Quality given by user. [ Used only for retrying purposes only. ] + */ + private quality; + /** + * Incoming message that we recieve. + * + * Storing this is essential. + * This helps to destroy the TCP connection completely if you stopped player in between the stream + */ + private request; + /** + * YouTube Stream Class constructor + * @param url Audio Endpoint url. + * @param type Type of Stream + * @param duration Duration of audio playback [ in seconds ] + * @param headerLength Length of the header in bytes. + * @param contentLength Total length of Audio file in bytes. + * @param bitrate Bitrate provided by YouTube. + * @param video_url YouTube video url. + * @param options Options provided to stream function. + */ + constructor(url: string, duration: number, headerLength: number, contentLength: number, bitrate: number, video_url: string, options: StreamOptions); + /** + * **INTERNAL Function** + * + * Uses stream functions to parse Webm Head and gets Offset byte to seek to. + * @returns Nothing + */ + private seek; + /** + * Retry if we get 404 or 403 Errors. + */ + private retry; + /** + * This cleans every used variable in class. + * + * This is used to prevent re-use of this class and helping garbage collector to collect it. + */ + private cleanup; + /** + * Getting data from audio endpoint url and passing it to stream. + * + * If 404 or 403 occurs, it will retry again. + */ + private loop; + /** + * Pauses timer. + * Stops running of loop. + * + * Useful if you don't want to get excess data to be stored in stream. + */ + pause(): void; + /** + * Resumes timer. + * Starts running of loop. + */ + resume(): void; +} + +interface ChannelIconInterface { + /** + * YouTube Channel Icon URL + */ + url: string; + /** + * YouTube Channel Icon Width + */ + width: number; + /** + * YouTube Channel Icon Height + */ + height: number; +} +/** + * YouTube Channel Class + */ +declare class YouTubeChannel { + /** + * YouTube Channel Title + */ + name?: string; + /** + * YouTube Channel Verified status. + */ + verified?: boolean; + /** + * YouTube Channel artist if any. + */ + artist?: boolean; + /** + * YouTube Channel ID. + */ + id?: string; + /** + * YouTube Class type. == "channel" + */ + type: 'video' | 'playlist' | 'channel'; + /** + * YouTube Channel Url + */ + url?: string; + /** + * YouTube Channel Icons data. + */ + icons?: ChannelIconInterface[]; + /** + * YouTube Channel subscribers count. + */ + subscribers?: string; + /** + * YouTube Channel Constructor + * @param data YouTube Channel data that we recieve from basic info or from search + */ + constructor(data?: any); + /** + * Returns channel icon url + * @param {object} options Icon options + * @param {number} [options.size=0] Icon size. **Default is 0** + */ + iconURL(options?: { + size: number; + }): string | undefined; + /** + * Converts Channel Class to channel name. + * @returns name of channel + */ + toString(): string; + /** + * Converts Channel Class to JSON format + * @returns json data of the channel + */ + toJSON(): ChannelJSON; +} +interface ChannelJSON { + /** + * YouTube Channel Title + */ + name?: string; + /** + * YouTube Channel Verified status. + */ + verified?: boolean; + /** + * YouTube Channel artist if any. + */ + artist?: boolean; + /** + * YouTube Channel ID. + */ + id?: string; + /** + * Type of Class [ Channel ] + */ + type: 'video' | 'playlist' | 'channel'; + /** + * YouTube Channel Url + */ + url?: string; + /** + * YouTube Channel Icon data. + */ + icons?: ChannelIconInterface[]; + /** + * YouTube Channel subscribers count. + */ + subscribers?: string; +} + +declare class YouTubeThumbnail { + url: string; + width: number; + height: number; + constructor(data: any); + toJSON(): { + url: string; + width: number; + height: number; + }; +} + +/** + * Licensed music in the video + * + * The property names change depending on your region's language. + */ +interface VideoMusic { + song?: string; + url?: string | null; + artist?: string; + album?: string; + writers?: string; + licenses?: string; +} +interface VideoOptions { + /** + * YouTube Video ID + */ + id?: string; + /** + * YouTube video url + */ + url: string; + /** + * YouTube Video title + */ + title?: string; + /** + * YouTube Video description. + */ + description?: string; + /** + * YouTube Video Duration Formatted + */ + durationRaw: string; + /** + * YouTube Video Duration in seconds + */ + durationInSec: number; + /** + * YouTube Video Uploaded Date + */ + uploadedAt?: string; + /** + * If the video is upcoming or a premiere that isn't currently live, this will contain the premiere date, for watch page playlists this will be true, it defaults to undefined + */ + upcoming?: Date | true; + /** + * YouTube Views + */ + views: number; + /** + * YouTube Thumbnail Data + */ + thumbnail?: { + width: number | undefined; + height: number | undefined; + url: string | undefined; + }; + /** + * YouTube Video's uploader Channel Data + */ + channel?: YouTubeChannel; + /** + * YouTube Video's likes + */ + likes: number; + /** + * YouTube Video live status + */ + live: boolean; + /** + * YouTube Video private status + */ + private: boolean; + /** + * YouTube Video tags + */ + tags: string[]; + /** + * `true` if the video has been identified by the YouTube community as inappropriate or offensive to some audiences and viewer discretion is advised + */ + discretionAdvised?: boolean; + /** + * Gives info about music content in that video. + * + * The property names of VideoMusic change depending on your region's language. + */ + music?: VideoMusic[]; + /** + * The chapters for this video + * + * If the video doesn't have any chapters or if the video object wasn't created by {@link video_basic_info} or {@link video_info} this will be an empty array. + */ + chapters: VideoChapter[]; +} +interface VideoChapter { + /** + * The title of the chapter + */ + title: string; + /** + * The timestamp of the start of the chapter + */ + timestamp: string; + /** + * The start of the chapter in seconds + */ + seconds: number; + /** + * Thumbnails of the frame at the start of this chapter + */ + thumbnails: YouTubeThumbnail[]; +} +/** + * Class for YouTube Video url + */ +declare class YouTubeVideo { + /** + * YouTube Video ID + */ + id?: string; + /** + * YouTube video url + */ + url: string; + /** + * YouTube Class type. == "video" + */ + type: 'video' | 'playlist' | 'channel'; + /** + * YouTube Video title + */ + title?: string; + /** + * YouTube Video description. + */ + description?: string; + /** + * YouTube Video Duration Formatted + */ + durationRaw: string; + /** + * YouTube Video Duration in seconds + */ + durationInSec: number; + /** + * YouTube Video Uploaded Date + */ + uploadedAt?: string; + /** + * YouTube Live Date + */ + liveAt?: string; + /** + * If the video is upcoming or a premiere that isn't currently live, this will contain the premiere date, for watch page playlists this will be true, it defaults to undefined + */ + upcoming?: Date | true; + /** + * YouTube Views + */ + views: number; + /** + * YouTube Thumbnail Data + */ + thumbnails: YouTubeThumbnail[]; + /** + * YouTube Video's uploader Channel Data + */ + channel?: YouTubeChannel; + /** + * YouTube Video's likes + */ + likes: number; + /** + * YouTube Video live status + */ + live: boolean; + /** + * YouTube Video private status + */ + private: boolean; + /** + * YouTube Video tags + */ + tags: string[]; + /** + * `true` if the video has been identified by the YouTube community as inappropriate or offensive to some audiences and viewer discretion is advised + */ + discretionAdvised?: boolean; + /** + * Gives info about music content in that video. + */ + music?: VideoMusic[]; + /** + * The chapters for this video + * + * If the video doesn't have any chapters or if the video object wasn't created by {@link video_basic_info} or {@link video_info} this will be an empty array. + */ + chapters: VideoChapter[]; + /** + * Constructor for YouTube Video Class + * @param data JSON parsed data. + */ + constructor(data: any); + /** + * Converts class to title name of video. + * @returns Title name + */ + toString(): string; + /** + * Converts class to JSON data + * @returns JSON data. + */ + toJSON(): VideoOptions; +} + +interface LiveStreamData { + isLive: boolean; + dashManifestUrl: string | null; + hlsManifestUrl: string | null; +} +interface formatData { + itag: number; + mimeType: string; + bitrate: number; + width: number; + height: number; + lastModified: string; + contentLength: string; + quality: string; + fps: number; + qualityLabel: string; + projectionType: string; + averageBitrate: number; + audioQuality: string; + approxDurationMs: string; + audioSampleRate: string; + audioChannels: number; + url: string; + signatureCipher: string; + cipher: string; + loudnessDb: number; + targetDurationSec: number; +} +interface InfoData { + LiveStreamData: LiveStreamData; + html5player: string; + format: Partial[]; + video_details: YouTubeVideo; + related_videos: string[]; +} +interface StreamInfoData { + LiveStreamData: LiveStreamData; + html5player: string; + format: Partial[]; + video_details: Pick; +} + +declare enum StreamType { + Arbitrary = "arbitrary", + Raw = "raw", + OggOpus = "ogg/opus", + WebmOpus = "webm/opus", + Opus = "opus" +} +interface StreamOptions { + seek?: number; + quality?: number; + language?: string; + htmldata?: boolean; + precache?: number; + discordPlayerCompatibility?: boolean; +} +/** + * Type for YouTube Stream + */ +type YouTubeStream = Stream | LiveStream | SeekStream; + +/** + * YouTube Playlist Class containing vital informations about playlist. + */ +declare class YouTubePlayList { + /** + * YouTube Playlist ID + */ + id?: string; + /** + * YouTube Playlist Name + */ + title?: string; + /** + * YouTube Class type. == "playlist" + */ + type: 'video' | 'playlist' | 'channel'; + /** + * Total no of videos in that playlist + */ + videoCount?: number; + /** + * Time when playlist was last updated + */ + lastUpdate?: string; + /** + * Total views of that playlist + */ + views?: number; + /** + * YouTube Playlist url + */ + url?: string; + /** + * YouTube Playlist url with starting video url. + */ + link?: string; + /** + * YouTube Playlist channel data + */ + channel?: YouTubeChannel; + /** + * YouTube Playlist thumbnail Data + */ + thumbnail?: YouTubeThumbnail; + /** + * Videos array containing data of first 100 videos + */ + private videos?; + /** + * Map contaning data of all fetched videos + */ + private fetched_videos; + /** + * Token containing API key, Token, ClientVersion. + */ + private _continuation; + /** + * Total no of pages count. + */ + private __count; + /** + * Constructor for YouTube Playlist Class + * @param data Json Parsed YouTube Playlist data + * @param searchResult If the data is from search or not + */ + constructor(data: any, searchResult?: boolean); + /** + * Updates variable according to a normal data. + * @param data Json Parsed YouTube Playlist data + */ + private __patch; + /** + * Updates variable according to a searched data. + * @param data Json Parsed YouTube Playlist data + */ + private __patchSearch; + /** + * Parses next segment of videos from playlist and returns parsed data. + * @param limit Total no of videos to parse. + * + * Default = Infinity + * @returns Array of YouTube Video Class + */ + next(limit?: number): Promise; + /** + * Fetches remaining data from playlist + * + * For fetching and getting all songs data, see `total_pages` property. + * @param max Max no of videos to fetch + * + * Default = Infinity + * @returns + */ + fetch(max?: number): Promise; + /** + * YouTube Playlists are divided into pages. + * + * For example, if you want to get 101 - 200 songs + * + * ```ts + * const playlist = await play.playlist_info('playlist url') + * + * await playlist.fetch() + * + * const result = playlist.page(2) + * ``` + * @param number Page number + * @returns Array of YouTube Video Class + * @see {@link YouTubePlayList.all_videos} + */ + page(number: number): YouTubeVideo[]; + /** + * Gets total number of pages in that playlist class. + * @see {@link YouTubePlayList.all_videos} + */ + get total_pages(): number; + /** + * This tells total number of videos that have been fetched so far. + * + * This can be equal to videosCount if all videos in playlist have been fetched and they are not hidden. + */ + get total_videos(): number; + /** + * Fetches all the videos in the playlist and returns them + * + * ```ts + * const playlist = await play.playlist_info('playlist url') + * + * const videos = await playlist.all_videos() + * ``` + * @returns An array of {@link YouTubeVideo} objects + * @see {@link YouTubePlayList.fetch} + */ + all_videos(): Promise; + /** + * Converts Playlist Class to a json parsed data. + * @returns + */ + toJSON(): PlaylistJSON$2; +} +interface PlaylistJSON$2 { + /** + * YouTube Playlist ID + */ + id?: string; + /** + * YouTube Playlist Name + */ + title?: string; + /** + * Total no of videos in that playlist + */ + videoCount?: number; + /** + * Time when playlist was last updated + */ + lastUpdate?: string; + /** + * Total views of that playlist + */ + views?: number; + /** + * YouTube Playlist url + */ + url?: string; + /** + * YouTube Playlist url with starting video url. + */ + link?: string; + /** + * YouTube Playlist channel data + */ + channel?: YouTubeChannel; + /** + * YouTube Playlist thumbnail Data + */ + thumbnail?: { + width: number | undefined; + height: number | undefined; + url: string | undefined; + }; + /** + * first 100 videos in that playlist + */ + videos?: YouTubeVideo[]; +} + +interface InfoOptions { + htmldata?: boolean; + language?: string; +} +interface PlaylistOptions { + incomplete?: boolean; + language?: string; +} +/** + * Validate YouTube URL or ID. + * + * **CAUTION :** If your search word is 11 or 12 characters long, you might get it validated as video ID. + * + * To avoid above, add one more condition to yt_validate + * ```ts + * if (url.startsWith('https') && yt_validate(url) === 'video') { + * // YouTube Video Url. + * } + * ``` + * @param url YouTube URL OR ID + * @returns + * ``` + * 'playlist' | 'video' | 'search' | false + * ``` + */ +declare function yt_validate(url: string): 'playlist' | 'video' | 'search' | false; +/** + * Extract ID of YouTube url. + * @param url ID or url of YouTube + * @returns ID of video or playlist. + */ +declare function extractID(url: string): string; +/** + * Basic function to get data from a YouTube url or ID. + * + * Example + * ```ts + * const video = await play.video_basic_info('youtube video url') + * + * const res = ... // Any https package get function. + * + * const video = await play.video_basic_info(res.body, { htmldata : true }) + * ``` + * @param url YouTube url or ID or html body data + * @param options Video Info Options + * - `boolean` htmldata : given data is html data or not + * @returns Video Basic Info {@link InfoData}. + */ +declare function video_basic_info(url: string, options?: InfoOptions): Promise; +/** + * Gets data from YouTube url or ID or html body data and deciphers it. + * ``` + * video_basic_info + decipher_info = video_info + * ``` + * + * Example + * ```ts + * const video = await play.video_info('youtube video url') + * + * const res = ... // Any https package get function. + * + * const video = await play.video_info(res.body, { htmldata : true }) + * ``` + * @param url YouTube url or ID or html body data + * @param options Video Info Options + * - `boolean` htmldata : given data is html data or not + * @returns Deciphered Video Info {@link InfoData}. + */ +declare function video_info(url: string, options?: InfoOptions): Promise; +/** + * Function uses data from video_basic_info and deciphers it if it contains signatures. + * @param data Data - {@link InfoData} + * @param audio_only `boolean` - To decipher only audio formats only. + * @returns Deciphered Video Info {@link InfoData} + */ +declare function decipher_info(data: T, audio_only?: boolean): Promise; +/** + * Gets YouTube playlist info from a playlist url. + * + * Example + * ```ts + * const playlist = await play.playlist_info('youtube playlist url') + * + * const playlist = await play.playlist_info('youtube playlist url', { incomplete : true }) + * ``` + * @param url Playlist URL + * @param options Playlist Info Options + * - `boolean` incomplete : When this is set to `false` (default) this function will throw an error + * if the playlist contains hidden videos. + * If it is set to `true`, it parses the playlist skipping the hidden videos, + * only visible videos are included in the resulting {@link YouTubePlaylist}. + * + * @returns YouTube Playlist + */ +declare function playlist_info(url: string, options?: PlaylistOptions): Promise; + +/** + * Type for YouTube returns + */ +type YouTube = YouTubeVideo | YouTubeChannel | YouTubePlayList; + +interface TrackJSON { + /** + * Spotify Track Name + */ + name: string; + /** + * Spotify Track ID + */ + id: string; + /** + * Spotify Track url + */ + url: string; + /** + * Spotify Track explicit info. + */ + explicit: boolean; + /** + * Spotify Track Duration in seconds + */ + durationInSec: number; + /** + * Spotify Track Duration in milli seconds + */ + durationInMs: number; + /** + * Spotify Track Artists data [ array ] + */ + artists: SpotifyArtists[]; + /** + * Spotify Track Album data + */ + album: SpotifyTrackAlbum | undefined; + /** + * Spotify Track Thumbnail Data + */ + thumbnail: SpotifyThumbnail | undefined; +} +interface PlaylistJSON$1 { + /** + * Spotify Playlist Name + */ + name: string; + /** + * Spotify Playlist collaborative boolean. + */ + collaborative: boolean; + /** + * Spotify Playlist Description + */ + description: string; + /** + * Spotify Playlist URL + */ + url: string; + /** + * Spotify Playlist ID + */ + id: string; + /** + * Spotify Playlist Thumbnail Data + */ + thumbnail: SpotifyThumbnail; + /** + * Spotify Playlist Owner Artist data + */ + owner: SpotifyArtists; + /** + * Spotify Playlist total tracks Count + */ + tracksCount: number; +} +interface AlbumJSON { + /** + * Spotify Album Name + */ + name: string; + /** + * Spotify Class type. == "album" + */ + type: 'track' | 'playlist' | 'album'; + /** + * Spotify Album url + */ + url: string; + /** + * Spotify Album id + */ + id: string; + /** + * Spotify Album Thumbnail data + */ + thumbnail: SpotifyThumbnail; + /** + * Spotify Album artists [ array ] + */ + artists: SpotifyArtists[]; + /** + * Spotify Album copyright data [ array ] + */ + copyrights: SpotifyCopyright[]; + /** + * Spotify Album Release date + */ + release_date: string; + /** + * Spotify Album Release Date **precise** + */ + release_date_precision: string; + /** + * Spotify Album total no of tracks + */ + tracksCount: number; +} + +interface SpotifyTrackAlbum { + /** + * Spotify Track Album name + */ + name: string; + /** + * Spotify Track Album url + */ + url: string; + /** + * Spotify Track Album id + */ + id: string; + /** + * Spotify Track Album release date + */ + release_date: string; + /** + * Spotify Track Album release date **precise** + */ + release_date_precision: string; + /** + * Spotify Track Album total tracks number + */ + total_tracks: number; +} +interface SpotifyArtists { + /** + * Spotify Artist Name + */ + name: string; + /** + * Spotify Artist Url + */ + url: string; + /** + * Spotify Artist ID + */ + id: string; +} +interface SpotifyThumbnail { + /** + * Spotify Thumbnail height + */ + height: number; + /** + * Spotify Thumbnail width + */ + width: number; + /** + * Spotify Thumbnail url + */ + url: string; +} +interface SpotifyCopyright { + /** + * Spotify Copyright Text + */ + text: string; + /** + * Spotify Copyright Type + */ + type: string; +} +/** + * Spotify Track Class + */ +declare class SpotifyTrack { + /** + * Spotify Track Name + */ + name: string; + /** + * Spotify Class type. == "track" + */ + type: 'track' | 'playlist' | 'album'; + /** + * Spotify Track ID + */ + id: string; + /** + * Spotify Track ISRC + */ + isrc: string; + /** + * Spotify Track url + */ + url: string; + /** + * Spotify Track explicit info. + */ + explicit: boolean; + /** + * Spotify Track playability info. + */ + playable: boolean; + /** + * Spotify Track Duration in seconds + */ + durationInSec: number; + /** + * Spotify Track Duration in milli seconds + */ + durationInMs: number; + /** + * Spotify Track Artists data [ array ] + */ + artists: SpotifyArtists[]; + /** + * Spotify Track Album data + */ + album: SpotifyTrackAlbum | undefined; + /** + * Spotify Track Thumbnail Data + */ + thumbnail: SpotifyThumbnail | undefined; + /** + * Constructor for Spotify Track + * @param data + */ + constructor(data: any); + toJSON(): TrackJSON; +} +/** + * Spotify Playlist Class + */ +declare class SpotifyPlaylist { + /** + * Spotify Playlist Name + */ + name: string; + /** + * Spotify Class type. == "playlist" + */ + type: 'track' | 'playlist' | 'album'; + /** + * Spotify Playlist collaborative boolean. + */ + collaborative: boolean; + /** + * Spotify Playlist Description + */ + description: string; + /** + * Spotify Playlist URL + */ + url: string; + /** + * Spotify Playlist ID + */ + id: string; + /** + * Spotify Playlist Thumbnail Data + */ + thumbnail: SpotifyThumbnail; + /** + * Spotify Playlist Owner Artist data + */ + owner: SpotifyArtists; + /** + * Spotify Playlist total tracks Count + */ + tracksCount: number; + /** + * Spotify Playlist Spotify data + * + * @private + */ + private spotifyData; + /** + * Spotify Playlist fetched tracks Map + * + * @private + */ + private fetched_tracks; + /** + * Boolean to tell whether it is a searched result or not. + */ + private readonly search; + /** + * Constructor for Spotify Playlist Class + * @param data JSON parsed data of playlist + * @param spotifyData Data about sporify token for furhter fetching. + */ + constructor(data: any, spotifyData: SpotifyDataOptions, search: boolean); + /** + * Fetches Spotify Playlist tracks more than 100 tracks. + * + * For getting all tracks in playlist, see `total_pages` property. + * @returns Playlist Class. + */ + fetch(): Promise; + /** + * Spotify Playlist tracks are divided in pages. + * + * For example getting data of 101 - 200 videos in a playlist, + * + * ```ts + * const playlist = await play.spotify('playlist url') + * + * await playlist.fetch() + * + * const result = playlist.page(2) + * ``` + * @param num Page Number + * @returns + */ + page(num: number): SpotifyTrack[]; + /** + * Gets total number of pages in that playlist class. + * @see {@link SpotifyPlaylist.all_tracks} + */ + get total_pages(): number; + /** + * Spotify Playlist total no of tracks that have been fetched so far. + */ + get total_tracks(): number; + /** + * Fetches all the tracks in the playlist and returns them + * + * ```ts + * const playlist = await play.spotify('playlist url') + * + * const tracks = await playlist.all_tracks() + * ``` + * @returns An array of {@link SpotifyTrack} + */ + all_tracks(): Promise; + /** + * Converts Class to JSON + * @returns JSON data + */ + toJSON(): PlaylistJSON$1; +} +/** + * Spotify Album Class + */ +declare class SpotifyAlbum { + /** + * Spotify Album Name + */ + name: string; + /** + * Spotify Class type. == "album" + */ + type: 'track' | 'playlist' | 'album'; + /** + * Spotify Album url + */ + url: string; + /** + * Spotify Album id + */ + id: string; + /** + * Spotify Album Thumbnail data + */ + thumbnail: SpotifyThumbnail; + /** + * Spotify Album artists [ array ] + */ + artists: SpotifyArtists[]; + /** + * Spotify Album copyright data [ array ] + */ + copyrights: SpotifyCopyright[]; + /** + * Spotify Album Release date + */ + release_date: string; + /** + * Spotify Album Release Date **precise** + */ + release_date_precision: string; + /** + * Spotify Album total no of tracks + */ + tracksCount: number; + /** + * Spotify Album Spotify data + * + * @private + */ + private spotifyData; + /** + * Spotify Album fetched tracks Map + * + * @private + */ + private fetched_tracks; + /** + * Boolean to tell whether it is a searched result or not. + */ + private readonly search; + /** + * Constructor for Spotify Album Class + * @param data Json parsed album data + * @param spotifyData Spotify credentials + */ + constructor(data: any, spotifyData: SpotifyDataOptions, search: boolean); + /** + * Fetches Spotify Album tracks more than 50 tracks. + * + * For getting all tracks in album, see `total_pages` property. + * @returns Album Class. + */ + fetch(): Promise; + /** + * Spotify Album tracks are divided in pages. + * + * For example getting data of 51 - 100 videos in a album, + * + * ```ts + * const album = await play.spotify('album url') + * + * await album.fetch() + * + * const result = album.page(2) + * ``` + * @param num Page Number + * @returns + */ + page(num: number): SpotifyTrack[] | undefined; + /** + * Gets total number of pages in that album class. + * @see {@link SpotifyAlbum.all_tracks} + */ + get total_pages(): number; + /** + * Spotify Album total no of tracks that have been fetched so far. + */ + get total_tracks(): number; + /** + * Fetches all the tracks in the album and returns them + * + * ```ts + * const album = await play.spotify('album url') + * + * const tracks = await album.all_tracks() + * ``` + * @returns An array of {@link SpotifyTrack} + */ + all_tracks(): Promise; + /** + * Converts Class to JSON + * @returns JSON data + */ + toJSON(): AlbumJSON; +} + +/** + * Spotify Data options that are stored in spotify.data file. + */ +interface SpotifyDataOptions { + client_id: string; + client_secret: string; + redirect_url?: string; + authorization_code?: string; + access_token?: string; + refresh_token?: string; + token_type?: string; + expires_in?: number; + expiry?: number; + market?: string; + file?: boolean; +} +/** + * Gets Spotify url details. + * + * ```ts + * let spot = await play.spotify('spotify url') + * + * // spot.type === "track" | "playlist" | "album" + * + * if (spot.type === "track") { + * spot = spot as play.SpotifyTrack + * // Code with spotify track class. + * } + * ``` + * @param url Spotify Url + * @returns A {@link SpotifyTrack} or {@link SpotifyPlaylist} or {@link SpotifyAlbum} + */ +declare function spotify(url: string): Promise; +/** + * Validate Spotify url + * @param url Spotify URL + * @returns + * ```ts + * 'track' | 'playlist' | 'album' | 'search' | false + * ``` + */ +declare function sp_validate(url: string): 'track' | 'playlist' | 'album' | 'search' | false; +/** + * Checks if spotify token is expired or not. + * + * Update token if returned false. + * ```ts + * if (play.is_expired()) { + * await play.refreshToken() + * } + * ``` + * @returns boolean + */ +declare function is_expired(): boolean; +/** + * type for Spotify Classes + */ +type Spotify = SpotifyAlbum | SpotifyPlaylist | SpotifyTrack; +/** + * Refreshes Token + * + * ```ts + * if (play.is_expired()) { + * await play.refreshToken() + * } + * ``` + * @returns boolean + */ +declare function refreshToken(): Promise; + +interface SoundTrackJSON { + /** + * SoundCloud Track Name + */ + name: string; + /** + * SoundCloud Track ID + */ + id: number; + /** + * SoundCloud Track url + */ + url: string; + /** + * User friendly SoundCloud track URL + */ + permalink: string; + /** + * SoundCloud Track fetched status + */ + fetched: boolean; + /** + * SoundCloud Track Duration in seconds + */ + durationInSec: number; + /** + * SoundCloud Track Duration in miili seconds + */ + durationInMs: number; + /** + * SoundCloud Track formats data + */ + formats: SoundCloudTrackFormat[]; + /** + * SoundCloud Track Publisher Data + */ + publisher: { + name: string; + id: number; + artist: string; + contains_music: boolean; + writer_composer: string; + } | null; + /** + * SoundCloud Track thumbnail + */ + thumbnail: string; + /** + * SoundCloud Track user data + */ + user: SoundCloudUser; +} +interface PlaylistJSON { + /** + * SoundCloud Playlist Name + */ + name: string; + /** + * SoundCloud Playlist ID + */ + id: number; + /** + * SoundCloud Playlist URL + */ + url: string; + /** + * SoundCloud Playlist Sub type. == "album" for soundcloud albums + */ + sub_type: string; + /** + * SoundCloud Playlist Total Duration in seconds + */ + durationInSec: number; + /** + * SoundCloud Playlist Total Duration in milli seconds + */ + durationInMs: number; + /** + * SoundCloud Playlist user data + */ + user: SoundCloudUser; + /** + * SoundCloud Playlist tracks [ It can be fetched or not fetched ] + */ + tracks: SoundCloudTrack[] | SoundCloudTrackDeprecated[]; + /** + * SoundCloud Playlist tracks number + */ + tracksCount: number; +} + +interface SoundCloudUser { + /** + * SoundCloud User Name + */ + name: string; + /** + * SoundCloud User ID + */ + id: string; + /** + * SoundCloud User URL + */ + url: string; + /** + * SoundCloud Class type. == "user" + */ + type: 'track' | 'playlist' | 'user'; + /** + * SoundCloud User Verified status + */ + verified: boolean; + /** + * SoundCloud User Description + */ + description: string; + /** + * SoundCloud User First Name + */ + first_name: string; + /** + * SoundCloud User Full Name + */ + full_name: string; + /** + * SoundCloud User Last Name + */ + last_name: string; + /** + * SoundCloud User thumbnail URL + */ + thumbnail: string; +} +interface SoundCloudTrackDeprecated { + /** + * SoundCloud Track fetched status + */ + fetched: boolean; + /** + * SoundCloud Track ID + */ + id: number; + /** + * SoundCloud Class type. == "track" + */ + type: 'track'; +} +interface SoundCloudTrackFormat { + /** + * SoundCloud Track Format Url + */ + url: string; + /** + * SoundCloud Track Format preset + */ + preset: string; + /** + * SoundCloud Track Format Duration + */ + duration: number; + /** + * SoundCloud Track Format data containing protocol and mime_type + */ + format: { + protocol: string; + mime_type: string; + }; + /** + * SoundCloud Track Format quality + */ + quality: string; +} +/** + * SoundCloud Track Class + */ +declare class SoundCloudTrack { + /** + * SoundCloud Track Name + */ + name: string; + /** + * SoundCloud Track ID + */ + id: number; + /** + * SoundCloud Track url + */ + url: string; + /** + * User friendly SoundCloud track URL + */ + permalink: string; + /** + * SoundCloud Track fetched status + */ + fetched: boolean; + /** + * SoundCloud Class type. === "track" + */ + type: 'track' | 'playlist' | 'user'; + /** + * SoundCloud Track Duration in seconds + */ + durationInSec: number; + /** + * SoundCloud Track Duration in miili seconds + */ + durationInMs: number; + /** + * SoundCloud Track formats data + */ + formats: SoundCloudTrackFormat[]; + /** + * SoundCloud Track Publisher Data + */ + publisher: { + name: string; + id: number; + artist: string; + contains_music: boolean; + writer_composer: string; + } | null; + /** + * SoundCloud Track thumbnail + */ + thumbnail: string; + /** + * SoundCloud Track user data + */ + user: SoundCloudUser; + /** + * Constructor for SoundCloud Track Class + * @param data JSON parsed track html data + */ + constructor(data: any); + /** + * Converts class to JSON + * @returns JSON parsed Data + */ + toJSON(): SoundTrackJSON; +} +/** + * SoundCloud Playlist Class + */ +declare class SoundCloudPlaylist { + /** + * SoundCloud Playlist Name + */ + name: string; + /** + * SoundCloud Playlist ID + */ + id: number; + /** + * SoundCloud Playlist URL + */ + url: string; + /** + * SoundCloud Class type. == "playlist" + */ + type: 'track' | 'playlist' | 'user'; + /** + * SoundCloud Playlist Sub type. == "album" for soundcloud albums + */ + sub_type: string; + /** + * SoundCloud Playlist Total Duration in seconds + */ + durationInSec: number; + /** + * SoundCloud Playlist Total Duration in milli seconds + */ + durationInMs: number; + /** + * SoundCloud Playlist user data + */ + user: SoundCloudUser; + /** + * SoundCloud Playlist tracks [ It can be fetched or not fetched ] + */ + tracks: SoundCloudTrack[] | SoundCloudTrackDeprecated[]; + /** + * SoundCloud Playlist tracks number + */ + tracksCount: number; + /** + * SoundCloud Client ID provided by user + * @private + */ + private client_id; + /** + * Constructor for SoundCloud Playlist + * @param data JSON parsed SoundCloud playlist data + * @param client_id Provided SoundCloud Client ID + */ + constructor(data: any, client_id: string); + /** + * Fetches all unfetched songs in a playlist. + * + * For fetching songs and getting all songs, see `fetched_tracks` property. + * @returns playlist class + */ + fetch(): Promise; + /** + * Get total no. of fetched tracks + * @see {@link SoundCloudPlaylist.all_tracks} + */ + get total_tracks(): number; + /** + * Fetches all the tracks in the playlist and returns them + * + * ```ts + * const playlist = await play.soundcloud('playlist url') + * + * const tracks = await playlist.all_tracks() + * ``` + * @returns An array of {@link SoundCloudTrack} + */ + all_tracks(): Promise; + /** + * Converts Class to JSON data + * @returns JSON parsed data + */ + toJSON(): PlaylistJSON; +} +/** + * SoundCloud Stream class + */ +declare class SoundCloudStream { + /** + * Readable Stream through which data passes + */ + stream: Readable; + /** + * Type of audio data that we recieved from normal youtube url. + */ + type: StreamType; + /** + * Dash Url containing segment urls. + * @private + */ + private url; + /** + * Total time of downloaded segments data. + * @private + */ + private downloaded_time; + /** + * Timer for looping code every 5 minutes + * @private + */ + private timer; + /** + * Total segments Downloaded so far + * @private + */ + private downloaded_segments; + /** + * Incoming message that we recieve. + * + * Storing this is essential. + * This helps to destroy the TCP connection completely if you stopped player in between the stream + * @private + */ + private request; + /** + * Array of segment time. Useful for calculating downloaded_time. + */ + private time; + /** + * Array of segment_urls in dash file. + */ + private segment_urls; + /** + * Constructor for SoundCloud Stream + * @param url Dash url containing dash file. + * @param type Stream Type + */ + constructor(url: string, type?: StreamType); + /** + * Parses SoundCloud dash file. + * @private + */ + private parser; + /** + * Starts looping of code for getting all segments urls data + */ + private start; + /** + * Main Loop function for getting all segments urls data + */ + private loop; + /** + * This cleans every used variable in class. + * + * This is used to prevent re-use of this class and helping garbage collector to collect it. + */ + private cleanup; + /** + * Pauses timer. + * Stops running of loop. + * + * Useful if you don't want to get excess data to be stored in stream. + */ + pause(): void; + /** + * Resumes timer. + * Starts running of loop. + */ + resume(): void; +} + +/** + * Gets info from a soundcloud url. + * + * ```ts + * let sound = await play.soundcloud('soundcloud url') + * + * // sound.type === "track" | "playlist" | "user" + * + * if (sound.type === "track") { + * spot = spot as play.SoundCloudTrack + * // Code with SoundCloud track class. + * } + * ``` + * @param url soundcloud url + * @returns A {@link SoundCloudTrack} or {@link SoundCloudPlaylist} + */ +declare function soundcloud(url: string): Promise; +/** + * Type of SoundCloud + */ +type SoundCloud = SoundCloudTrack | SoundCloudPlaylist; +/** + * Gets Free SoundCloud Client ID. + * + * Use this in beginning of your code to add SoundCloud support. + * + * ```ts + * play.getFreeClientID().then((clientID) => play.setToken({ + * soundcloud : { + * client_id : clientID + * } + * })) + * ``` + * @returns client ID + */ +declare function getFreeClientID(): Promise; +/** + * Validates a soundcloud url + * @param url soundcloud url + * @returns + * ```ts + * false | 'track' | 'playlist' + * ``` + */ +declare function so_validate(url: string): Promise; + +/** + * Interface representing an image on Deezer + * available in four sizes + */ +interface DeezerImage { + /** + * The largest version of the image + */ + xl: string; + /** + * The second largest version of the image + */ + big: string; + /** + * The second smallest version of the image + */ + medium: string; + /** + * The smallest version of the image + */ + small: string; +} +/** + * Interface representing a Deezer genre + */ +interface DeezerGenre { + /** + * The name of the genre + */ + name: string; + /** + * The thumbnail of the genre available in four sizes + */ + picture: DeezerImage; +} +/** + * Interface representing a Deezer user account + */ +interface DeezerUser { + /** + * The id of the user + */ + id: number; + /** + * The name of the user + */ + name: string; +} +/** + * Class representing a Deezer track + */ +declare class DeezerTrack { + /** + * The id of the track + */ + id: number; + /** + * The title of the track + */ + title: string; + /** + * A shorter version of the title + */ + shortTitle: string; + /** + * The URL of the track on Deezer + */ + url: string; + /** + * The duration of the track in seconds + */ + durationInSec: number; + /** + * The rank of the track + */ + rank: number; + /** + * `true` if the track contains any explicit lyrics + */ + explicit: boolean; + /** + * URL to a file containing the first 30 seconds of the track + */ + previewURL: string; + /** + * The artist of the track + */ + artist: DeezerArtist; + /** + * The album that this track is in + */ + album: DeezerTrackAlbum; + /** + * The type, always `'track'`, useful to determine what the deezer function returned + */ + type: 'track' | 'playlist' | 'album'; + /** + * Signifies that some properties are not populated + * + * Partial tracks can be populated by calling {@link DeezerTrack.fetch}. + * + * `true` for tracks in search results and `false` if the track was fetched directly or expanded. + */ + partial: boolean; + /** + * The position of the track in the album + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + trackPosition?: number; + /** + * The number of the disk the track is on + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + diskNumber?: number; + /** + * The release date + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + releaseDate?: Date; + /** + * The number of beats per minute + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + bpm?: number; + /** + * The gain of the track + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + gain?: number; + /** + * The artists that have contributed to the track + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + contributors?: DeezerArtist[]; + /** + * Creates a Deezer track from the data in an API response + * @param data the data to use to create the track + * @param partial Whether the track should be partial + * @see {@link DeezerTrack.partial} + */ + constructor(data: any, partial: boolean); + /** + * Fetches and populates the missing fields + * + * The property {@link partial} will be `false` if this method finishes successfully. + * + * @returns A promise with the same track this method was called on. + */ + fetch(): Promise; + /** + * Converts instances of this class to JSON data + * @returns JSON data. + */ + toJSON(): { + id: number; + title: string; + shortTitle: string; + url: string; + durationInSec: number; + rank: number; + explicit: boolean; + previewURL: string; + artist: DeezerArtist; + album: DeezerTrackAlbum; + type: "playlist" | "album" | "track"; + trackPosition: number | undefined; + diskNumber: number | undefined; + releaseDate: Date | undefined; + bpm: number | undefined; + gain: number | undefined; + contributors: DeezerArtist[] | undefined; + }; +} +/** + * Class for Deezer Albums + */ +declare class DeezerAlbum { + /** + * The id of the album + */ + id: number; + /** + * The title of the album + */ + title: string; + /** + * The URL to the album on Deezer + */ + url: string; + /** + * The record type of the album (e.g. EP, ALBUM, etc ...) + */ + recordType: string; + /** + * `true` if the album contains any explicit lyrics + */ + explicit: boolean; + /** + * The artist of the album + */ + artist: DeezerArtist; + /** + * The album cover available in four sizes + */ + cover: DeezerImage; + /** + * The type, always `'album'`, useful to determine what the deezer function returned + */ + type: 'track' | 'playlist' | 'album'; + /** + * The number of tracks in the album + */ + tracksCount: number; + /** + * Signifies that some properties are not populated + * + * Partial albums can be populated by calling {@link DeezerAlbum.fetch}. + * + * `true` for albums in search results and `false` if the album was fetched directly or expanded. + */ + partial: boolean; + /** + * The **u**niversal **p**roduct **c**ode of the album + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + upc?: string; + /** + * The duration of the album in seconds + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + durationInSec?: number; + /** + * The number of fans the album has + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + numberOfFans?: number; + /** + * The release date of the album + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + releaseDate?: Date; + /** + * Whether the album is available + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + available?: boolean; + /** + * The list of genres present in this album + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + genres?: DeezerGenre[]; + /** + * The contributors to the album + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + contributors?: DeezerArtist[]; + /** + * The list of tracks in the album + * + * empty (length === 0) for partial albums + * + * Use {@link DeezerAlbum.fetch} to populate the tracks and other properties + * + * @see {@link DeezerAlbum.partial} + */ + tracks: DeezerTrack[]; + /** + * Creates a Deezer album from the data in an API response + * @param data the data to use to create the album + * @param partial Whether the album should be partial + * @see {@link DeezerAlbum.partial} + */ + constructor(data: any, partial: boolean); + /** + * Fetches and populates the missing fields including all tracks. + * + * The property {@link DeezerAlbum.partial} will be `false` if this method finishes successfully. + * + * @returns A promise with the same album this method was called on. + */ + fetch(): Promise; + /** + * Fetches all the tracks in the album and returns them + * + * ```ts + * const album = await play.deezer('album url') + * + * const tracks = await album.all_tracks() + * ``` + * @returns An array of {@link DeezerTrack} + */ + all_tracks(): Promise; + /** + * Converts instances of this class to JSON data + * @returns JSON data. + */ + toJSON(): { + id: number; + title: string; + url: string; + recordType: string; + explicit: boolean; + artist: DeezerArtist; + cover: DeezerImage; + type: "playlist" | "album" | "track"; + upc: string | undefined; + tracksCount: number; + durationInSec: number | undefined; + numberOfFans: number | undefined; + releaseDate: Date | undefined; + available: boolean | undefined; + genres: DeezerGenre[] | undefined; + contributors: DeezerArtist[] | undefined; + tracks: { + id: number; + title: string; + shortTitle: string; + url: string; + durationInSec: number; + rank: number; + explicit: boolean; + previewURL: string; + artist: DeezerArtist; + album: DeezerTrackAlbum; + type: "playlist" | "album" | "track"; + trackPosition: number | undefined; + diskNumber: number | undefined; + releaseDate: Date | undefined; + bpm: number | undefined; + gain: number | undefined; + contributors: DeezerArtist[] | undefined; + }[]; + }; +} +/** + * Class for Deezer Playlists + */ +declare class DeezerPlaylist { + /** + * The id of the playlist + */ + id: number; + /** + * The title of the playlist + */ + title: string; + /** + * Whether the playlist is public or private + */ + public: boolean; + /** + * The URL of the playlist on Deezer + */ + url: string; + /** + * Cover picture of the playlist available in four sizes + */ + picture: DeezerImage; + /** + * The date of the playlist's creation + */ + creationDate: Date; + /** + * The type, always `'playlist'`, useful to determine what the deezer function returned + */ + type: 'track' | 'playlist' | 'album'; + /** + * The Deezer user that created the playlist + */ + creator: DeezerUser; + /** + * The number of tracks in the playlist + */ + tracksCount: number; + /** + * Signifies that some properties are not populated + * + * Partial playlists can be populated by calling {@link DeezerPlaylist.fetch}. + * + * `true` for playlists in search results and `false` if the album was fetched directly or expanded. + */ + partial: boolean; + /** + * Description of the playlist + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + description?: string; + /** + * Duration of the playlist in seconds + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + durationInSec?: number; + /** + * `true` if the playlist is the loved tracks playlist + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + isLoved?: boolean; + /** + * Whether multiple users have worked on the playlist + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + collaborative?: boolean; + /** + * The number of fans the playlist has + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + fans?: number; + /** + * The list of tracks in the playlist + * + * empty (length === 0) for partial and non public playlists + * + * Use {@link DeezerPlaylist.fetch} to populate the tracks and other properties + * + * @see {@link DeezerPlaylist.partial} + * @see {@link DeezerPlaylist.public} + */ + tracks: DeezerTrack[]; + /** + * Creates a Deezer playlist from the data in an API response + * @param data the data to use to create the playlist + * @param partial Whether the playlist should be partial + * @see {@link DeezerPlaylist.partial} + */ + constructor(data: any, partial: boolean); + /** + * Fetches and populates the missing fields, including all tracks. + * + * The property {@link DeezerPlaylist.partial} will be `false` if this method finishes successfully. + * + * @returns A promise with the same playlist this method was called on. + */ + fetch(): Promise; + /** + * Fetches all the tracks in the playlist and returns them + * + * ```ts + * const playlist = await play.deezer('playlist url') + * + * const tracks = await playlist.all_tracks() + * ``` + * @returns An array of {@link DeezerTrack} + */ + all_tracks(): Promise; + /** + * Converts instances of this class to JSON data + * @returns JSON data. + */ + toJSON(): { + id: number; + title: string; + public: boolean; + url: string; + picture: DeezerImage; + creationDate: Date; + type: "playlist" | "album" | "track"; + creator: DeezerUser; + tracksCount: number; + description: string | undefined; + durationInSec: number | undefined; + isLoved: boolean | undefined; + collaborative: boolean | undefined; + fans: number | undefined; + tracks: { + id: number; + title: string; + shortTitle: string; + url: string; + durationInSec: number; + rank: number; + explicit: boolean; + previewURL: string; + artist: DeezerArtist; + album: DeezerTrackAlbum; + type: "playlist" | "album" | "track"; + trackPosition: number | undefined; + diskNumber: number | undefined; + releaseDate: Date | undefined; + bpm: number | undefined; + gain: number | undefined; + contributors: DeezerArtist[] | undefined; + }[]; + }; +} +declare class DeezerTrackAlbum { + id: number; + title: string; + url: string; + cover: DeezerImage; + releaseDate?: Date; + constructor(data: any); +} +/** + * Class representing a Deezer artist + */ +declare class DeezerArtist { + /** + * The id of the artist + */ + id: number; + /** + * The name of the artist + */ + name: string; + /** + * The URL of the artist on Deezer + */ + url: string; + /** + * The picture of the artist available in four sizes + */ + picture?: DeezerImage; + /** + * The of the artist on the track + */ + role?: string; + constructor(data: any); +} + +interface DeezerAdvancedSearchOptions { + /** + * The maximum number of results to return, maximum `100`, defaults to `10`. + */ + limit?: number; + /** + * The name of the artist. + */ + artist?: string; + /** + * The title of the album. + */ + album?: string; + /** + * The title of the track. + */ + title?: string; + /** + * The label that released the track. + */ + label?: string; + /** + * The minimum duration in seconds. + */ + minDurationInSec?: number; + /** + * The maximum duration in seconds. + */ + maxDurationInSec?: number; + /** + * The minimum BPM. + */ + minBPM?: number; + /** + * The minimum BPM. + */ + maxBPM?: number; +} +/** + * Shared type for Deezer tracks, playlists and albums + */ +type Deezer = DeezerTrack | DeezerPlaylist | DeezerAlbum; +/** + * Fetches the information for a track, playlist or album on Deezer + * @param url The track, playlist or album URL + * @returns A {@link DeezerTrack}, {@link DeezerPlaylist} or {@link DeezerAlbum} + * object depending on the provided URL. + */ +declare function deezer(url: string): Promise; +/** + * Validates a Deezer URL + * @param url The URL to validate + * @returns The type of the URL either `'track'`, `'playlist'`, `'album'`, `'search'` or `false`. + * `false` means that the provided URL was a wrongly formatted or an unsupported Deezer URL. + */ +declare function dz_validate(url: string): Promise<'track' | 'playlist' | 'album' | 'search' | false>; +/** + * Searches Deezer for tracks using the specified metadata. + * @param options The metadata and limit for the search + * + * * limit?: The maximum number of results to return, maximum `100`, defaults to `10`. + * * artist?: The name of the artist + * * album?: The title of the album + * * title?: The title of the track + * * label?: The label that released the track + * * minDurationInSec?: The minimum duration in seconds + * * maxDurationInSec?: The maximum duration in seconds + * * minBpm?: The minimum BPM + * * maxBpm?: The minimum BPM + * @returns An array of tracks matching the metadata + */ +declare function dz_advanced_track_search(options: DeezerAdvancedSearchOptions): Promise; + +interface tokenOptions { + spotify?: { + client_id: string; + client_secret: string; + refresh_token: string; + market: string; + }; + soundcloud?: { + client_id: string; + }; + youtube?: { + cookie: string; + }; + useragent?: string[]; +} +/** + * Sets + * + * i> YouTube :- cookies. + * + * ii> SoundCloud :- client ID. + * + * iii> Spotify :- client ID, client secret, refresh token, market. + * + * iv> Useragents :- array of string. + * + * locally in memory. + * + * Example : + * ```ts + * play.setToken({ + * youtube : { + * cookie : "Your Cookies" + * } + * }) // YouTube Cookies + * + * await play.setToken({ + * spotify : { + * client_id: 'ID', + client_secret: 'secret', + refresh_token: 'token', + market: 'US' + * } + * }) // Await this only when setting data for spotify + * + * play.setToken({ + * useragent: ['Your User-agent'] + * }) // Use this to avoid 429 errors. + * ``` + * @param options {@link tokenOptions} + */ +declare function setToken(options: tokenOptions): Promise; + +interface SearchOptions { + limit?: number; + source?: { + youtube?: 'video' | 'playlist' | 'channel'; + spotify?: 'album' | 'playlist' | 'track'; + soundcloud?: 'tracks' | 'playlists' | 'albums'; + deezer?: 'track' | 'playlist' | 'album'; + }; + fuzzy?: boolean; + language?: string; + /** + * !!! Before enabling this for public servers, please consider using Discord features like NSFW channels as not everyone in your server wants to see NSFW images. !!! + * Unblurred images will likely have different dimensions than specified in the {@link YouTubeThumbnail} objects. + */ + unblurNSFWThumbnails?: boolean; +} + +declare function stream(url: string, options: { + seek?: number; +} & StreamOptions): Promise; +declare function stream(url: string, options?: StreamOptions): Promise; +declare function search(query: string, options: { + source: { + deezer: 'album'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + deezer: 'playlist'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + deezer: 'track'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + soundcloud: 'albums'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + soundcloud: 'playlists'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + soundcloud: 'tracks'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + spotify: 'album'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + spotify: 'playlist'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + spotify: 'track'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + youtube: 'channel'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + youtube: 'playlist'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + youtube: 'video'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + limit: number; +} & SearchOptions): Promise; +declare function search(query: string, options?: SearchOptions): Promise; +declare function stream_from_info(info: SoundCloudTrack, options?: StreamOptions): Promise; +declare function stream_from_info(info: InfoData, options?: StreamOptions): Promise; +/** + * Validates url that play-dl supports. + * + * - `so` - SoundCloud + * - `sp` - Spotify + * - `dz` - Deezer + * - `yt` - YouTube + * @param url URL + * @returns + * ```ts + * 'so_playlist' / 'so_track' | 'sp_track' | 'sp_album' | 'sp_playlist' | 'dz_track' | 'dz_playlist' | 'dz_album' | 'yt_video' | 'yt_playlist' | 'search' | false + * ``` + */ +declare function validate(url: string): Promise<'so_playlist' | 'so_track' | 'sp_track' | 'sp_album' | 'sp_playlist' | 'dz_track' | 'dz_playlist' | 'dz_album' | 'yt_video' | 'yt_playlist' | 'search' | false>; +/** + * Authorization interface for Spotify, SoundCloud and YouTube. + * + * Either stores info in `.data` folder or shows relevant data to be used in `setToken` function. + * + * ```ts + * const play = require('play-dl') + * + * play.authorization() + * ``` + * + * Just run the above command and you will get a interface asking some questions. + */ +declare function authorization(): void; +/** + * Attaches paused, playing, autoPaused Listeners to discordjs voice AudioPlayer. + * + * Useful if you don't want extra data to be downloaded by play-dl. + * @param player discordjs voice AudioPlayer + * @param resource A {@link YouTubeStream} or {@link SoundCloudStream} + */ +declare function attachListeners(player: EventEmitter, resource: YouTubeStream | SoundCloudStream): void; + +declare const _default: { + DeezerAlbum: typeof DeezerAlbum; + DeezerPlaylist: typeof DeezerPlaylist; + DeezerTrack: typeof DeezerTrack; + SoundCloudPlaylist: typeof SoundCloudPlaylist; + SoundCloudStream: typeof SoundCloudStream; + SoundCloudTrack: typeof SoundCloudTrack; + SpotifyAlbum: typeof SpotifyAlbum; + SpotifyPlaylist: typeof SpotifyPlaylist; + SpotifyTrack: typeof SpotifyTrack; + YouTubeChannel: typeof YouTubeChannel; + YouTubePlayList: typeof YouTubePlayList; + YouTubeVideo: typeof YouTubeVideo; + attachListeners: typeof attachListeners; + authorization: typeof authorization; + decipher_info: typeof decipher_info; + deezer: typeof deezer; + dz_advanced_track_search: typeof dz_advanced_track_search; + dz_validate: typeof dz_validate; + extractID: typeof extractID; + getFreeClientID: typeof getFreeClientID; + is_expired: typeof is_expired; + playlist_info: typeof playlist_info; + refreshToken: typeof refreshToken; + search: typeof search; + setToken: typeof setToken; + so_validate: typeof so_validate; + soundcloud: typeof soundcloud; + spotify: typeof spotify; + sp_validate: typeof sp_validate; + stream: typeof stream; + stream_from_info: typeof stream_from_info; + validate: typeof validate; + video_basic_info: typeof video_basic_info; + video_info: typeof video_info; + yt_validate: typeof yt_validate; +}; + +export { type Deezer, DeezerAlbum, DeezerPlaylist, DeezerTrack, type InfoData, type SoundCloud, SoundCloudPlaylist, SoundCloudStream, SoundCloudTrack, type Spotify, SpotifyAlbum, SpotifyPlaylist, SpotifyTrack, type YouTube, YouTubeChannel, YouTubePlayList, type YouTubeStream, YouTubeVideo, attachListeners, authorization, decipher_info, deezer, _default as default, dz_advanced_track_search, dz_validate, extractID, getFreeClientID, is_expired, playlist_info, refreshToken, search, setToken, so_validate, soundcloud, sp_validate, spotify, stream, stream_from_info, validate, video_basic_info, video_info, yt_validate }; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..246967d --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,2869 @@ +import { Readable, Duplex, DuplexOptions } from 'node:stream'; +import { WebmHeader } from 'play-audio'; +import { EventEmitter } from 'stream'; + +/** + * YouTube Live Stream class for playing audio from Live Stream videos. + */ +declare class LiveStream { + /** + * Readable Stream through which data passes + */ + stream: Readable; + /** + * Type of audio data that we recieved from live stream youtube url. + */ + type: StreamType; + /** + * Incoming message that we recieve. + * + * Storing this is essential. + * This helps to destroy the TCP connection completely if you stopped player in between the stream + */ + private request?; + /** + * Timer that creates loop from interval time provided. + */ + private normal_timer?; + /** + * Timer used to update dash url so as to avoid 404 errors after long hours of streaming. + * + * It updates dash_url every 30 minutes. + */ + private dash_timer; + /** + * Given Dash URL. + */ + private dash_url; + /** + * Base URL in dash manifest file. + */ + private base_url; + /** + * Interval to fetch data again to dash url. + */ + private interval; + /** + * Timer used to update dash url so as to avoid 404 errors after long hours of streaming. + * + * It updates dash_url every 30 minutes. + */ + private video_url; + /** + * No of segments of data to add in stream before starting to loop + */ + private precache; + /** + * Segment sequence number + */ + private sequence; + /** + * Live Stream Class Constructor + * @param dash_url dash manifest URL + * @param target_interval interval time for fetching dash data again + * @param video_url Live Stream video url. + */ + constructor(dash_url: string, interval: number, video_url: string, precache?: number); + /** + * This cleans every used variable in class. + * + * This is used to prevent re-use of this class and helping garbage collector to collect it. + */ + private cleanup; + /** + * Updates dash url. + * + * Used by dash_timer for updating dash_url every 30 minutes. + */ + private dash_updater; + /** + * Initializes dash after getting dash url. + * + * Start if it is first time of initialishing dash function. + */ + private initialize_dash; + /** + * Used only after initializing dash function first time. + * @param len Length of data that you want to + */ + private first_data; + /** + * This loops function in Live Stream Class. + * + * Gets next segment and push it. + */ + private loop; + /** + * Deprecated Functions + */ + pause(): void; + /** + * Deprecated Functions + */ + resume(): void; +} +/** + * YouTube Stream Class for playing audio from normal videos. + */ +declare class Stream { + /** + * Readable Stream through which data passes + */ + stream: Readable; + /** + * Type of audio data that we recieved from normal youtube url. + */ + type: StreamType; + /** + * Audio Endpoint Format Url to get data from. + */ + private url; + /** + * Used to calculate no of bytes data that we have recieved + */ + private bytes_count; + /** + * Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds) + */ + private per_sec_bytes; + /** + * Total length of audio file in bytes + */ + private content_length; + /** + * YouTube video url. [ Used only for retrying purposes only. ] + */ + private video_url; + /** + * Timer for looping data every 265 seconds. + */ + private timer; + /** + * Quality given by user. [ Used only for retrying purposes only. ] + */ + private quality; + /** + * Incoming message that we recieve. + * + * Storing this is essential. + * This helps to destroy the TCP connection completely if you stopped player in between the stream + */ + private request; + /** + * YouTube Stream Class constructor + * @param url Audio Endpoint url. + * @param type Type of Stream + * @param duration Duration of audio playback [ in seconds ] + * @param contentLength Total length of Audio file in bytes. + * @param video_url YouTube video url. + * @param options Options provided to stream function. + */ + constructor(url: string, type: StreamType, duration: number, contentLength: number, video_url: string, options: StreamOptions); + /** + * Retry if we get 404 or 403 Errors. + */ + private retry; + /** + * This cleans every used variable in class. + * + * This is used to prevent re-use of this class and helping garbage collector to collect it. + */ + private cleanup; + /** + * Getting data from audio endpoint url and passing it to stream. + * + * If 404 or 403 occurs, it will retry again. + */ + private loop; + /** + * Pauses timer. + * Stops running of loop. + * + * Useful if you don't want to get excess data to be stored in stream. + */ + pause(): void; + /** + * Resumes timer. + * Starts running of loop. + */ + resume(): void; +} + +declare enum WebmSeekerState { + READING_HEAD = "READING_HEAD", + READING_DATA = "READING_DATA" +} +interface WebmSeekerOptions extends DuplexOptions { + mode?: 'precise' | 'granular'; +} +declare class WebmSeeker extends Duplex { + remaining?: Buffer; + state: WebmSeekerState; + chunk?: Buffer; + cursor: number; + header: WebmHeader; + headfound: boolean; + headerparsed: boolean; + seekfound: boolean; + private data_size; + private offset; + private data_length; + private sec; + private time; + constructor(sec: number, options: WebmSeekerOptions); + private get vint_length(); + private vint_value; + cleanup(): void; + _read(): void; + seek(content_length: number): Error | number; + _write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void; + private readHead; + private readTag; + private getClosestBlock; + private parseEbmlID; + _destroy(error: Error | null, callback: (error: Error | null) => void): void; + _final(callback: (error?: Error | null) => void): void; +} + +/** + * YouTube Stream Class for seeking audio to a timeStamp. + */ +declare class SeekStream { + /** + * WebmSeeker Stream through which data passes + */ + stream: WebmSeeker; + /** + * Type of audio data that we recieved from normal youtube url. + */ + type: StreamType; + /** + * Audio Endpoint Format Url to get data from. + */ + private url; + /** + * Used to calculate no of bytes data that we have recieved + */ + private bytes_count; + /** + * Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds) + */ + private per_sec_bytes; + /** + * Length of the header in bytes + */ + private header_length; + /** + * Total length of audio file in bytes + */ + private content_length; + /** + * YouTube video url. [ Used only for retrying purposes only. ] + */ + private video_url; + /** + * Timer for looping data every 265 seconds. + */ + private timer; + /** + * Quality given by user. [ Used only for retrying purposes only. ] + */ + private quality; + /** + * Incoming message that we recieve. + * + * Storing this is essential. + * This helps to destroy the TCP connection completely if you stopped player in between the stream + */ + private request; + /** + * YouTube Stream Class constructor + * @param url Audio Endpoint url. + * @param type Type of Stream + * @param duration Duration of audio playback [ in seconds ] + * @param headerLength Length of the header in bytes. + * @param contentLength Total length of Audio file in bytes. + * @param bitrate Bitrate provided by YouTube. + * @param video_url YouTube video url. + * @param options Options provided to stream function. + */ + constructor(url: string, duration: number, headerLength: number, contentLength: number, bitrate: number, video_url: string, options: StreamOptions); + /** + * **INTERNAL Function** + * + * Uses stream functions to parse Webm Head and gets Offset byte to seek to. + * @returns Nothing + */ + private seek; + /** + * Retry if we get 404 or 403 Errors. + */ + private retry; + /** + * This cleans every used variable in class. + * + * This is used to prevent re-use of this class and helping garbage collector to collect it. + */ + private cleanup; + /** + * Getting data from audio endpoint url and passing it to stream. + * + * If 404 or 403 occurs, it will retry again. + */ + private loop; + /** + * Pauses timer. + * Stops running of loop. + * + * Useful if you don't want to get excess data to be stored in stream. + */ + pause(): void; + /** + * Resumes timer. + * Starts running of loop. + */ + resume(): void; +} + +interface ChannelIconInterface { + /** + * YouTube Channel Icon URL + */ + url: string; + /** + * YouTube Channel Icon Width + */ + width: number; + /** + * YouTube Channel Icon Height + */ + height: number; +} +/** + * YouTube Channel Class + */ +declare class YouTubeChannel { + /** + * YouTube Channel Title + */ + name?: string; + /** + * YouTube Channel Verified status. + */ + verified?: boolean; + /** + * YouTube Channel artist if any. + */ + artist?: boolean; + /** + * YouTube Channel ID. + */ + id?: string; + /** + * YouTube Class type. == "channel" + */ + type: 'video' | 'playlist' | 'channel'; + /** + * YouTube Channel Url + */ + url?: string; + /** + * YouTube Channel Icons data. + */ + icons?: ChannelIconInterface[]; + /** + * YouTube Channel subscribers count. + */ + subscribers?: string; + /** + * YouTube Channel Constructor + * @param data YouTube Channel data that we recieve from basic info or from search + */ + constructor(data?: any); + /** + * Returns channel icon url + * @param {object} options Icon options + * @param {number} [options.size=0] Icon size. **Default is 0** + */ + iconURL(options?: { + size: number; + }): string | undefined; + /** + * Converts Channel Class to channel name. + * @returns name of channel + */ + toString(): string; + /** + * Converts Channel Class to JSON format + * @returns json data of the channel + */ + toJSON(): ChannelJSON; +} +interface ChannelJSON { + /** + * YouTube Channel Title + */ + name?: string; + /** + * YouTube Channel Verified status. + */ + verified?: boolean; + /** + * YouTube Channel artist if any. + */ + artist?: boolean; + /** + * YouTube Channel ID. + */ + id?: string; + /** + * Type of Class [ Channel ] + */ + type: 'video' | 'playlist' | 'channel'; + /** + * YouTube Channel Url + */ + url?: string; + /** + * YouTube Channel Icon data. + */ + icons?: ChannelIconInterface[]; + /** + * YouTube Channel subscribers count. + */ + subscribers?: string; +} + +declare class YouTubeThumbnail { + url: string; + width: number; + height: number; + constructor(data: any); + toJSON(): { + url: string; + width: number; + height: number; + }; +} + +/** + * Licensed music in the video + * + * The property names change depending on your region's language. + */ +interface VideoMusic { + song?: string; + url?: string | null; + artist?: string; + album?: string; + writers?: string; + licenses?: string; +} +interface VideoOptions { + /** + * YouTube Video ID + */ + id?: string; + /** + * YouTube video url + */ + url: string; + /** + * YouTube Video title + */ + title?: string; + /** + * YouTube Video description. + */ + description?: string; + /** + * YouTube Video Duration Formatted + */ + durationRaw: string; + /** + * YouTube Video Duration in seconds + */ + durationInSec: number; + /** + * YouTube Video Uploaded Date + */ + uploadedAt?: string; + /** + * If the video is upcoming or a premiere that isn't currently live, this will contain the premiere date, for watch page playlists this will be true, it defaults to undefined + */ + upcoming?: Date | true; + /** + * YouTube Views + */ + views: number; + /** + * YouTube Thumbnail Data + */ + thumbnail?: { + width: number | undefined; + height: number | undefined; + url: string | undefined; + }; + /** + * YouTube Video's uploader Channel Data + */ + channel?: YouTubeChannel; + /** + * YouTube Video's likes + */ + likes: number; + /** + * YouTube Video live status + */ + live: boolean; + /** + * YouTube Video private status + */ + private: boolean; + /** + * YouTube Video tags + */ + tags: string[]; + /** + * `true` if the video has been identified by the YouTube community as inappropriate or offensive to some audiences and viewer discretion is advised + */ + discretionAdvised?: boolean; + /** + * Gives info about music content in that video. + * + * The property names of VideoMusic change depending on your region's language. + */ + music?: VideoMusic[]; + /** + * The chapters for this video + * + * If the video doesn't have any chapters or if the video object wasn't created by {@link video_basic_info} or {@link video_info} this will be an empty array. + */ + chapters: VideoChapter[]; +} +interface VideoChapter { + /** + * The title of the chapter + */ + title: string; + /** + * The timestamp of the start of the chapter + */ + timestamp: string; + /** + * The start of the chapter in seconds + */ + seconds: number; + /** + * Thumbnails of the frame at the start of this chapter + */ + thumbnails: YouTubeThumbnail[]; +} +/** + * Class for YouTube Video url + */ +declare class YouTubeVideo { + /** + * YouTube Video ID + */ + id?: string; + /** + * YouTube video url + */ + url: string; + /** + * YouTube Class type. == "video" + */ + type: 'video' | 'playlist' | 'channel'; + /** + * YouTube Video title + */ + title?: string; + /** + * YouTube Video description. + */ + description?: string; + /** + * YouTube Video Duration Formatted + */ + durationRaw: string; + /** + * YouTube Video Duration in seconds + */ + durationInSec: number; + /** + * YouTube Video Uploaded Date + */ + uploadedAt?: string; + /** + * YouTube Live Date + */ + liveAt?: string; + /** + * If the video is upcoming or a premiere that isn't currently live, this will contain the premiere date, for watch page playlists this will be true, it defaults to undefined + */ + upcoming?: Date | true; + /** + * YouTube Views + */ + views: number; + /** + * YouTube Thumbnail Data + */ + thumbnails: YouTubeThumbnail[]; + /** + * YouTube Video's uploader Channel Data + */ + channel?: YouTubeChannel; + /** + * YouTube Video's likes + */ + likes: number; + /** + * YouTube Video live status + */ + live: boolean; + /** + * YouTube Video private status + */ + private: boolean; + /** + * YouTube Video tags + */ + tags: string[]; + /** + * `true` if the video has been identified by the YouTube community as inappropriate or offensive to some audiences and viewer discretion is advised + */ + discretionAdvised?: boolean; + /** + * Gives info about music content in that video. + */ + music?: VideoMusic[]; + /** + * The chapters for this video + * + * If the video doesn't have any chapters or if the video object wasn't created by {@link video_basic_info} or {@link video_info} this will be an empty array. + */ + chapters: VideoChapter[]; + /** + * Constructor for YouTube Video Class + * @param data JSON parsed data. + */ + constructor(data: any); + /** + * Converts class to title name of video. + * @returns Title name + */ + toString(): string; + /** + * Converts class to JSON data + * @returns JSON data. + */ + toJSON(): VideoOptions; +} + +interface LiveStreamData { + isLive: boolean; + dashManifestUrl: string | null; + hlsManifestUrl: string | null; +} +interface formatData { + itag: number; + mimeType: string; + bitrate: number; + width: number; + height: number; + lastModified: string; + contentLength: string; + quality: string; + fps: number; + qualityLabel: string; + projectionType: string; + averageBitrate: number; + audioQuality: string; + approxDurationMs: string; + audioSampleRate: string; + audioChannels: number; + url: string; + signatureCipher: string; + cipher: string; + loudnessDb: number; + targetDurationSec: number; +} +interface InfoData { + LiveStreamData: LiveStreamData; + html5player: string; + format: Partial[]; + video_details: YouTubeVideo; + related_videos: string[]; +} +interface StreamInfoData { + LiveStreamData: LiveStreamData; + html5player: string; + format: Partial[]; + video_details: Pick; +} + +declare enum StreamType { + Arbitrary = "arbitrary", + Raw = "raw", + OggOpus = "ogg/opus", + WebmOpus = "webm/opus", + Opus = "opus" +} +interface StreamOptions { + seek?: number; + quality?: number; + language?: string; + htmldata?: boolean; + precache?: number; + discordPlayerCompatibility?: boolean; +} +/** + * Type for YouTube Stream + */ +type YouTubeStream = Stream | LiveStream | SeekStream; + +/** + * YouTube Playlist Class containing vital informations about playlist. + */ +declare class YouTubePlayList { + /** + * YouTube Playlist ID + */ + id?: string; + /** + * YouTube Playlist Name + */ + title?: string; + /** + * YouTube Class type. == "playlist" + */ + type: 'video' | 'playlist' | 'channel'; + /** + * Total no of videos in that playlist + */ + videoCount?: number; + /** + * Time when playlist was last updated + */ + lastUpdate?: string; + /** + * Total views of that playlist + */ + views?: number; + /** + * YouTube Playlist url + */ + url?: string; + /** + * YouTube Playlist url with starting video url. + */ + link?: string; + /** + * YouTube Playlist channel data + */ + channel?: YouTubeChannel; + /** + * YouTube Playlist thumbnail Data + */ + thumbnail?: YouTubeThumbnail; + /** + * Videos array containing data of first 100 videos + */ + private videos?; + /** + * Map contaning data of all fetched videos + */ + private fetched_videos; + /** + * Token containing API key, Token, ClientVersion. + */ + private _continuation; + /** + * Total no of pages count. + */ + private __count; + /** + * Constructor for YouTube Playlist Class + * @param data Json Parsed YouTube Playlist data + * @param searchResult If the data is from search or not + */ + constructor(data: any, searchResult?: boolean); + /** + * Updates variable according to a normal data. + * @param data Json Parsed YouTube Playlist data + */ + private __patch; + /** + * Updates variable according to a searched data. + * @param data Json Parsed YouTube Playlist data + */ + private __patchSearch; + /** + * Parses next segment of videos from playlist and returns parsed data. + * @param limit Total no of videos to parse. + * + * Default = Infinity + * @returns Array of YouTube Video Class + */ + next(limit?: number): Promise; + /** + * Fetches remaining data from playlist + * + * For fetching and getting all songs data, see `total_pages` property. + * @param max Max no of videos to fetch + * + * Default = Infinity + * @returns + */ + fetch(max?: number): Promise; + /** + * YouTube Playlists are divided into pages. + * + * For example, if you want to get 101 - 200 songs + * + * ```ts + * const playlist = await play.playlist_info('playlist url') + * + * await playlist.fetch() + * + * const result = playlist.page(2) + * ``` + * @param number Page number + * @returns Array of YouTube Video Class + * @see {@link YouTubePlayList.all_videos} + */ + page(number: number): YouTubeVideo[]; + /** + * Gets total number of pages in that playlist class. + * @see {@link YouTubePlayList.all_videos} + */ + get total_pages(): number; + /** + * This tells total number of videos that have been fetched so far. + * + * This can be equal to videosCount if all videos in playlist have been fetched and they are not hidden. + */ + get total_videos(): number; + /** + * Fetches all the videos in the playlist and returns them + * + * ```ts + * const playlist = await play.playlist_info('playlist url') + * + * const videos = await playlist.all_videos() + * ``` + * @returns An array of {@link YouTubeVideo} objects + * @see {@link YouTubePlayList.fetch} + */ + all_videos(): Promise; + /** + * Converts Playlist Class to a json parsed data. + * @returns + */ + toJSON(): PlaylistJSON$2; +} +interface PlaylistJSON$2 { + /** + * YouTube Playlist ID + */ + id?: string; + /** + * YouTube Playlist Name + */ + title?: string; + /** + * Total no of videos in that playlist + */ + videoCount?: number; + /** + * Time when playlist was last updated + */ + lastUpdate?: string; + /** + * Total views of that playlist + */ + views?: number; + /** + * YouTube Playlist url + */ + url?: string; + /** + * YouTube Playlist url with starting video url. + */ + link?: string; + /** + * YouTube Playlist channel data + */ + channel?: YouTubeChannel; + /** + * YouTube Playlist thumbnail Data + */ + thumbnail?: { + width: number | undefined; + height: number | undefined; + url: string | undefined; + }; + /** + * first 100 videos in that playlist + */ + videos?: YouTubeVideo[]; +} + +interface InfoOptions { + htmldata?: boolean; + language?: string; +} +interface PlaylistOptions { + incomplete?: boolean; + language?: string; +} +/** + * Validate YouTube URL or ID. + * + * **CAUTION :** If your search word is 11 or 12 characters long, you might get it validated as video ID. + * + * To avoid above, add one more condition to yt_validate + * ```ts + * if (url.startsWith('https') && yt_validate(url) === 'video') { + * // YouTube Video Url. + * } + * ``` + * @param url YouTube URL OR ID + * @returns + * ``` + * 'playlist' | 'video' | 'search' | false + * ``` + */ +declare function yt_validate(url: string): 'playlist' | 'video' | 'search' | false; +/** + * Extract ID of YouTube url. + * @param url ID or url of YouTube + * @returns ID of video or playlist. + */ +declare function extractID(url: string): string; +/** + * Basic function to get data from a YouTube url or ID. + * + * Example + * ```ts + * const video = await play.video_basic_info('youtube video url') + * + * const res = ... // Any https package get function. + * + * const video = await play.video_basic_info(res.body, { htmldata : true }) + * ``` + * @param url YouTube url or ID or html body data + * @param options Video Info Options + * - `boolean` htmldata : given data is html data or not + * @returns Video Basic Info {@link InfoData}. + */ +declare function video_basic_info(url: string, options?: InfoOptions): Promise; +/** + * Gets data from YouTube url or ID or html body data and deciphers it. + * ``` + * video_basic_info + decipher_info = video_info + * ``` + * + * Example + * ```ts + * const video = await play.video_info('youtube video url') + * + * const res = ... // Any https package get function. + * + * const video = await play.video_info(res.body, { htmldata : true }) + * ``` + * @param url YouTube url or ID or html body data + * @param options Video Info Options + * - `boolean` htmldata : given data is html data or not + * @returns Deciphered Video Info {@link InfoData}. + */ +declare function video_info(url: string, options?: InfoOptions): Promise; +/** + * Function uses data from video_basic_info and deciphers it if it contains signatures. + * @param data Data - {@link InfoData} + * @param audio_only `boolean` - To decipher only audio formats only. + * @returns Deciphered Video Info {@link InfoData} + */ +declare function decipher_info(data: T, audio_only?: boolean): Promise; +/** + * Gets YouTube playlist info from a playlist url. + * + * Example + * ```ts + * const playlist = await play.playlist_info('youtube playlist url') + * + * const playlist = await play.playlist_info('youtube playlist url', { incomplete : true }) + * ``` + * @param url Playlist URL + * @param options Playlist Info Options + * - `boolean` incomplete : When this is set to `false` (default) this function will throw an error + * if the playlist contains hidden videos. + * If it is set to `true`, it parses the playlist skipping the hidden videos, + * only visible videos are included in the resulting {@link YouTubePlaylist}. + * + * @returns YouTube Playlist + */ +declare function playlist_info(url: string, options?: PlaylistOptions): Promise; + +/** + * Type for YouTube returns + */ +type YouTube = YouTubeVideo | YouTubeChannel | YouTubePlayList; + +interface TrackJSON { + /** + * Spotify Track Name + */ + name: string; + /** + * Spotify Track ID + */ + id: string; + /** + * Spotify Track url + */ + url: string; + /** + * Spotify Track explicit info. + */ + explicit: boolean; + /** + * Spotify Track Duration in seconds + */ + durationInSec: number; + /** + * Spotify Track Duration in milli seconds + */ + durationInMs: number; + /** + * Spotify Track Artists data [ array ] + */ + artists: SpotifyArtists[]; + /** + * Spotify Track Album data + */ + album: SpotifyTrackAlbum | undefined; + /** + * Spotify Track Thumbnail Data + */ + thumbnail: SpotifyThumbnail | undefined; +} +interface PlaylistJSON$1 { + /** + * Spotify Playlist Name + */ + name: string; + /** + * Spotify Playlist collaborative boolean. + */ + collaborative: boolean; + /** + * Spotify Playlist Description + */ + description: string; + /** + * Spotify Playlist URL + */ + url: string; + /** + * Spotify Playlist ID + */ + id: string; + /** + * Spotify Playlist Thumbnail Data + */ + thumbnail: SpotifyThumbnail; + /** + * Spotify Playlist Owner Artist data + */ + owner: SpotifyArtists; + /** + * Spotify Playlist total tracks Count + */ + tracksCount: number; +} +interface AlbumJSON { + /** + * Spotify Album Name + */ + name: string; + /** + * Spotify Class type. == "album" + */ + type: 'track' | 'playlist' | 'album'; + /** + * Spotify Album url + */ + url: string; + /** + * Spotify Album id + */ + id: string; + /** + * Spotify Album Thumbnail data + */ + thumbnail: SpotifyThumbnail; + /** + * Spotify Album artists [ array ] + */ + artists: SpotifyArtists[]; + /** + * Spotify Album copyright data [ array ] + */ + copyrights: SpotifyCopyright[]; + /** + * Spotify Album Release date + */ + release_date: string; + /** + * Spotify Album Release Date **precise** + */ + release_date_precision: string; + /** + * Spotify Album total no of tracks + */ + tracksCount: number; +} + +interface SpotifyTrackAlbum { + /** + * Spotify Track Album name + */ + name: string; + /** + * Spotify Track Album url + */ + url: string; + /** + * Spotify Track Album id + */ + id: string; + /** + * Spotify Track Album release date + */ + release_date: string; + /** + * Spotify Track Album release date **precise** + */ + release_date_precision: string; + /** + * Spotify Track Album total tracks number + */ + total_tracks: number; +} +interface SpotifyArtists { + /** + * Spotify Artist Name + */ + name: string; + /** + * Spotify Artist Url + */ + url: string; + /** + * Spotify Artist ID + */ + id: string; +} +interface SpotifyThumbnail { + /** + * Spotify Thumbnail height + */ + height: number; + /** + * Spotify Thumbnail width + */ + width: number; + /** + * Spotify Thumbnail url + */ + url: string; +} +interface SpotifyCopyright { + /** + * Spotify Copyright Text + */ + text: string; + /** + * Spotify Copyright Type + */ + type: string; +} +/** + * Spotify Track Class + */ +declare class SpotifyTrack { + /** + * Spotify Track Name + */ + name: string; + /** + * Spotify Class type. == "track" + */ + type: 'track' | 'playlist' | 'album'; + /** + * Spotify Track ID + */ + id: string; + /** + * Spotify Track ISRC + */ + isrc: string; + /** + * Spotify Track url + */ + url: string; + /** + * Spotify Track explicit info. + */ + explicit: boolean; + /** + * Spotify Track playability info. + */ + playable: boolean; + /** + * Spotify Track Duration in seconds + */ + durationInSec: number; + /** + * Spotify Track Duration in milli seconds + */ + durationInMs: number; + /** + * Spotify Track Artists data [ array ] + */ + artists: SpotifyArtists[]; + /** + * Spotify Track Album data + */ + album: SpotifyTrackAlbum | undefined; + /** + * Spotify Track Thumbnail Data + */ + thumbnail: SpotifyThumbnail | undefined; + /** + * Constructor for Spotify Track + * @param data + */ + constructor(data: any); + toJSON(): TrackJSON; +} +/** + * Spotify Playlist Class + */ +declare class SpotifyPlaylist { + /** + * Spotify Playlist Name + */ + name: string; + /** + * Spotify Class type. == "playlist" + */ + type: 'track' | 'playlist' | 'album'; + /** + * Spotify Playlist collaborative boolean. + */ + collaborative: boolean; + /** + * Spotify Playlist Description + */ + description: string; + /** + * Spotify Playlist URL + */ + url: string; + /** + * Spotify Playlist ID + */ + id: string; + /** + * Spotify Playlist Thumbnail Data + */ + thumbnail: SpotifyThumbnail; + /** + * Spotify Playlist Owner Artist data + */ + owner: SpotifyArtists; + /** + * Spotify Playlist total tracks Count + */ + tracksCount: number; + /** + * Spotify Playlist Spotify data + * + * @private + */ + private spotifyData; + /** + * Spotify Playlist fetched tracks Map + * + * @private + */ + private fetched_tracks; + /** + * Boolean to tell whether it is a searched result or not. + */ + private readonly search; + /** + * Constructor for Spotify Playlist Class + * @param data JSON parsed data of playlist + * @param spotifyData Data about sporify token for furhter fetching. + */ + constructor(data: any, spotifyData: SpotifyDataOptions, search: boolean); + /** + * Fetches Spotify Playlist tracks more than 100 tracks. + * + * For getting all tracks in playlist, see `total_pages` property. + * @returns Playlist Class. + */ + fetch(): Promise; + /** + * Spotify Playlist tracks are divided in pages. + * + * For example getting data of 101 - 200 videos in a playlist, + * + * ```ts + * const playlist = await play.spotify('playlist url') + * + * await playlist.fetch() + * + * const result = playlist.page(2) + * ``` + * @param num Page Number + * @returns + */ + page(num: number): SpotifyTrack[]; + /** + * Gets total number of pages in that playlist class. + * @see {@link SpotifyPlaylist.all_tracks} + */ + get total_pages(): number; + /** + * Spotify Playlist total no of tracks that have been fetched so far. + */ + get total_tracks(): number; + /** + * Fetches all the tracks in the playlist and returns them + * + * ```ts + * const playlist = await play.spotify('playlist url') + * + * const tracks = await playlist.all_tracks() + * ``` + * @returns An array of {@link SpotifyTrack} + */ + all_tracks(): Promise; + /** + * Converts Class to JSON + * @returns JSON data + */ + toJSON(): PlaylistJSON$1; +} +/** + * Spotify Album Class + */ +declare class SpotifyAlbum { + /** + * Spotify Album Name + */ + name: string; + /** + * Spotify Class type. == "album" + */ + type: 'track' | 'playlist' | 'album'; + /** + * Spotify Album url + */ + url: string; + /** + * Spotify Album id + */ + id: string; + /** + * Spotify Album Thumbnail data + */ + thumbnail: SpotifyThumbnail; + /** + * Spotify Album artists [ array ] + */ + artists: SpotifyArtists[]; + /** + * Spotify Album copyright data [ array ] + */ + copyrights: SpotifyCopyright[]; + /** + * Spotify Album Release date + */ + release_date: string; + /** + * Spotify Album Release Date **precise** + */ + release_date_precision: string; + /** + * Spotify Album total no of tracks + */ + tracksCount: number; + /** + * Spotify Album Spotify data + * + * @private + */ + private spotifyData; + /** + * Spotify Album fetched tracks Map + * + * @private + */ + private fetched_tracks; + /** + * Boolean to tell whether it is a searched result or not. + */ + private readonly search; + /** + * Constructor for Spotify Album Class + * @param data Json parsed album data + * @param spotifyData Spotify credentials + */ + constructor(data: any, spotifyData: SpotifyDataOptions, search: boolean); + /** + * Fetches Spotify Album tracks more than 50 tracks. + * + * For getting all tracks in album, see `total_pages` property. + * @returns Album Class. + */ + fetch(): Promise; + /** + * Spotify Album tracks are divided in pages. + * + * For example getting data of 51 - 100 videos in a album, + * + * ```ts + * const album = await play.spotify('album url') + * + * await album.fetch() + * + * const result = album.page(2) + * ``` + * @param num Page Number + * @returns + */ + page(num: number): SpotifyTrack[] | undefined; + /** + * Gets total number of pages in that album class. + * @see {@link SpotifyAlbum.all_tracks} + */ + get total_pages(): number; + /** + * Spotify Album total no of tracks that have been fetched so far. + */ + get total_tracks(): number; + /** + * Fetches all the tracks in the album and returns them + * + * ```ts + * const album = await play.spotify('album url') + * + * const tracks = await album.all_tracks() + * ``` + * @returns An array of {@link SpotifyTrack} + */ + all_tracks(): Promise; + /** + * Converts Class to JSON + * @returns JSON data + */ + toJSON(): AlbumJSON; +} + +/** + * Spotify Data options that are stored in spotify.data file. + */ +interface SpotifyDataOptions { + client_id: string; + client_secret: string; + redirect_url?: string; + authorization_code?: string; + access_token?: string; + refresh_token?: string; + token_type?: string; + expires_in?: number; + expiry?: number; + market?: string; + file?: boolean; +} +/** + * Gets Spotify url details. + * + * ```ts + * let spot = await play.spotify('spotify url') + * + * // spot.type === "track" | "playlist" | "album" + * + * if (spot.type === "track") { + * spot = spot as play.SpotifyTrack + * // Code with spotify track class. + * } + * ``` + * @param url Spotify Url + * @returns A {@link SpotifyTrack} or {@link SpotifyPlaylist} or {@link SpotifyAlbum} + */ +declare function spotify(url: string): Promise; +/** + * Validate Spotify url + * @param url Spotify URL + * @returns + * ```ts + * 'track' | 'playlist' | 'album' | 'search' | false + * ``` + */ +declare function sp_validate(url: string): 'track' | 'playlist' | 'album' | 'search' | false; +/** + * Checks if spotify token is expired or not. + * + * Update token if returned false. + * ```ts + * if (play.is_expired()) { + * await play.refreshToken() + * } + * ``` + * @returns boolean + */ +declare function is_expired(): boolean; +/** + * type for Spotify Classes + */ +type Spotify = SpotifyAlbum | SpotifyPlaylist | SpotifyTrack; +/** + * Refreshes Token + * + * ```ts + * if (play.is_expired()) { + * await play.refreshToken() + * } + * ``` + * @returns boolean + */ +declare function refreshToken(): Promise; + +interface SoundTrackJSON { + /** + * SoundCloud Track Name + */ + name: string; + /** + * SoundCloud Track ID + */ + id: number; + /** + * SoundCloud Track url + */ + url: string; + /** + * User friendly SoundCloud track URL + */ + permalink: string; + /** + * SoundCloud Track fetched status + */ + fetched: boolean; + /** + * SoundCloud Track Duration in seconds + */ + durationInSec: number; + /** + * SoundCloud Track Duration in miili seconds + */ + durationInMs: number; + /** + * SoundCloud Track formats data + */ + formats: SoundCloudTrackFormat[]; + /** + * SoundCloud Track Publisher Data + */ + publisher: { + name: string; + id: number; + artist: string; + contains_music: boolean; + writer_composer: string; + } | null; + /** + * SoundCloud Track thumbnail + */ + thumbnail: string; + /** + * SoundCloud Track user data + */ + user: SoundCloudUser; +} +interface PlaylistJSON { + /** + * SoundCloud Playlist Name + */ + name: string; + /** + * SoundCloud Playlist ID + */ + id: number; + /** + * SoundCloud Playlist URL + */ + url: string; + /** + * SoundCloud Playlist Sub type. == "album" for soundcloud albums + */ + sub_type: string; + /** + * SoundCloud Playlist Total Duration in seconds + */ + durationInSec: number; + /** + * SoundCloud Playlist Total Duration in milli seconds + */ + durationInMs: number; + /** + * SoundCloud Playlist user data + */ + user: SoundCloudUser; + /** + * SoundCloud Playlist tracks [ It can be fetched or not fetched ] + */ + tracks: SoundCloudTrack[] | SoundCloudTrackDeprecated[]; + /** + * SoundCloud Playlist tracks number + */ + tracksCount: number; +} + +interface SoundCloudUser { + /** + * SoundCloud User Name + */ + name: string; + /** + * SoundCloud User ID + */ + id: string; + /** + * SoundCloud User URL + */ + url: string; + /** + * SoundCloud Class type. == "user" + */ + type: 'track' | 'playlist' | 'user'; + /** + * SoundCloud User Verified status + */ + verified: boolean; + /** + * SoundCloud User Description + */ + description: string; + /** + * SoundCloud User First Name + */ + first_name: string; + /** + * SoundCloud User Full Name + */ + full_name: string; + /** + * SoundCloud User Last Name + */ + last_name: string; + /** + * SoundCloud User thumbnail URL + */ + thumbnail: string; +} +interface SoundCloudTrackDeprecated { + /** + * SoundCloud Track fetched status + */ + fetched: boolean; + /** + * SoundCloud Track ID + */ + id: number; + /** + * SoundCloud Class type. == "track" + */ + type: 'track'; +} +interface SoundCloudTrackFormat { + /** + * SoundCloud Track Format Url + */ + url: string; + /** + * SoundCloud Track Format preset + */ + preset: string; + /** + * SoundCloud Track Format Duration + */ + duration: number; + /** + * SoundCloud Track Format data containing protocol and mime_type + */ + format: { + protocol: string; + mime_type: string; + }; + /** + * SoundCloud Track Format quality + */ + quality: string; +} +/** + * SoundCloud Track Class + */ +declare class SoundCloudTrack { + /** + * SoundCloud Track Name + */ + name: string; + /** + * SoundCloud Track ID + */ + id: number; + /** + * SoundCloud Track url + */ + url: string; + /** + * User friendly SoundCloud track URL + */ + permalink: string; + /** + * SoundCloud Track fetched status + */ + fetched: boolean; + /** + * SoundCloud Class type. === "track" + */ + type: 'track' | 'playlist' | 'user'; + /** + * SoundCloud Track Duration in seconds + */ + durationInSec: number; + /** + * SoundCloud Track Duration in miili seconds + */ + durationInMs: number; + /** + * SoundCloud Track formats data + */ + formats: SoundCloudTrackFormat[]; + /** + * SoundCloud Track Publisher Data + */ + publisher: { + name: string; + id: number; + artist: string; + contains_music: boolean; + writer_composer: string; + } | null; + /** + * SoundCloud Track thumbnail + */ + thumbnail: string; + /** + * SoundCloud Track user data + */ + user: SoundCloudUser; + /** + * Constructor for SoundCloud Track Class + * @param data JSON parsed track html data + */ + constructor(data: any); + /** + * Converts class to JSON + * @returns JSON parsed Data + */ + toJSON(): SoundTrackJSON; +} +/** + * SoundCloud Playlist Class + */ +declare class SoundCloudPlaylist { + /** + * SoundCloud Playlist Name + */ + name: string; + /** + * SoundCloud Playlist ID + */ + id: number; + /** + * SoundCloud Playlist URL + */ + url: string; + /** + * SoundCloud Class type. == "playlist" + */ + type: 'track' | 'playlist' | 'user'; + /** + * SoundCloud Playlist Sub type. == "album" for soundcloud albums + */ + sub_type: string; + /** + * SoundCloud Playlist Total Duration in seconds + */ + durationInSec: number; + /** + * SoundCloud Playlist Total Duration in milli seconds + */ + durationInMs: number; + /** + * SoundCloud Playlist user data + */ + user: SoundCloudUser; + /** + * SoundCloud Playlist tracks [ It can be fetched or not fetched ] + */ + tracks: SoundCloudTrack[] | SoundCloudTrackDeprecated[]; + /** + * SoundCloud Playlist tracks number + */ + tracksCount: number; + /** + * SoundCloud Client ID provided by user + * @private + */ + private client_id; + /** + * Constructor for SoundCloud Playlist + * @param data JSON parsed SoundCloud playlist data + * @param client_id Provided SoundCloud Client ID + */ + constructor(data: any, client_id: string); + /** + * Fetches all unfetched songs in a playlist. + * + * For fetching songs and getting all songs, see `fetched_tracks` property. + * @returns playlist class + */ + fetch(): Promise; + /** + * Get total no. of fetched tracks + * @see {@link SoundCloudPlaylist.all_tracks} + */ + get total_tracks(): number; + /** + * Fetches all the tracks in the playlist and returns them + * + * ```ts + * const playlist = await play.soundcloud('playlist url') + * + * const tracks = await playlist.all_tracks() + * ``` + * @returns An array of {@link SoundCloudTrack} + */ + all_tracks(): Promise; + /** + * Converts Class to JSON data + * @returns JSON parsed data + */ + toJSON(): PlaylistJSON; +} +/** + * SoundCloud Stream class + */ +declare class SoundCloudStream { + /** + * Readable Stream through which data passes + */ + stream: Readable; + /** + * Type of audio data that we recieved from normal youtube url. + */ + type: StreamType; + /** + * Dash Url containing segment urls. + * @private + */ + private url; + /** + * Total time of downloaded segments data. + * @private + */ + private downloaded_time; + /** + * Timer for looping code every 5 minutes + * @private + */ + private timer; + /** + * Total segments Downloaded so far + * @private + */ + private downloaded_segments; + /** + * Incoming message that we recieve. + * + * Storing this is essential. + * This helps to destroy the TCP connection completely if you stopped player in between the stream + * @private + */ + private request; + /** + * Array of segment time. Useful for calculating downloaded_time. + */ + private time; + /** + * Array of segment_urls in dash file. + */ + private segment_urls; + /** + * Constructor for SoundCloud Stream + * @param url Dash url containing dash file. + * @param type Stream Type + */ + constructor(url: string, type?: StreamType); + /** + * Parses SoundCloud dash file. + * @private + */ + private parser; + /** + * Starts looping of code for getting all segments urls data + */ + private start; + /** + * Main Loop function for getting all segments urls data + */ + private loop; + /** + * This cleans every used variable in class. + * + * This is used to prevent re-use of this class and helping garbage collector to collect it. + */ + private cleanup; + /** + * Pauses timer. + * Stops running of loop. + * + * Useful if you don't want to get excess data to be stored in stream. + */ + pause(): void; + /** + * Resumes timer. + * Starts running of loop. + */ + resume(): void; +} + +/** + * Gets info from a soundcloud url. + * + * ```ts + * let sound = await play.soundcloud('soundcloud url') + * + * // sound.type === "track" | "playlist" | "user" + * + * if (sound.type === "track") { + * spot = spot as play.SoundCloudTrack + * // Code with SoundCloud track class. + * } + * ``` + * @param url soundcloud url + * @returns A {@link SoundCloudTrack} or {@link SoundCloudPlaylist} + */ +declare function soundcloud(url: string): Promise; +/** + * Type of SoundCloud + */ +type SoundCloud = SoundCloudTrack | SoundCloudPlaylist; +/** + * Gets Free SoundCloud Client ID. + * + * Use this in beginning of your code to add SoundCloud support. + * + * ```ts + * play.getFreeClientID().then((clientID) => play.setToken({ + * soundcloud : { + * client_id : clientID + * } + * })) + * ``` + * @returns client ID + */ +declare function getFreeClientID(): Promise; +/** + * Validates a soundcloud url + * @param url soundcloud url + * @returns + * ```ts + * false | 'track' | 'playlist' + * ``` + */ +declare function so_validate(url: string): Promise; + +/** + * Interface representing an image on Deezer + * available in four sizes + */ +interface DeezerImage { + /** + * The largest version of the image + */ + xl: string; + /** + * The second largest version of the image + */ + big: string; + /** + * The second smallest version of the image + */ + medium: string; + /** + * The smallest version of the image + */ + small: string; +} +/** + * Interface representing a Deezer genre + */ +interface DeezerGenre { + /** + * The name of the genre + */ + name: string; + /** + * The thumbnail of the genre available in four sizes + */ + picture: DeezerImage; +} +/** + * Interface representing a Deezer user account + */ +interface DeezerUser { + /** + * The id of the user + */ + id: number; + /** + * The name of the user + */ + name: string; +} +/** + * Class representing a Deezer track + */ +declare class DeezerTrack { + /** + * The id of the track + */ + id: number; + /** + * The title of the track + */ + title: string; + /** + * A shorter version of the title + */ + shortTitle: string; + /** + * The URL of the track on Deezer + */ + url: string; + /** + * The duration of the track in seconds + */ + durationInSec: number; + /** + * The rank of the track + */ + rank: number; + /** + * `true` if the track contains any explicit lyrics + */ + explicit: boolean; + /** + * URL to a file containing the first 30 seconds of the track + */ + previewURL: string; + /** + * The artist of the track + */ + artist: DeezerArtist; + /** + * The album that this track is in + */ + album: DeezerTrackAlbum; + /** + * The type, always `'track'`, useful to determine what the deezer function returned + */ + type: 'track' | 'playlist' | 'album'; + /** + * Signifies that some properties are not populated + * + * Partial tracks can be populated by calling {@link DeezerTrack.fetch}. + * + * `true` for tracks in search results and `false` if the track was fetched directly or expanded. + */ + partial: boolean; + /** + * The position of the track in the album + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + trackPosition?: number; + /** + * The number of the disk the track is on + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + diskNumber?: number; + /** + * The release date + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + releaseDate?: Date; + /** + * The number of beats per minute + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + bpm?: number; + /** + * The gain of the track + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + gain?: number; + /** + * The artists that have contributed to the track + * + * `undefined` for partial tracks + * + * @see {@link DeezerTrack.partial} + */ + contributors?: DeezerArtist[]; + /** + * Creates a Deezer track from the data in an API response + * @param data the data to use to create the track + * @param partial Whether the track should be partial + * @see {@link DeezerTrack.partial} + */ + constructor(data: any, partial: boolean); + /** + * Fetches and populates the missing fields + * + * The property {@link partial} will be `false` if this method finishes successfully. + * + * @returns A promise with the same track this method was called on. + */ + fetch(): Promise; + /** + * Converts instances of this class to JSON data + * @returns JSON data. + */ + toJSON(): { + id: number; + title: string; + shortTitle: string; + url: string; + durationInSec: number; + rank: number; + explicit: boolean; + previewURL: string; + artist: DeezerArtist; + album: DeezerTrackAlbum; + type: "playlist" | "album" | "track"; + trackPosition: number | undefined; + diskNumber: number | undefined; + releaseDate: Date | undefined; + bpm: number | undefined; + gain: number | undefined; + contributors: DeezerArtist[] | undefined; + }; +} +/** + * Class for Deezer Albums + */ +declare class DeezerAlbum { + /** + * The id of the album + */ + id: number; + /** + * The title of the album + */ + title: string; + /** + * The URL to the album on Deezer + */ + url: string; + /** + * The record type of the album (e.g. EP, ALBUM, etc ...) + */ + recordType: string; + /** + * `true` if the album contains any explicit lyrics + */ + explicit: boolean; + /** + * The artist of the album + */ + artist: DeezerArtist; + /** + * The album cover available in four sizes + */ + cover: DeezerImage; + /** + * The type, always `'album'`, useful to determine what the deezer function returned + */ + type: 'track' | 'playlist' | 'album'; + /** + * The number of tracks in the album + */ + tracksCount: number; + /** + * Signifies that some properties are not populated + * + * Partial albums can be populated by calling {@link DeezerAlbum.fetch}. + * + * `true` for albums in search results and `false` if the album was fetched directly or expanded. + */ + partial: boolean; + /** + * The **u**niversal **p**roduct **c**ode of the album + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + upc?: string; + /** + * The duration of the album in seconds + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + durationInSec?: number; + /** + * The number of fans the album has + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + numberOfFans?: number; + /** + * The release date of the album + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + releaseDate?: Date; + /** + * Whether the album is available + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + available?: boolean; + /** + * The list of genres present in this album + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + genres?: DeezerGenre[]; + /** + * The contributors to the album + * + * `undefined` for partial albums + * + * @see {@link DeezerAlbum.partial} + */ + contributors?: DeezerArtist[]; + /** + * The list of tracks in the album + * + * empty (length === 0) for partial albums + * + * Use {@link DeezerAlbum.fetch} to populate the tracks and other properties + * + * @see {@link DeezerAlbum.partial} + */ + tracks: DeezerTrack[]; + /** + * Creates a Deezer album from the data in an API response + * @param data the data to use to create the album + * @param partial Whether the album should be partial + * @see {@link DeezerAlbum.partial} + */ + constructor(data: any, partial: boolean); + /** + * Fetches and populates the missing fields including all tracks. + * + * The property {@link DeezerAlbum.partial} will be `false` if this method finishes successfully. + * + * @returns A promise with the same album this method was called on. + */ + fetch(): Promise; + /** + * Fetches all the tracks in the album and returns them + * + * ```ts + * const album = await play.deezer('album url') + * + * const tracks = await album.all_tracks() + * ``` + * @returns An array of {@link DeezerTrack} + */ + all_tracks(): Promise; + /** + * Converts instances of this class to JSON data + * @returns JSON data. + */ + toJSON(): { + id: number; + title: string; + url: string; + recordType: string; + explicit: boolean; + artist: DeezerArtist; + cover: DeezerImage; + type: "playlist" | "album" | "track"; + upc: string | undefined; + tracksCount: number; + durationInSec: number | undefined; + numberOfFans: number | undefined; + releaseDate: Date | undefined; + available: boolean | undefined; + genres: DeezerGenre[] | undefined; + contributors: DeezerArtist[] | undefined; + tracks: { + id: number; + title: string; + shortTitle: string; + url: string; + durationInSec: number; + rank: number; + explicit: boolean; + previewURL: string; + artist: DeezerArtist; + album: DeezerTrackAlbum; + type: "playlist" | "album" | "track"; + trackPosition: number | undefined; + diskNumber: number | undefined; + releaseDate: Date | undefined; + bpm: number | undefined; + gain: number | undefined; + contributors: DeezerArtist[] | undefined; + }[]; + }; +} +/** + * Class for Deezer Playlists + */ +declare class DeezerPlaylist { + /** + * The id of the playlist + */ + id: number; + /** + * The title of the playlist + */ + title: string; + /** + * Whether the playlist is public or private + */ + public: boolean; + /** + * The URL of the playlist on Deezer + */ + url: string; + /** + * Cover picture of the playlist available in four sizes + */ + picture: DeezerImage; + /** + * The date of the playlist's creation + */ + creationDate: Date; + /** + * The type, always `'playlist'`, useful to determine what the deezer function returned + */ + type: 'track' | 'playlist' | 'album'; + /** + * The Deezer user that created the playlist + */ + creator: DeezerUser; + /** + * The number of tracks in the playlist + */ + tracksCount: number; + /** + * Signifies that some properties are not populated + * + * Partial playlists can be populated by calling {@link DeezerPlaylist.fetch}. + * + * `true` for playlists in search results and `false` if the album was fetched directly or expanded. + */ + partial: boolean; + /** + * Description of the playlist + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + description?: string; + /** + * Duration of the playlist in seconds + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + durationInSec?: number; + /** + * `true` if the playlist is the loved tracks playlist + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + isLoved?: boolean; + /** + * Whether multiple users have worked on the playlist + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + collaborative?: boolean; + /** + * The number of fans the playlist has + * + * `undefined` for partial playlists + * + * @see {@link DeezerPlaylist.partial} + */ + fans?: number; + /** + * The list of tracks in the playlist + * + * empty (length === 0) for partial and non public playlists + * + * Use {@link DeezerPlaylist.fetch} to populate the tracks and other properties + * + * @see {@link DeezerPlaylist.partial} + * @see {@link DeezerPlaylist.public} + */ + tracks: DeezerTrack[]; + /** + * Creates a Deezer playlist from the data in an API response + * @param data the data to use to create the playlist + * @param partial Whether the playlist should be partial + * @see {@link DeezerPlaylist.partial} + */ + constructor(data: any, partial: boolean); + /** + * Fetches and populates the missing fields, including all tracks. + * + * The property {@link DeezerPlaylist.partial} will be `false` if this method finishes successfully. + * + * @returns A promise with the same playlist this method was called on. + */ + fetch(): Promise; + /** + * Fetches all the tracks in the playlist and returns them + * + * ```ts + * const playlist = await play.deezer('playlist url') + * + * const tracks = await playlist.all_tracks() + * ``` + * @returns An array of {@link DeezerTrack} + */ + all_tracks(): Promise; + /** + * Converts instances of this class to JSON data + * @returns JSON data. + */ + toJSON(): { + id: number; + title: string; + public: boolean; + url: string; + picture: DeezerImage; + creationDate: Date; + type: "playlist" | "album" | "track"; + creator: DeezerUser; + tracksCount: number; + description: string | undefined; + durationInSec: number | undefined; + isLoved: boolean | undefined; + collaborative: boolean | undefined; + fans: number | undefined; + tracks: { + id: number; + title: string; + shortTitle: string; + url: string; + durationInSec: number; + rank: number; + explicit: boolean; + previewURL: string; + artist: DeezerArtist; + album: DeezerTrackAlbum; + type: "playlist" | "album" | "track"; + trackPosition: number | undefined; + diskNumber: number | undefined; + releaseDate: Date | undefined; + bpm: number | undefined; + gain: number | undefined; + contributors: DeezerArtist[] | undefined; + }[]; + }; +} +declare class DeezerTrackAlbum { + id: number; + title: string; + url: string; + cover: DeezerImage; + releaseDate?: Date; + constructor(data: any); +} +/** + * Class representing a Deezer artist + */ +declare class DeezerArtist { + /** + * The id of the artist + */ + id: number; + /** + * The name of the artist + */ + name: string; + /** + * The URL of the artist on Deezer + */ + url: string; + /** + * The picture of the artist available in four sizes + */ + picture?: DeezerImage; + /** + * The of the artist on the track + */ + role?: string; + constructor(data: any); +} + +interface DeezerAdvancedSearchOptions { + /** + * The maximum number of results to return, maximum `100`, defaults to `10`. + */ + limit?: number; + /** + * The name of the artist. + */ + artist?: string; + /** + * The title of the album. + */ + album?: string; + /** + * The title of the track. + */ + title?: string; + /** + * The label that released the track. + */ + label?: string; + /** + * The minimum duration in seconds. + */ + minDurationInSec?: number; + /** + * The maximum duration in seconds. + */ + maxDurationInSec?: number; + /** + * The minimum BPM. + */ + minBPM?: number; + /** + * The minimum BPM. + */ + maxBPM?: number; +} +/** + * Shared type for Deezer tracks, playlists and albums + */ +type Deezer = DeezerTrack | DeezerPlaylist | DeezerAlbum; +/** + * Fetches the information for a track, playlist or album on Deezer + * @param url The track, playlist or album URL + * @returns A {@link DeezerTrack}, {@link DeezerPlaylist} or {@link DeezerAlbum} + * object depending on the provided URL. + */ +declare function deezer(url: string): Promise; +/** + * Validates a Deezer URL + * @param url The URL to validate + * @returns The type of the URL either `'track'`, `'playlist'`, `'album'`, `'search'` or `false`. + * `false` means that the provided URL was a wrongly formatted or an unsupported Deezer URL. + */ +declare function dz_validate(url: string): Promise<'track' | 'playlist' | 'album' | 'search' | false>; +/** + * Searches Deezer for tracks using the specified metadata. + * @param options The metadata and limit for the search + * + * * limit?: The maximum number of results to return, maximum `100`, defaults to `10`. + * * artist?: The name of the artist + * * album?: The title of the album + * * title?: The title of the track + * * label?: The label that released the track + * * minDurationInSec?: The minimum duration in seconds + * * maxDurationInSec?: The maximum duration in seconds + * * minBpm?: The minimum BPM + * * maxBpm?: The minimum BPM + * @returns An array of tracks matching the metadata + */ +declare function dz_advanced_track_search(options: DeezerAdvancedSearchOptions): Promise; + +interface tokenOptions { + spotify?: { + client_id: string; + client_secret: string; + refresh_token: string; + market: string; + }; + soundcloud?: { + client_id: string; + }; + youtube?: { + cookie: string; + }; + useragent?: string[]; +} +/** + * Sets + * + * i> YouTube :- cookies. + * + * ii> SoundCloud :- client ID. + * + * iii> Spotify :- client ID, client secret, refresh token, market. + * + * iv> Useragents :- array of string. + * + * locally in memory. + * + * Example : + * ```ts + * play.setToken({ + * youtube : { + * cookie : "Your Cookies" + * } + * }) // YouTube Cookies + * + * await play.setToken({ + * spotify : { + * client_id: 'ID', + client_secret: 'secret', + refresh_token: 'token', + market: 'US' + * } + * }) // Await this only when setting data for spotify + * + * play.setToken({ + * useragent: ['Your User-agent'] + * }) // Use this to avoid 429 errors. + * ``` + * @param options {@link tokenOptions} + */ +declare function setToken(options: tokenOptions): Promise; + +interface SearchOptions { + limit?: number; + source?: { + youtube?: 'video' | 'playlist' | 'channel'; + spotify?: 'album' | 'playlist' | 'track'; + soundcloud?: 'tracks' | 'playlists' | 'albums'; + deezer?: 'track' | 'playlist' | 'album'; + }; + fuzzy?: boolean; + language?: string; + /** + * !!! Before enabling this for public servers, please consider using Discord features like NSFW channels as not everyone in your server wants to see NSFW images. !!! + * Unblurred images will likely have different dimensions than specified in the {@link YouTubeThumbnail} objects. + */ + unblurNSFWThumbnails?: boolean; +} + +declare function stream(url: string, options: { + seek?: number; +} & StreamOptions): Promise; +declare function stream(url: string, options?: StreamOptions): Promise; +declare function search(query: string, options: { + source: { + deezer: 'album'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + deezer: 'playlist'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + deezer: 'track'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + soundcloud: 'albums'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + soundcloud: 'playlists'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + soundcloud: 'tracks'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + spotify: 'album'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + spotify: 'playlist'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + spotify: 'track'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + youtube: 'channel'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + youtube: 'playlist'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + source: { + youtube: 'video'; + }; +} & SearchOptions): Promise; +declare function search(query: string, options: { + limit: number; +} & SearchOptions): Promise; +declare function search(query: string, options?: SearchOptions): Promise; +declare function stream_from_info(info: SoundCloudTrack, options?: StreamOptions): Promise; +declare function stream_from_info(info: InfoData, options?: StreamOptions): Promise; +/** + * Validates url that play-dl supports. + * + * - `so` - SoundCloud + * - `sp` - Spotify + * - `dz` - Deezer + * - `yt` - YouTube + * @param url URL + * @returns + * ```ts + * 'so_playlist' / 'so_track' | 'sp_track' | 'sp_album' | 'sp_playlist' | 'dz_track' | 'dz_playlist' | 'dz_album' | 'yt_video' | 'yt_playlist' | 'search' | false + * ``` + */ +declare function validate(url: string): Promise<'so_playlist' | 'so_track' | 'sp_track' | 'sp_album' | 'sp_playlist' | 'dz_track' | 'dz_playlist' | 'dz_album' | 'yt_video' | 'yt_playlist' | 'search' | false>; +/** + * Authorization interface for Spotify, SoundCloud and YouTube. + * + * Either stores info in `.data` folder or shows relevant data to be used in `setToken` function. + * + * ```ts + * const play = require('play-dl') + * + * play.authorization() + * ``` + * + * Just run the above command and you will get a interface asking some questions. + */ +declare function authorization(): void; +/** + * Attaches paused, playing, autoPaused Listeners to discordjs voice AudioPlayer. + * + * Useful if you don't want extra data to be downloaded by play-dl. + * @param player discordjs voice AudioPlayer + * @param resource A {@link YouTubeStream} or {@link SoundCloudStream} + */ +declare function attachListeners(player: EventEmitter, resource: YouTubeStream | SoundCloudStream): void; + +declare const _default: { + DeezerAlbum: typeof DeezerAlbum; + DeezerPlaylist: typeof DeezerPlaylist; + DeezerTrack: typeof DeezerTrack; + SoundCloudPlaylist: typeof SoundCloudPlaylist; + SoundCloudStream: typeof SoundCloudStream; + SoundCloudTrack: typeof SoundCloudTrack; + SpotifyAlbum: typeof SpotifyAlbum; + SpotifyPlaylist: typeof SpotifyPlaylist; + SpotifyTrack: typeof SpotifyTrack; + YouTubeChannel: typeof YouTubeChannel; + YouTubePlayList: typeof YouTubePlayList; + YouTubeVideo: typeof YouTubeVideo; + attachListeners: typeof attachListeners; + authorization: typeof authorization; + decipher_info: typeof decipher_info; + deezer: typeof deezer; + dz_advanced_track_search: typeof dz_advanced_track_search; + dz_validate: typeof dz_validate; + extractID: typeof extractID; + getFreeClientID: typeof getFreeClientID; + is_expired: typeof is_expired; + playlist_info: typeof playlist_info; + refreshToken: typeof refreshToken; + search: typeof search; + setToken: typeof setToken; + so_validate: typeof so_validate; + soundcloud: typeof soundcloud; + spotify: typeof spotify; + sp_validate: typeof sp_validate; + stream: typeof stream; + stream_from_info: typeof stream_from_info; + validate: typeof validate; + video_basic_info: typeof video_basic_info; + video_info: typeof video_info; + yt_validate: typeof yt_validate; +}; + +export { type Deezer, DeezerAlbum, DeezerPlaylist, DeezerTrack, type InfoData, type SoundCloud, SoundCloudPlaylist, SoundCloudStream, SoundCloudTrack, type Spotify, SpotifyAlbum, SpotifyPlaylist, SpotifyTrack, type YouTube, YouTubeChannel, YouTubePlayList, type YouTubeStream, YouTubeVideo, attachListeners, authorization, decipher_info, deezer, _default as default, dz_advanced_track_search, dz_validate, extractID, getFreeClientID, is_expired, playlist_info, refreshToken, search, setToken, so_validate, soundcloud, sp_validate, spotify, stream, stream_from_info, validate, video_basic_info, video_info, yt_validate }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..90928ce --- /dev/null +++ b/dist/index.js @@ -0,0 +1,22 @@ +"use strict";var X=Object.defineProperty;var ai=Object.getOwnPropertyDescriptor;var li=Object.getOwnPropertyNames;var ui=Object.prototype.hasOwnProperty;var a=(i,e)=>X(i,"name",{value:e,configurable:!0});var ci=(i,e)=>{for(var t in e)X(i,t,{get:e[t],enumerable:!0})},hi=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of li(e))!ui.call(i,s)&&s!==t&&X(i,s,{get:()=>e[s],enumerable:!(r=ai(e,s))||r.enumerable});return i};var di=i=>hi(X({},"__esModule",{value:!0}),i);var Ji={};ci(Ji,{DeezerAlbum:()=>M,DeezerPlaylist:()=>q,DeezerTrack:()=>g,SoundCloudPlaylist:()=>$,SoundCloudStream:()=>z,SoundCloudTrack:()=>T,SpotifyAlbum:()=>A,SpotifyPlaylist:()=>N,SpotifyTrack:()=>E,YouTubeChannel:()=>v,YouTubePlayList:()=>I,YouTubeVideo:()=>S,attachListeners:()=>ii,authorization:()=>ti,decipher_info:()=>j,deezer:()=>et,default:()=>Wi,dz_advanced_track_search:()=>tt,dz_validate:()=>ve,extractID:()=>le,getFreeClientID:()=>Fe,is_expired:()=>Ye,playlist_info:()=>ce,refreshToken:()=>ge,search:()=>Zt,setToken:()=>it,so_validate:()=>ke,soundcloud:()=>_e,sp_validate:()=>be,spotify:()=>Be,stream:()=>Qt,stream_from_info:()=>Xt,validate:()=>ei,video_basic_info:()=>G,video_info:()=>ue,yt_validate:()=>B});module.exports=di(Ji);var yt=require("https"),bt=require("url"),F=require("zlib");var V=require("fs");var k;(0,V.existsSync)(".data/youtube.data")&&(k=JSON.parse((0,V.readFileSync)(".data/youtube.data","utf-8")),k.file=!0);function ht(){let i="";if(k?.cookie){for(let[e,t]of Object.entries(k.cookie))i+=`${e}=${t};`;return i}}a(ht,"getCookies");function pi(i,e){return k?.cookie?(i=i.trim(),e=e.trim(),Object.assign(k.cookie,{[i]:e}),!0):!1}a(pi,"setCookie");function mi(){k.cookie&&k.file&&(0,V.writeFileSync)(".data/youtube.data",JSON.stringify(k,void 0,4))}a(mi,"uploadCookie");function dt(i){let e=i.cookie,t={};e.split(";").forEach(r=>{let s=r.split("=");if(s.length<=1)return;let n=s.shift()?.trim(),o=s.join("=").trim();Object.assign(t,{[n]:o})}),k={cookie:t},k.file=!1}a(dt,"setCookieToken");function pt(i){k?.cookie&&(i.forEach(e=>{e.split(";").forEach(t=>{let r=t.split("=");if(r.length<=1)return;let s=r.shift()?.trim(),n=r.join("=").trim();pi(s,n)})}),mi())}a(pt,"cookieHeaders");var ee=["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.30","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 YaBrowser/19.10.3.281 Yowser/2.5 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"];function mt(i){ee.push(...i)}a(mt,"setUserAgent");function yi(i,e){return i=Math.ceil(i),e=Math.floor(e),Math.floor(Math.random()*(e-i+1))+i}a(yi,"getRandomInt");function ft(){let i=yi(0,ee.length-1);return ee[i]}a(ft,"getRandomUserAgent");function _(i,e={method:"GET"}){return new Promise(async(t,r)=>{let s=await ie(i,e).catch(n=>n);if(s instanceof Error){r(s);return}Number(s.statusCode)>=300&&Number(s.statusCode)<400&&(s=await _(s.headers.location,e)),t(s)})}a(_,"request_stream");function gt(i,e={method:"GET"}){return new Promise(async(t,r)=>{let s=await ie(i,e).catch(n=>n);if(s instanceof Error){r(s);return}if(Number(s.statusCode)>=300&&Number(s.statusCode)<400)s=await gt(s.headers.location,e);else if(Number(s.statusCode)>400){r(new Error(`Got ${s.statusCode} from the request`));return}t(s)})}a(gt,"internalRequest");function h(i,e={method:"GET"}){return new Promise(async(t,r)=>{let s=!1;if(e.cookies){let u=ht();typeof u=="string"&&e.headers&&(Object.assign(e.headers,{cookie:u}),s=!0)}if(e.cookieJar){let u=[];for(let m of Object.entries(e.cookieJar))u.push(m.join("="));if(u.length!==0){e.headers||(e.headers={});let m=s?`; ${e.headers.cookie}`:"";Object.assign(e.headers,{cookie:`${u.join("; ")}${m}`})}}e.headers&&(e.headers={...e.headers,"accept-encoding":"gzip, deflate, br","user-agent":ft()});let n=await gt(i,e).catch(u=>u);if(n instanceof Error){r(n);return}if(n.headers&&n.headers["set-cookie"]){if(e.cookieJar)for(let u of n.headers["set-cookie"]){let m=u.split(";")[0].trim().split("=");e.cookieJar[m.shift()]=m.join("=")}s&&pt(n.headers["set-cookie"])}let o=[],l,c=n.headers["content-encoding"];c==="gzip"?l=(0,F.createGunzip)():c==="br"?l=(0,F.createBrotliDecompress)():c==="deflate"&&(l=(0,F.createDeflate)()),l?(n.pipe(l),l.setEncoding("utf-8"),l.on("data",u=>o.push(u)),l.on("end",()=>t(o.join("")))):(n.setEncoding("utf-8"),n.on("data",u=>o.push(u)),n.on("end",()=>t(o.join(""))))})}a(h,"request");function te(i){return new Promise(async(e,t)=>{let r=await ie(i,{method:"HEAD"}).catch(n=>n);if(r instanceof Error){t(r);return}let s=Number(r.statusCode);if(s<300)e(i);else if(s<400){let n=await te(r.headers.location).catch(o=>o);if(n instanceof Error){t(n);return}e(n)}else t(new Error(`${r.statusCode}: ${r.statusMessage}, ${i}`))})}a(te,"request_resolve_redirect");function Ee(i){return new Promise(async(e,t)=>{let r=await ie(i,{method:"HEAD"}).catch(n=>n);if(r instanceof Error){t(r);return}let s=Number(r.statusCode);if(s<300)e(Number(r.headers["content-length"]));else if(s<400){let n=await te(r.headers.location).catch(l=>l);if(n instanceof Error){t(n);return}let o=await Ee(n).catch(l=>l);if(o instanceof Error){t(o);return}e(o)}else t(new Error(`Failed to get content length with error: ${r.statusCode}, ${r.statusMessage}, ${i}`))})}a(Ee,"request_content_length");function ie(i,e={}){return new Promise((t,r)=>{let s=new bt.URL(i);e.method??="GET";let n={host:s.hostname,path:s.pathname+s.search,headers:e.headers??{},method:e.method},o=(0,yt.request)(n,t);o.on("error",l=>{r(l)}),e.method==="POST"&&o.write(e.body),o.end()})}a(ie,"https_getter");var Oe=require("stream");var re=require("url");var K="[a-zA-Z_\\$]\\w*",bi="'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'",gi='"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"',_t=`(?:${bi}|${gi})`,P=`(?:${K}|${_t})`,wi=`(?:\\.${K}|\\[${_t}\\])`,wt=`(?:''|"")`,kt=":function\\(a\\)\\{(?:return )?a\\.reverse\\(\\)\\}",vt=":function\\(a,b\\)\\{return a\\.slice\\(b\\)\\}",St=":function\\(a,b\\)\\{a\\.splice\\(0,b\\)\\}",Et=":function\\(a,b\\)\\{var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?\\}",_i=new RegExp(`var (${K})=\\{((?:(?:${P}${kt}|${P}${vt}|${P}${St}|${P}${Et}),?\\r?\\n?)+)\\};`),ki=new RegExp(`${`function(?: ${K})?\\(a\\)\\{a=a\\.split\\(${wt}\\);\\s*((?:(?:a=)?${K}`}${wi}\\(a,\\d+\\);)+)return a\\.join\\(${wt}\\)\\}`),vi=new RegExp(`(?:^|,)(${P})${kt}`,"m"),Si=new RegExp(`(?:^|,)(${P})${vt}`,"m"),Ei=new RegExp(`(?:^|,)(${P})${St}`,"m"),Ti=new RegExp(`(?:^|,)(${P})${Et}`,"m");function xi(i){let e=ki.exec(i),t=_i.exec(i);if(!e||!t)return null;let r=t[1].replace(/\$/g,"\\$"),s=t[2].replace(/\$/g,"\\$"),n=e[1].replace(/\$/g,"\\$"),o=vi.exec(s),l=o&&o[1].replace(/\$/g,"\\$").replace(/\$|^'|^"|'$|"$/g,"");o=Si.exec(s);let c=o&&o[1].replace(/\$/g,"\\$").replace(/\$|^'|^"|'$|"$/g,"");o=Ei.exec(s);let u=o&&o[1].replace(/\$/g,"\\$").replace(/\$|^'|^"|'$|"$/g,"");o=Ti.exec(s);let m=o&&o[1].replace(/\$/g,"\\$").replace(/\$|^'|^"|'$|"$/g,""),y=`(${[l,c,u,m].join("|")})`,f=`(?:a=)?${r}(?:\\.${y}|\\['${y}'\\]|\\["${y}"\\])\\(a,(\\d+)\\)`,b=new RegExp(f,"g"),R=[];for(;(o=b.exec(n))!==null;)switch(o[1]||o[2]||o[3]){case m:R.push(`sw${o[4]}`);break;case l:R.push("rv");break;case c:R.push(`sl${o[4]}`);break;case u:R.push(`sp${o[4]}`);break}return R}a(xi,"js_tokens");function Ii(i,e){let t=e.split(""),r=i.length;for(let s=0;s{let n=s.signatureCipher||s.cipher;if(n){let o=Object.fromEntries(new re.URLSearchParams(n));Object.assign(s,o),delete s.signatureCipher,delete s.cipher}if(r&&s.s){let o=Ii(r,s.s);Ri(s,o),delete s.s,delete s.sp}}),i}a(Tt,"format_decipher");var Te=class Te{constructor(e={}){if(!e)throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);this.type="channel",this.name=e.name||null,this.verified=!!e.verified||!1,this.artist=!!e.artist||!1,this.id=e.id||null,this.url=e.url||null,this.icons=e.icons||[{url:null,width:0,height:0}],this.subscribers=e.subscribers||null}iconURL(e={size:0}){if(typeof e.size!="number"||e.size<0)throw new Error("invalid icon size");if(!this.icons?.[0]?.url)return;let t=this.icons?.[0]?.url.split("=s")[1].split("-c")[0];return this.icons?.[0]?.url.replace(`=s${t}-c`,`=s${e.size}-c`)}toString(){return this.name||""}toJSON(){return{name:this.name,verified:this.verified,artist:this.artist,id:this.id,url:this.url,icons:this.icons,type:this.type,subscribers:this.subscribers}}};a(Te,"YouTubeChannel");var v=Te;var xe=class xe{constructor(e){this.url=e.url,this.width=e.width,this.height=e.height}toJSON(){return{url:this.url,width:this.width,height:this.height}}};a(xe,"YouTubeThumbnail");var U=xe;var Ie=class Ie{constructor(e){if(!e)throw new Error(`Can not initiate ${this.constructor.name} without data`);this.id=e.id||void 0,this.url=`https://www.youtube.com/watch?v=${this.id}`,this.type="video",this.title=e.title||void 0,this.description=e.description||void 0,this.durationRaw=e.duration_raw||"0:00",this.durationInSec=(e.duration<0?0:e.duration)||0,this.uploadedAt=e.uploadedAt||void 0,this.liveAt=e.liveAt||void 0,this.upcoming=e.upcoming,this.views=parseInt(e.views)||0;let t=[];for(let r of e.thumbnails)t.push(new U(r));this.thumbnails=t||[],this.channel=new v(e.channel)||{},this.likes=e.likes||0,this.live=!!e.live,this.private=!!e.private,this.tags=e.tags||[],this.discretionAdvised=e.discretionAdvised??void 0,this.music=e.music||[],this.chapters=e.chapters||[]}toString(){return this.url||""}toJSON(){return{id:this.id,url:this.url,title:this.title,description:this.description,durationInSec:this.durationInSec,durationRaw:this.durationRaw,uploadedAt:this.uploadedAt,thumbnail:this.thumbnails[this.thumbnails.length-1].toJSON()||this.thumbnails,channel:this.channel,views:this.views,tags:this.tags,likes:this.likes,live:this.live,private:this.private,discretionAdvised:this.discretionAdvised,music:this.music,chapters:this.chapters}}};a(Ie,"YouTubeVideo");var S=Ie;var Ci="https://www.youtube.com/youtubei/v1/browse?key=",De=class De{constructor(e,t=!1){this._continuation={};if(!e)throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);this.__count=0,this.fetched_videos=new Map,this.type="playlist",t?this.__patchSearch(e):this.__patch(e)}__patch(e){this.id=e.id||void 0,this.url=e.url||void 0,this.title=e.title||void 0,this.videoCount=e.videoCount||0,this.lastUpdate=e.lastUpdate||void 0,this.views=e.views||0,this.link=e.link||void 0,this.channel=new v(e.channel)||void 0,this.thumbnail=e.thumbnail?new U(e.thumbnail):void 0,this.videos=e.videos||[],this.__count++,this.fetched_videos.set(`${this.__count}`,this.videos),this._continuation.api=e.continuation?.api??void 0,this._continuation.token=e.continuation?.token??void 0,this._continuation.clientVersion=e.continuation?.clientVersion??""}__patchSearch(e){this.id=e.id||void 0,this.url=this.id?`https://www.youtube.com/playlist?list=${this.id}`:void 0,this.title=e.title||void 0,this.thumbnail=new U(e.thumbnail)||void 0,this.channel=e.channel||void 0,this.videos=[],this.videoCount=e.videos||0,this.link=void 0,this.lastUpdate=void 0,this.views=0}async next(e=1/0){if(!this._continuation||!this._continuation.token)return[];let t=await h(`${Ci}${this._continuation.api}&prettyPrint=false`,{method:"POST",body:JSON.stringify({continuation:this._continuation.token,context:{client:{utcOffsetMinutes:0,gl:"US",hl:"en",clientName:"WEB",clientVersion:this._continuation.clientVersion},user:{},request:{}}})}),r=JSON.parse(t)?.onResponseReceivedActions[0]?.appendContinuationItemsAction?.continuationItems;if(!r)return[];let s=Re(r,e);return this.fetched_videos.set(`${this.__count}`,s),this._continuation.token=se(r),s}async fetch(e=1/0){if(!this._continuation.token)return this;for(e<1&&(e=1/0);typeof this._continuation.token=="string"&&this._continuation.token.length;){this.__count++;let r=await this.next();if(e-=r.length,e<=0||!r.length)break}return this}page(e){if(!e)throw new Error("Page number is not provided");if(!this.fetched_videos.has(`${e}`))throw new Error("Given Page number is invalid");return this.fetched_videos.get(`${e}`)}get total_pages(){return this.fetched_videos.size}get total_videos(){let e=this.total_pages;return(e-1)*100+this.fetched_videos.get(`${e}`).length}async all_videos(){await this.fetch();let e=[];for(let t of this.fetched_videos.values())e.push(...t);return e}toJSON(){return{id:this.id,title:this.title,thumbnail:this.thumbnail?.toJSON()||this.thumbnail,channel:this.channel,url:this.url,videos:this.videos}}};a(De,"YouTubePlayList");var I=De;var oe=require("url");var ne=/^[a-zA-Z\d_-]{11,12}$/,Oi=/^(PL|UU|LL|RD|OL)[a-zA-Z\d_-]{10,}$/,ae="AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",It=/^((?:https?:)?\/\/)?(?:(?:www|m|music)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|shorts\/|embed\/|live\/|v\/)?)([\w\-]+)(\S+)?$/,$i=/^((?:https?:)?\/\/)?(?:(?:www|m|music)\.)?((?:youtube\.com|youtu.be))\/(?:(playlist|watch))?(.*)?((\?|\&)list=)(PL|UU|LL|RD|OL)[a-zA-Z\d_-]{10,}(&.*)?$/;function B(i){let e=i.trim();if(e.indexOf("list=")===-1)if(e.startsWith("https"))if(e.match(It)){let t;return e.includes("youtu.be/")?t=e.split("youtu.be/")[1].split(/(\?|\/|&)/)[0]:e.includes("youtube.com/embed/")?t=e.split("youtube.com/embed/")[1].split(/(\?|\/|&)/)[0]:e.includes("youtube.com/shorts/")?t=e.split("youtube.com/shorts/")[1].split(/(\?|\/|&)/)[0]:t=e.split("watch?v=")[1]?.split(/(\?|\/|&)/)[0],t?.match(ne)?"video":!1}else return!1;else return e.match(ne)?"video":e.match(Oi)?"playlist":"search";else return e.match($i)?"playlist":B(e.replace(/(\?|\&)list=[^&]*/,""))}a(B,"yt_validate");function Ce(i){if(i.startsWith("https://")&&i.match(It)){let e;if(i.includes("youtu.be/")?e=i.split("youtu.be/")[1].split(/(\?|\/|&)/)[0]:i.includes("youtube.com/embed/")?e=i.split("youtube.com/embed/")[1].split(/(\?|\/|&)/)[0]:i.includes("youtube.com/shorts/")?e=i.split("youtube.com/shorts/")[1].split(/(\?|\/|&)/)[0]:i.includes("youtube.com/live/")?e=i.split("youtube.com/live/")[1].split(/(\?|\/|&)/)[0]:e=(i.split("watch?v=")[1]??i.split("&v=")[1]).split(/(\?|\/|&)/)[0],e.match(ne))return e}else if(i.match(ne))return i;return!1}a(Ce,"extractVideoId");function le(i){let e=B(i);if(!e||e==="search")throw new Error("This is not a YouTube url or videoId or PlaylistID");let t=i.trim();if(t.startsWith("https"))if(t.indexOf("list=")===-1){let r=Ce(t);if(!r)throw new Error("This is not a YouTube url or videoId or PlaylistID");return r}else return t.split("list=")[1].split("&")[0];else return t}a(le,"extractID");async function G(i,e={}){if(typeof i!="string")throw new Error("url parameter is not a URL string or a string of HTML");let t=i.trim(),r,s={};if(e.htmldata)r=t;else{let p=Ce(t);if(!p)throw new Error("This is not a YouTube Watch URL");let w=`https://www.youtube.com/watch?v=${p}&has_verified=1`;r=await h(w,{headers:{"accept-language":e.language||"en-US;q=0.9"},cookies:!0,cookieJar:s})}if(r.indexOf("Our systems have detected unusual traffic from your computer network.")!==-1)throw new Error("Captcha page: YouTube has detected that you are a bot!");let n=r.split("var ytInitialPlayerResponse = ")?.[1]?.split(";")[0].split(/(?<=}}});\s*(var|const|let)\s/)[0];if(!n)throw new Error("Initial Player Response Data is undefined.");let o=r.split("var ytInitialData = ")?.[1]?.split(";")[0].split(/;\s*(var|const|let)\s/)[0];if(!o)throw new Error("Initial Response Data is undefined.");let l=JSON.parse(n),c=JSON.parse(o),u=l.videoDetails,m=!1,y=!1;if(l.playabilityStatus.status!=="OK")if(l.playabilityStatus.status==="CONTENT_CHECK_REQUIRED"){if(e.htmldata)throw new Error(`Accepting the viewer discretion is not supported when using htmldata, video: ${u.videoId}`);m=!0;let p=c.topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton.buttonRenderer.command.saveConsentAction;p&&Object.assign(s,{VISITOR_INFO1_LIVE:p.visitorCookie,CONSENT:p.consentCookie});let w=await Dt(u.videoId,s,r,!0);l.streamingData=w.streamingData,c.contents.twoColumnWatchNextResults.secondaryResults=w.relatedVideos}else if(l.playabilityStatus.status==="LIVE_STREAM_OFFLINE")y=!0;else throw new Error(`While getting info from url +${l.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText??l.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText??l.playabilityStatus.reason}`);let f=c.contents.twoColumnWatchNextResults.results?.results?.contents[1]?.videoSecondaryInfoRenderer?.owner?.videoOwnerRenderer,b=f?.badges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase(),R=`https://www.youtube.com${r.split('"jsUrl":"')[1].split('"')[0]}`,Q=[];c.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach(p=>{p.compactVideoRenderer&&Q.push(`https://www.youtube.com/watch?v=${p.compactVideoRenderer.videoId}`),p.itemSectionRenderer?.contents&&p.itemSectionRenderer.contents.forEach(w=>{w.compactVideoRenderer&&Q.push(`https://www.youtube.com/watch?v=${w.compactVideoRenderer.videoId}`)})});let Z=l.microformat.playerMicroformatRenderer,rt=c.engagementPanels.find(p=>p?.engagementPanelSectionListRenderer?.panelIdentifier=="engagement-panel-structured-description")?.engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items.find(p=>p.videoDescriptionMusicSectionRenderer)?.videoDescriptionMusicSectionRenderer.carouselLockups,st=[];rt&&rt.forEach(p=>{if(!p.carouselLockupRenderer)return;let w=p.carouselLockupRenderer,ri=w.videoLockup?.compactVideoRenderer.title.simpleText??w.videoLockup?.compactVideoRenderer.title.runs?.find(x=>x.text)?.text,si=w.infoRows?.map(x=>[x.infoRowRenderer.title.simpleText.toLowerCase(),(x.infoRowRenderer.expandedMetadata??x.infoRowRenderer.defaultMetadata)?.runs?.map(oi=>oi.text).join("")??x.infoRowRenderer.defaultMetadata?.simpleText??x.infoRowRenderer.expandedMetadata?.simpleText??""]),ni=Object.fromEntries(si??{}),ct=w.videoLockup?.compactVideoRenderer.navigationEndpoint?.watchEndpoint.videoId??w.infoRows?.find(x=>x.infoRowRenderer.title.simpleText.toLowerCase()=="song")?.infoRowRenderer.defaultMetadata.runs?.find(x=>x.navigationEndpoint)?.navigationEndpoint.watchEndpoint?.videoId;st.push({song:ri,url:ct?`https://www.youtube.com/watch?v=${ct}`:null,...ni})});let nt=c.playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer.playerBar?.multiMarkersPlayerBarRenderer.markersMap?.find(p=>p.key==="DESCRIPTION_CHAPTERS")?.value?.chapters,ot=[];if(nt)for(let{chapterRenderer:p}of nt)ot.push({title:p.title.simpleText,timestamp:xt(p.timeRangeStartMillis/1e3),seconds:p.timeRangeStartMillis/1e3,thumbnails:p.thumbnail.thumbnails});let Se;if(y)if(Z.liveBroadcastDetails.startTimestamp)Se=new Date(Z.liveBroadcastDetails.startTimestamp);else{let p=l.playabilityStatus.liveStreamability.liveStreamabilityRenderer.offlineSlate.liveStreamOfflineSlateRenderer.scheduledStartTime;Se=new Date(parseInt(p)*1e3)}let at=c.contents.twoColumnWatchNextResults.results.results.contents.find(p=>p.videoPrimaryInfoRenderer)?.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons?.find(p=>p.toggleButtonRenderer?.defaultIcon.iconType==="LIKE"||p.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultIcon.iconType==="LIKE"),lt=new S({id:u.videoId,title:u.title,description:u.shortDescription,duration:Number(u.lengthSeconds),duration_raw:xt(u.lengthSeconds),uploadedAt:Z.publishDate,liveAt:Z.liveBroadcastDetails?.startTimestamp,upcoming:Se,thumbnails:u.thumbnail.thumbnails,channel:{name:u.author,id:u.channelId,url:`https://www.youtube.com/channel/${u.channelId}`,verified:!!b?.includes("verified"),artist:!!b?.includes("artist"),icons:f?.thumbnail?.thumbnails||void 0},views:u.viewCount,tags:u.keywords,likes:parseInt(at?.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\D+/g,"")??at?.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\D+/g,"")??0),live:u.isLiveContent,private:u.isPrivate,discretionAdvised:m,music:st,chapters:ot}),ut=[];return y||(ut=await Rt(u.videoId,s,r)),{LiveStreamData:{isLive:lt.live,dashManifestUrl:l.streamingData?.dashManifestUrl??null,hlsManifestUrl:l.streamingData?.hlsManifestUrl??null},html5player:R,format:ut,video_details:lt,related_videos:Q}}a(G,"video_basic_info");async function Y(i,e={}){if(typeof i!="string")throw new Error("url parameter is not a URL string or a string of HTML");let t,r={};if(e.htmldata)t=i;else{let f=Ce(i);if(!f)throw new Error("This is not a YouTube Watch URL");let b=`https://www.youtube.com/watch?v=${f}&has_verified=1`;t=await h(b,{headers:{"accept-language":"en-US,en;q=0.9"},cookies:!0,cookieJar:r})}if(t.indexOf("Our systems have detected unusual traffic from your computer network.")!==-1)throw new Error("Captcha page: YouTube has detected that you are a bot!");let s=t.split("var ytInitialPlayerResponse = ")?.[1]?.split(";")[0].split(/(?<=}}});\s*(var|const|let)\s/)[0];if(!s)throw new Error("Initial Player Response Data is undefined.");let n=JSON.parse(s),o=!1;if(n.playabilityStatus.status!=="OK")if(n.playabilityStatus.status==="CONTENT_CHECK_REQUIRED"){if(e.htmldata)throw new Error(`Accepting the viewer discretion is not supported when using htmldata, video: ${n.videoDetails.videoId}`);let f=t.split("var ytInitialData = ")?.[1]?.split(";")[0].split(/;\s*(var|const|let)\s/)[0];if(!f)throw new Error("Initial Response Data is undefined.");let b=JSON.parse(f).topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton.buttonRenderer.command.saveConsentAction;b&&Object.assign(r,{VISITOR_INFO1_LIVE:b.visitorCookie,CONSENT:b.consentCookie});let R=await Dt(n.videoDetails.videoId,r,t,!1);n.streamingData=R.streamingData}else if(n.playabilityStatus.status==="LIVE_STREAM_OFFLINE")o=!0;else throw new Error(`While getting info from url +${n.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText??n.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText??n.playabilityStatus.reason}`);let l=`https://www.youtube.com${t.split('"jsUrl":"')[1].split('"')[0]}`,c=Number(n.videoDetails.lengthSeconds),u={url:`https://www.youtube.com/watch?v=${n.videoDetails.videoId}`,durationInSec:(c<0?0:c)||0},m=[];o||(m=await Rt(n.videoDetails.videoId,r,t));let y={isLive:n.videoDetails.isLiveContent,dashManifestUrl:n.streamingData?.dashManifestUrl??null,hlsManifestUrl:n.streamingData?.hlsManifestUrl??null};return await j({LiveStreamData:y,html5player:l,format:m,video_details:u},!0)}a(Y,"video_stream_info");function xt(i){let e=Number(i),t=Math.floor(e/3600),r=Math.floor(e%3600/60),s=Math.floor(e%3600%60),n=t>0?(t<10?`0${t}`:t)+":":"",o=r>0?(r<10?`0${r}`:r)+":":"00:",l=s>0?s<10?`0${s}`:s:"00";return n+o+l}a(xt,"parseSeconds");async function ue(i,e={}){let t=await G(i.trim(),e);return await j(t)}a(ue,"video_info");async function j(i,e=!1){return i.LiveStreamData.isLive===!0&&i.LiveStreamData.dashManifestUrl!==null&&i.video_details.durationInSec===0||i.format.length>0&&(i.format[0].signatureCipher||i.format[0].cipher)&&(e&&(i.format=W(i.format)),i.format=await Tt(i.format,i.html5player)),i}a(j,"decipher_info");async function ce(i,e={}){if(!i||typeof i!="string")throw new Error(`Expected playlist url, received ${typeof i}!`);let t=i.trim();if(t.startsWith("https")||(t=`https://www.youtube.com/playlist?list=${t}`),t.indexOf("list=")===-1)throw new Error("This is not a Playlist URL");if(t.includes("music.youtube.com")){let n=new oe.URL(t);n.hostname="www.youtube.com",t=n.toString()}let r=await h(t,{headers:{"accept-language":e.language||"en-US;q=0.9"}});if(r.indexOf("Our systems have detected unusual traffic from your computer network.")!==-1)throw new Error("Captcha page: YouTube has detected that you are a bot!");let s=JSON.parse(r.split("var ytInitialData = ")[1].split(";")[0].split(/;\s*(var|const|let)\s/)[0]);if(s.alerts)if(s.alerts[0].alertWithButtonRenderer?.type==="INFO"){if(!e.incomplete)throw new Error(`While parsing playlist url +${s.alerts[0].alertWithButtonRenderer.text.simpleText}`)}else throw s.alerts[0].alertRenderer?.type==="ERROR"?new Error(`While parsing playlist url +${s.alerts[0].alertRenderer.text.runs[0].text}`):new Error(`While parsing playlist url +Unknown Playlist Error`);return s.currentVideoEndpoint?Pi(s,r,t):Ni(s,r)}a(ce,"playlist_info");function Re(i,e=1/0){let t=[];for(let r=0;rObject.keys(e)[0]==="continuationItemRenderer")?.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token}a(se,"getContinuationToken");async function Dt(i,e,t,r){let s=t.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0]??t.split('innertubeApiKey":"')[1]?.split('"')[0]??ae,n=t.split('"XSRF_TOKEN":"')[1]?.split('"')[0].replaceAll("\\u003d","=")??t.split('"xsrf_token":"')[1]?.split('"')[0].replaceAll("\\u003d","=");if(!n)throw new Error(`Unable to extract XSRF_TOKEN to accept the viewer discretion popup for video: ${i}.`);let o=await h(`https://www.youtube.com/youtubei/v1/verify_age?key=${s}&prettyPrint=false`,{method:"POST",body:JSON.stringify({context:{client:{utcOffsetMinutes:0,gl:"US",hl:"en",clientName:"WEB",clientVersion:t.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0]??t.split('"innertube_context_client_version":"')[1]?.split('"')[0]??""},user:{},request:{}},nextEndpoint:{urlEndpoint:{url:`/watch?v=${i}&has_verified=1`}},setControvercy:!0}),cookies:!0,cookieJar:e}),l=JSON.parse(o).actions[0].navigateAction.endpoint,c=await h(`https://www.youtube.com/${l.urlEndpoint.url}&pbj=1`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new oe.URLSearchParams([["command",JSON.stringify(l)],["session_token",n]]).toString(),cookies:!0,cookieJar:e});if(c.includes("

Something went wrong

"))throw new Error(`Unable to accept the viewer discretion popup for video: ${i}`);let u=JSON.parse(c);if(u[2].playerResponse.playabilityStatus.status!=="OK")throw new Error(`While getting info from url after trying to accept the discretion popup for video ${i} +${u[2].playerResponse.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText??u[2].playerResponse.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText}`);let m=u[2].playerResponse.streamingData;return r?{streamingData:m,relatedVideos:u[3].response.contents.twoColumnWatchNextResults.secondaryResults}:{streamingData:m}}a(Dt,"acceptViewerDiscretion");async function Rt(i,e,t){let r=t.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0]??t.split('innertubeApiKey":"')[1]?.split('"')[0]??ae,s=await h(`https://www.youtube.com/youtubei/v1/player?key=${r}&prettyPrint=false`,{method:"POST",body:JSON.stringify({context:{client:{clientName:"IOS",clientVersion:"19.09.3",deviceModel:"iPhone16,1",userAgent:"com.google.ios.youtube/19.09.3 (iPhone; CPU iPhone OS 17_5 like Mac OS X)",hl:"en",timeZone:"UTC",utcOffsetMinutes:0}},videoId:i,playbackContext:{contentPlaybackContext:{html5Preference:"HTML5_PREF_WANTS"}},contentCheckOk:!0,racyCheckOk:!0}),cookies:!0,cookieJar:e});return JSON.parse(s).streamingData.adaptiveFormats}a(Rt,"getIosFormats");function Pi(i,e,t){let r=i.contents.twoColumnWatchNextResults.playlist?.playlist;if(!r)throw new Error("Watch playlist unavailable due to YouTube layout changes.");let s=Ai(r.contents),n=e.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0]??e.split('innertubeApiKey":"')[1]?.split('"')[0]??ae,o=r.totalVideos,l=r.shortBylineText?.runs?.[0],c=r.badges?.[0]?.metadataBadgeRenderer?.style.toLowerCase();return new I({continuation:{api:n,token:se(r.contents),clientVersion:e.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0]??e.split('"innertube_context_client_version":"')[1]?.split('"')[0]??""},id:r.playlistId||"",title:r.title||"",videoCount:parseInt(o)||0,videos:s,url:t,channel:{id:l?.navigationEndpoint?.browseEndpoint?.browseId||null,name:l?.text||null,url:`https://www.youtube.com${l?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl||l?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url}`,verified:!!c?.includes("verified"),artist:!!c?.includes("artist")}})}a(Pi,"getWatchPlaylist");function Ni(i,e){let t=i.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].playlistVideoListRenderer.contents,r=i.sidebar.playlistSidebarRenderer.items,s=e.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0]??e.split('innertubeApiKey":"')[1]?.split('"')[0]??ae,n=Re(t,100),o=r[0].playlistSidebarPrimaryInfoRenderer;if(!o.title.runs||!o.title.runs.length)throw new Error("Failed to Parse Playlist info.");let l=r[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner,c=o.stats.length===3?o.stats[1].simpleText.replace(/\D/g,""):0,u=o.stats.find(f=>"runs"in f&&f.runs.find(b=>b.text.toLowerCase().includes("last update")))?.runs.pop()?.text??null,m=o.stats[0].runs[0].text.replace(/\D/g,"")||0;return new I({continuation:{api:s,token:se(t),clientVersion:e.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0]??e.split('"innertube_context_client_version":"')[1]?.split('"')[0]??""},id:o.title.runs[0].navigationEndpoint.watchEndpoint.playlistId,title:o.title.runs[0].text,videoCount:parseInt(m)||0,lastUpdate:u,views:parseInt(c)||0,videos:n,url:`https://www.youtube.com/playlist?list=${o.title.runs[0].navigationEndpoint.watchEndpoint.playlistId}`,link:`https://www.youtube.com${o.title.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,channel:l?{name:l.videoOwnerRenderer.title.runs[0].text,id:l.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId,url:`https://www.youtube.com${l.videoOwnerRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url||l.videoOwnerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl}`,icons:l.videoOwnerRenderer.thumbnail.thumbnails??[]}:{},thumbnail:o.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length?o.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[o.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length-1]:null})}a(Ni,"getNormalPlaylist");function Ai(i,e=1/0){let t=[];for(let r=0;r{this.dash_updater(),this.dash_timer.reuse()},1800),this.stream.on("close",()=>{this.cleanup()}),this.initialize_dash()}cleanup(){this.normal_timer?.destroy(),this.dash_timer.destroy(),this.request?.destroy(),this.video_url="",this.request=void 0,this.dash_url="",this.base_url="",this.interval=0}async dash_updater(){let e=await Y(this.video_url);return e.LiveStreamData.dashManifestUrl&&(this.dash_url=e.LiveStreamData.dashManifestUrl),this.initialize_dash()}async initialize_dash(){let t=(await h(this.dash_url)).split('")[0].split("");if(t[t.length-1]===""&&t.pop(),this.base_url=t[t.length-1].split("")[1].split("")[0],await _(`https://${new Ct.URL(this.base_url).host}/generate_204`),this.sequence===0){let r=t[t.length-1].split("")[1].split("")[0].replaceAll('');r[r.length-1]===""&&r.pop(),r.length>this.precache&&r.splice(0,r.length-this.precache),this.sequence=Number(r[0].split("sq/")[1].split("/")[0]),this.first_data(r.length)}}async first_data(e){for(let t=1;t<=e;t++)await new Promise(async r=>{let s=await _(this.base_url+"sq/"+this.sequence).catch(n=>n);if(s instanceof Error){this.stream.emit("error",s);return}this.request=s,s.on("data",n=>{this.stream.push(n)}),s.on("end",()=>{this.sequence++,r("")}),s.once("error",n=>{this.stream.emit("error",n)})});this.normal_timer=new O(()=>{this.loop(),this.normal_timer?.reuse()},this.interval)}loop(){return new Promise(async e=>{let t=await _(this.base_url+"sq/"+this.sequence).catch(r=>r);if(t instanceof Error){this.stream.emit("error",t);return}this.request=t,t.on("data",r=>{this.stream.push(r)}),t.on("end",()=>{this.sequence++,e("")}),t.once("error",r=>{this.stream.emit("error",r)})})}pause(){}resume(){}};a($e,"LiveStream");var he=$e,Pe=class Pe{constructor(e,t,r,s,n,o){this.stream=new Oe.Readable({highWaterMark:5*1e3*1e3,read(){}}),this.url=e,this.quality=o.quality,this.type=t,this.bytes_count=0,this.video_url=n,this.per_sec_bytes=Math.ceil(s/r),this.content_length=s,this.request=null,this.timer=new O(()=>{this.timer.reuse(),this.loop()},265),this.stream.on("close",()=>{this.timer.destroy(),this.cleanup()}),this.loop()}async retry(){let e=await Y(this.video_url),t=W(e.format);this.url=t[this.quality].url}cleanup(){this.request?.destroy(),this.request=null,this.url=""}async loop(){if(this.stream.destroyed){this.timer.destroy(),this.cleanup();return}let e=this.bytes_count+this.per_sec_bytes*300,t=await _(this.url,{headers:{range:`bytes=${this.bytes_count}-${e>=this.content_length?"":e}`}}).catch(r=>r);if(t instanceof Error){this.stream.emit("error",t),this.bytes_count=0,this.per_sec_bytes=0,this.cleanup();return}if(Number(t.statusCode)>=400){this.cleanup(),await this.retry(),this.timer.reuse(),this.loop();return}this.request=t,t.on("data",r=>{this.stream.push(r)}),t.once("error",async()=>{this.cleanup(),await this.retry(),this.timer.reuse(),this.loop()}),t.on("data",r=>{this.bytes_count+=r.length}),t.on("end",()=>{e>=this.content_length&&(this.timer.destroy(),this.stream.push(null),this.cleanup())})}pause(){this.timer.pause()}resume(){this.timer.resume()}};a(Pe,"Stream");var de=Pe,Ne=class Ne{constructor(e,t){this.callback=e,this.time_total=t,this.time_left=t,this.paused=!1,this.destroyed=!1,this.time_start=process.hrtime()[0],this.timer=setTimeout(this.callback,this.time_total*1e3)}pause(){return!this.paused&&!this.destroyed?(this.paused=!0,clearTimeout(this.timer),this.time_left=this.time_left-(process.hrtime()[0]-this.time_start),!0):!1}resume(){return this.paused&&!this.destroyed?(this.paused=!1,this.time_start=process.hrtime()[0],this.timer=setTimeout(this.callback,this.time_left*1e3),!0):!1}reuse(){return this.destroyed?!1:(clearTimeout(this.timer),this.time_left=this.time_total,this.paused=!1,this.time_start=process.hrtime()[0],this.timer=setTimeout(this.callback,this.time_total*1e3),!0)}destroy(){clearTimeout(this.timer),this.destroyed=!0,this.callback=()=>{},this.time_total=0,this.time_left=0,this.paused=!1,this.time_start=0}};a(Ne,"Timer");var O=Ne;var H=require("play-audio"),Ot=require("stream");var Li=Object.keys(H.WebmElements),ze=class ze extends Ot.Duplex{constructor(e,t){super(t),this.state="READING_HEAD",this.cursor=0,this.header=new H.WebmHeader,this.headfound=!1,this.headerparsed=!1,this.seekfound=!1,this.data_length=0,this.data_size=0,this.offset=0,this.sec=e,this.time=Math.floor(e/10)*10}get vint_length(){let e=0;for(;e<8&&!(1<<7-e&this.chunk[this.cursor]);e++);return++e}vint_value(){if(!this.chunk)return!1;let e=this.vint_length;if(this.chunk.lengththis.cursor;){let e=this.cursor,t=this.vint_length;if(this.chunk.length2&&this.time===this.header.segment.cues.at(-2).time/1e3&&this.emit("headComplete"),r.type===0){this.cursor+=this.data_size;continue}if(this.chunk.lengththis.cursor;){let e=this.cursor,t=this.vint_length;if(this.chunk.lengththis.chunk.length)continue;let t=this.chunk.slice(this.cursor+this.data_size,this.cursor+this.data_size+this.data_length),r=this.header.segment.tracks[this.header.audioTrack];if(!r||r.trackType!==2)return new Error("No audio Track in this webm file.");if((t[0]&15)===r.trackNumber)this.cursor+=this.data_size+this.data_length,this.push(t.slice(4)),e=!0;else continue}return e?(this.seekfound=!0,this.readTag()):new Error("Failed to find nearest correct simple Block.")}parseEbmlID(e){return Li.includes(e)?H.WebmElements[e]:!1}_destroy(e,t){this.cleanup(),t(e)}_final(e){this.cleanup(),e()}};a(ze,"WebmSeeker");var pe=ze;var Le=class Le{constructor(e,t,r,s,n,o,l){this.stream=new pe(l.seek,{highWaterMark:5*1e3*1e3,readableObjectMode:!0}),this.url=e,this.quality=l.quality,this.type="opus",this.bytes_count=0,this.video_url=o,this.per_sec_bytes=Math.ceil(n?n/8:s/t),this.header_length=r,this.content_length=s,this.request=null,this.timer=new O(()=>{this.timer.reuse(),this.loop()},265),this.stream.on("close",()=>{this.timer.destroy(),this.cleanup()}),this.seek()}async seek(){let e=await new Promise(async(r,s)=>{if(this.stream.headerparsed)r("");else{let n=await _(this.url,{headers:{range:`bytes=0-${this.header_length}`}}).catch(o=>o);if(n instanceof Error){s(n);return}if(Number(n.statusCode)>=400){s(400);return}this.request=n,n.pipe(this.stream,{end:!1}),n.once("end",()=>{this.stream.state="READING_DATA",r("")}),this.stream.once("headComplete",()=>{n.unpipe(this.stream),n.destroy(),this.stream.state="READING_DATA",r("")})}}).catch(r=>r);if(e instanceof Error){this.stream.emit("error",e),this.bytes_count=0,this.per_sec_bytes=0,this.cleanup();return}else if(e===400)return await this.retry(),this.timer.reuse(),this.seek();let t=this.stream.seek(this.content_length);if(t instanceof Error){this.stream.emit("error",t),this.bytes_count=0,this.per_sec_bytes=0,this.cleanup();return}this.stream.seekfound=!1,this.bytes_count=t,this.timer.reuse(),this.loop()}async retry(){let e=await Y(this.video_url),t=W(e.format);this.url=t[this.quality].url}cleanup(){this.request?.destroy(),this.request=null,this.url=""}async loop(){if(this.stream.destroyed){this.timer.destroy(),this.cleanup();return}let e=this.bytes_count+this.per_sec_bytes*300,t=await _(this.url,{headers:{range:`bytes=${this.bytes_count}-${e>=this.content_length?"":e}`}}).catch(r=>r);if(t instanceof Error){this.stream.emit("error",t),this.bytes_count=0,this.per_sec_bytes=0,this.cleanup();return}if(Number(t.statusCode)>=400){this.cleanup(),await this.retry(),this.timer.reuse(),this.loop();return}this.request=t,t.pipe(this.stream,{end:!1}),t.once("error",async()=>{this.cleanup(),await this.retry(),this.timer.reuse(),this.loop()}),t.on("data",r=>{this.bytes_count+=r.length}),t.on("end",()=>{e>=this.content_length&&(this.timer.destroy(),this.stream.end(),this.cleanup())})}pause(){this.timer.pause()}resume(){this.timer.resume()}};a(Le,"SeekStream");var me=Le;var $t=require("url");function W(i){let e=[];return i.forEach(t=>{let r=t.mimeType;r.startsWith("audio")&&(t.codec=r.split('codecs="')[1].split('"')[0],t.container=r.split("audio/")[1].split(";")[0],e.push(t))}),e}a(W,"parseAudioFormats");async function fe(i,e={}){let t=await Y(i,{htmldata:e.htmldata,language:e.language});return await ye(t,e)}a(fe,"stream");async function ye(i,e={}){if(i.format.length===0)throw new Error("Upcoming and premiere videos that are not currently live cannot be streamed.");if(e.quality&&!Number.isInteger(e.quality))throw new Error("Quality must be set to an integer.");let t=[];if(i.LiveStreamData.isLive===!0&&i.LiveStreamData.dashManifestUrl!==null&&i.video_details.durationInSec===0)return new he(i.LiveStreamData.dashManifestUrl,i.format[i.format.length-1].targetDurationSec,i.video_details.url,e.precache);let r=W(i.format);typeof e.quality!="number"?e.quality=r.length-1:e.quality<=0?e.quality=0:e.quality>=r.length&&(e.quality=r.length-1),r.length!==0?t.push(r[e.quality]):t.push(i.format[i.format.length-1]);let s=t[0].codec==="opus"&&t[0].container==="webm"?"webm/opus":"arbitrary";if(await _(`https://${new $t.URL(t[0].url).host}/generate_204`),s==="webm/opus")if(e.discordPlayerCompatibility){if(e.seek)throw new Error("Can not seek with discordPlayerCompatibility set to true.")}else{if(e.seek??=0,e.seek>=i.video_details.durationInSec||e.seek<0)throw new Error(`Seeking beyond limit. [ 0 - ${i.video_details.durationInSec-1}]`);return new me(t[0].url,i.video_details.durationInSec,t[0].indexRange.end,Number(t[0].contentLength),Number(t[0].bitrate),i.video_details.url,e)}let n;return t[0].contentLength?n=Number(t[0].contentLength):n=await Ee(t[0].url),new de(t[0].url,s,i.video_details.durationInSec,n,i.video_details.url,e)}a(ye,"stream_from_info");var Mi=["-oaymwEpCOADEI4CSFryq4qpAxsIARUAAAAAGAElAADIQj0AgKJDeAHtAZmZGUI=","-oaymwEiCOADEI4CSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BmZkZQg==","-oaymwEiCOgCEMoBSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmbmQQ==","-oaymwEiCNAFEJQDSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmZmQg==","-oaymwEdCNAFEJQDSFryq4qpAw8IARUAAIhCGAHtAWZmZkI=","-oaymwEdCNACELwBSFryq4qpAw8IARUAAIhCGAHtAT0K10E="];function Nt(i,e){if(!i)throw new Error("Can't parse Search result without data");e?e.type||(e.type="video"):e={type:"video",limit:0};let t=typeof e.limit=="number"&&e.limit>0;e.unblurNSFWThumbnails??=!1;let r=i.split("var ytInitialData = ")?.[1]?.split(";")[0].split(/;\s*(var|const|let)\s/)[0],s=JSON.parse(r),n=[],o=s.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.flatMap(l=>l.itemSectionRenderer?.contents);for(let l of o){if(t&&n.length===e.limit)break;if(!(!l||!l.videoRenderer&&!l.channelRenderer&&!l.playlistRenderer))switch(e.type){case"video":{let c=Bi(l);c&&(e.unblurNSFWThumbnails&&c.thumbnails.forEach(Pt),n.push(c));break}case"channel":{let c=Ui(l);c&&n.push(c);break}case"playlist":{let c=Yi(l);c&&(e.unblurNSFWThumbnails&&c.thumbnail&&Pt(c.thumbnail),n.push(c));break}default:throw new Error(`Unknown search type: ${e.type}`)}}return n}a(Nt,"ParseSearchResult");function qi(i){if(!i)return 0;let e=i.split(":"),t=0;switch(e.length){case 3:t=parseInt(e[0])*60*60+parseInt(e[1])*60+parseInt(e[2]);break;case 2:t=parseInt(e[0])*60+parseInt(e[1]);break;default:t=parseInt(e[0])}return t}a(qi,"parseDuration");function Ui(i){if(!i||!i.channelRenderer)throw new Error("Failed to Parse YouTube Channel");let e=i.channelRenderer.ownerBadges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase(),t=`https://www.youtube.com${i.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl||i.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url}`,r=i.channelRenderer.thumbnail.thumbnails[i.channelRenderer.thumbnail.thumbnails.length-1];return new v({id:i.channelRenderer.channelId,name:i.channelRenderer.title.simpleText,icon:{url:r.url.replace("//","https://"),width:r.width,height:r.height},url:t,verified:!!e?.includes("verified"),artist:!!e?.includes("artist"),subscribers:i.channelRenderer.subscriberCountText?.simpleText??"0 subscribers"})}a(Ui,"parseChannel");function Bi(i){if(!i||!i.videoRenderer)throw new Error("Failed to Parse YouTube Video");let e=i.videoRenderer.ownerText.runs[0],t=i.videoRenderer.ownerBadges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase(),r=i.videoRenderer.lengthText;return new S({id:i.videoRenderer.videoId,url:`https://www.youtube.com/watch?v=${i.videoRenderer.videoId}`,title:i.videoRenderer.title.runs[0].text,description:i.videoRenderer.detailedMetadataSnippets?.[0].snippetText.runs?.length?i.videoRenderer.detailedMetadataSnippets[0].snippetText.runs.map(n=>n.text).join(""):"",duration:r?qi(r.simpleText):0,duration_raw:r?r.simpleText:null,thumbnails:i.videoRenderer.thumbnail.thumbnails,channel:{id:e.navigationEndpoint.browseEndpoint.browseId||null,name:e.text||null,url:`https://www.youtube.com${e.navigationEndpoint.browseEndpoint.canonicalBaseUrl||e.navigationEndpoint.commandMetadata.webCommandMetadata.url}`,icons:i.videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails,verified:!!t?.includes("verified"),artist:!!t?.includes("artist")},uploadedAt:i.videoRenderer.publishedTimeText?.simpleText??null,upcoming:i.videoRenderer.upcomingEventData?.startTime?new Date(parseInt(i.videoRenderer.upcomingEventData.startTime)*1e3):void 0,views:i.videoRenderer.viewCountText?.simpleText?.replace(/\D/g,"")??0,live:!r})}a(Bi,"parseVideo");function Yi(i){if(!i||!i.playlistRenderer)throw new Error("Failed to Parse YouTube Playlist");let e=i.playlistRenderer.thumbnails[0].thumbnails[i.playlistRenderer.thumbnails[0].thumbnails.length-1],t=i.playlistRenderer.shortBylineText.runs?.[0];return new I({id:i.playlistRenderer.playlistId,title:i.playlistRenderer.title.simpleText,thumbnail:{id:i.playlistRenderer.playlistId,url:e.url,height:e.height,width:e.width},channel:{id:t?.navigationEndpoint.browseEndpoint.browseId,name:t?.text,url:`https://www.youtube.com${t?.navigationEndpoint.commandMetadata.webCommandMetadata.url}`},videos:parseInt(i.playlistRenderer.videoCount.replace(/\D/g,""))},!0)}a(Yi,"parsePlaylist");function Pt(i){if(Mi.find(e=>i.url.includes(e)))switch(i.url=i.url.split("?")[0],i.url.split("/").at(-1).split(".")[0]){case"hq2":case"hqdefault":i.width=480,i.height=360;break;case"hq720":i.width=1280,i.height=720;break;case"sddefault":i.width=640,i.height=480;break;case"mqdefault":i.width=320,i.height=180;break;case"default":i.width=120,i.height=90;break;default:i.width=i.height=NaN}}a(Pt,"unblurThumbnail");async function At(i,e={}){let t="https://www.youtube.com/results?search_query="+i;if(e.type??="video",t.indexOf("&sp=")===-1)switch(t+="&sp=",e.type){case"channel":t+="EgIQAg%253D%253D";break;case"playlist":t+="EgIQAw%253D%253D";break;case"video":t+="EgIQAQ%253D%253D";break;default:throw new Error(`Unknown search type: ${e.type}`)}let r=await h(t,{headers:{"accept-language":e.language||"en-US;q=0.9"}});if(r.indexOf("Our systems have detected unusual traffic from your computer network.")!==-1)throw new Error("Captcha page: YouTube has detected that you are a bot!");return Nt(r,e)}a(At,"yt_search");var Me=class Me{constructor(e){this.name=e.name,this.id=e.id,this.isrc=e.external_ids?.isrc||"",this.type="track",this.url=e.external_urls.spotify,this.explicit=e.explicit,this.playable=e.is_playable,this.durationInMs=e.duration_ms,this.durationInSec=Math.round(this.durationInMs/1e3);let t=[];e.artists.forEach(r=>{t.push({name:r.name,id:r.id,url:r.external_urls.spotify})}),this.artists=t,e.album?.name?this.album={name:e.album.name,url:e.external_urls.spotify,id:e.album.id,release_date:e.album.release_date,release_date_precision:e.album.release_date_precision,total_tracks:e.album.total_tracks}:this.album=void 0,e.album?.images?.[0]?this.thumbnail=e.album.images[0]:this.thumbnail=void 0}toJSON(){return{name:this.name,id:this.id,url:this.url,explicit:this.explicit,durationInMs:this.durationInMs,durationInSec:this.durationInSec,artists:this.artists,album:this.album,thumbnail:this.thumbnail}}};a(Me,"SpotifyTrack");var E=Me,qe=class qe{constructor(e,t,r){this.name=e.name,this.type="playlist",this.search=r,this.collaborative=e.collaborative,this.description=e.description,this.url=e.external_urls.spotify,this.id=e.id,this.thumbnail=e.images[0],this.owner={name:e.owner.display_name,url:e.owner.external_urls.spotify,id:e.owner.id},this.tracksCount=Number(e.tracks.total);let s=[];this.search||e.tracks.items.forEach(n=>{n.track&&s.push(new E(n.track))}),this.fetched_tracks=new Map,this.fetched_tracks.set("1",s),this.spotifyData=t}async fetch(){if(this.search)return this;let e;if(this.tracksCount>1e3?e=1e3:e=this.tracksCount,e<=100)return this;let t=[];for(let r=2;r<=Math.ceil(e/100);r++)t.push(new Promise(async(s,n)=>{let o=await h(`https://api.spotify.com/v1/playlists/${this.id}/tracks?offset=${(r-1)*100}&limit=100&market=${this.spotifyData.market}`,{headers:{Authorization:`${this.spotifyData.token_type} ${this.spotifyData.access_token}`}}).catch(u=>n(`Response Error : +${u}`)),l=[];if(typeof o!="string")return;JSON.parse(o).items.forEach(u=>{u.track&&l.push(new E(u.track))}),this.fetched_tracks.set(`${r}`,l),s("Success")}));return await Promise.allSettled(t),this}page(e){if(!e)throw new Error("Page number is not provided");if(!this.fetched_tracks.has(`${e}`))throw new Error("Given Page number is invalid");return this.fetched_tracks.get(`${e}`)}get total_pages(){return this.fetched_tracks.size}get total_tracks(){if(this.search)return this.tracksCount;let e=this.total_pages;return(e-1)*100+this.fetched_tracks.get(`${e}`).length}async all_tracks(){await this.fetch();let e=[];for(let t of this.fetched_tracks.values())e.push(...t);return e}toJSON(){return{name:this.name,collaborative:this.collaborative,description:this.description,url:this.url,id:this.id,thumbnail:this.thumbnail,owner:this.owner,tracksCount:this.tracksCount}}};a(qe,"SpotifyPlaylist");var N=qe,Ue=class Ue{constructor(e,t,r){this.name=e.name,this.type="album",this.id=e.id,this.search=r,this.url=e.external_urls.spotify,this.thumbnail=e.images[0];let s=[];e.artists.forEach(o=>{s.push({name:o.name,id:o.id,url:o.external_urls.spotify})}),this.artists=s,this.copyrights=e.copyrights,this.release_date=e.release_date,this.release_date_precision=e.release_date_precision,this.tracksCount=e.total_tracks;let n=[];this.search||e.tracks.items.forEach(o=>{n.push(new E(o))}),this.fetched_tracks=new Map,this.fetched_tracks.set("1",n),this.spotifyData=t}async fetch(){if(this.search)return this;let e;if(this.tracksCount>500?e=500:e=this.tracksCount,e<=50)return this;let t=[];for(let r=2;r<=Math.ceil(e/50);r++)t.push(new Promise(async(s,n)=>{let o=await h(`https://api.spotify.com/v1/albums/${this.id}/tracks?offset=${(r-1)*50}&limit=50&market=${this.spotifyData.market}`,{headers:{Authorization:`${this.spotifyData.token_type} ${this.spotifyData.access_token}`}}).catch(u=>n(`Response Error : +${u}`)),l=[];if(typeof o!="string")return;JSON.parse(o).items.forEach(u=>{u&&l.push(new E(u))}),this.fetched_tracks.set(`${r}`,l),s("Success")}));return await Promise.allSettled(t),this}page(e){if(!e)throw new Error("Page number is not provided");if(!this.fetched_tracks.has(`${e}`))throw new Error("Given Page number is invalid");return this.fetched_tracks.get(`${e}`)}get total_pages(){return this.fetched_tracks.size}get total_tracks(){if(this.search)return this.tracksCount;let e=this.total_pages;return(e-1)*100+this.fetched_tracks.get(`${e}`).length}async all_tracks(){await this.fetch();let e=[];for(let t of this.fetched_tracks.values())e.push(...t);return e}toJSON(){return{name:this.name,id:this.id,type:this.type,url:this.url,thumbnail:this.thumbnail,artists:this.artists,copyrights:this.copyrights,release_date:this.release_date,release_date_precision:this.release_date_precision,tracksCount:this.tracksCount}}};a(Ue,"SpotifyAlbum");var A=Ue;var J=require("fs");var d;(0,J.existsSync)(".data/spotify.data")&&(d=JSON.parse((0,J.readFileSync)(".data/spotify.data","utf-8")),d.file=!0);var zt=/^((https:)?\/\/)?open\.spotify\.com\/(?:intl\-.{2}\/)?(track|album|playlist)\//;async function Be(i){if(!d)throw new Error(`Spotify Data is missing +Did you forgot to do authorization ?`);let e=i.trim();if(!e.match(zt))throw new Error("This is not a Spotify URL");if(e.indexOf("track/")!==-1){let t=e.split("track/")[1].split("&")[0].split("?")[0],r=await h(`https://api.spotify.com/v1/tracks/${t}?market=${d.market}`,{headers:{Authorization:`${d.token_type} ${d.access_token}`}}).catch(n=>n);if(r instanceof Error)throw r;let s=JSON.parse(r);if(s.error)throw new Error(`Got ${s.error.status} from the spotify request: ${s.error.message}`);return new E(s)}else if(e.indexOf("album/")!==-1){let t=i.split("album/")[1].split("&")[0].split("?")[0],r=await h(`https://api.spotify.com/v1/albums/${t}?market=${d.market}`,{headers:{Authorization:`${d.token_type} ${d.access_token}`}}).catch(n=>n);if(r instanceof Error)throw r;let s=JSON.parse(r);if(s.error)throw new Error(`Got ${s.error.status} from the spotify request: ${s.error.message}`);return new A(s,d,!1)}else if(e.indexOf("playlist/")!==-1){let t=i.split("playlist/")[1].split("&")[0].split("?")[0],r=await h(`https://api.spotify.com/v1/playlists/${t}?market=${d.market}`,{headers:{Authorization:`${d.token_type} ${d.access_token}`}}).catch(n=>n);if(r instanceof Error)throw r;let s=JSON.parse(r);if(s.error)throw new Error(`Got ${s.error.status} from the spotify request: ${s.error.message}`);return new N(s,d,!1)}else throw new Error("URL is out of scope for play-dl.")}a(Be,"spotify");function be(i){let e=i.trim();return e.startsWith("https")?e.match(zt)?e.indexOf("track/")!==-1?"track":e.indexOf("album/")!==-1?"album":e.indexOf("playlist/")!==-1?"playlist":!1:!1:"search"}a(be,"sp_validate");async function Lt(i,e){let t=await h("https://accounts.spotify.com/api/token",{headers:{Authorization:`Basic ${Buffer.from(`${i.client_id}:${i.client_secret}`).toString("base64")}`,"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=authorization_code&code=${i.authorization_code}&redirect_uri=${encodeURI(i.redirect_url)}`,method:"POST"}).catch(s=>s);if(t instanceof Error)throw t;let r=JSON.parse(t);return d={client_id:i.client_id,client_secret:i.client_secret,redirect_url:i.redirect_url,access_token:r.access_token,refresh_token:r.refresh_token,expires_in:Number(r.expires_in),expiry:Date.now()+(r.expires_in-1)*1e3,token_type:r.token_type,market:i.market},e?(0,J.writeFileSync)(".data/spotify.data",JSON.stringify(d,void 0,4)):(console.log(`Client ID : ${d.client_id}`),console.log(`Client Secret : ${d.client_secret}`),console.log(`Refresh Token : ${d.refresh_token}`),console.log(`Market : ${d.market}`),console.log(` +Paste above info in setToken function.`)),!0}a(Lt,"SpotifyAuthorize");function Ye(){return Date.now()>=d.expiry}a(Ye,"is_expired");async function Mt(i,e,t=10){let r=[];if(!d)throw new Error(`Spotify Data is missing +Did you forget to do authorization ?`);if(i.length===0)throw new Error("Pass some query to search.");if(t>50||t<0)throw new Error("You crossed limit range of Spotify [ 0 - 50 ]");let s=await h(`https://api.spotify.com/v1/search?type=${e}&q=${i}&limit=${t}&market=${d.market}`,{headers:{Authorization:`${d.token_type} ${d.access_token}`}}).catch(o=>o);if(s instanceof Error)throw s;let n=JSON.parse(s);return e==="track"?n.tracks.items.forEach(o=>{r.push(new E(o))}):e==="album"?n.albums.items.forEach(o=>{r.push(new A(o,d,!0))}):e==="playlist"&&n.playlists.items.forEach(o=>{r.push(new N(o,d,!0))}),r}a(Mt,"sp_search");async function ge(){let i=await h("https://accounts.spotify.com/api/token",{headers:{Authorization:`Basic ${Buffer.from(`${d.client_id}:${d.client_secret}`).toString("base64")}`,"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=refresh_token&refresh_token=${d.refresh_token}`,method:"POST"}).catch(t=>t);if(i instanceof Error)return!1;let e=JSON.parse(i);return d.access_token=e.access_token,d.expires_in=Number(e.expires_in),d.expiry=Date.now()+(e.expires_in-1)*1e3,d.token_type=e.token_type,d.file&&(0,J.writeFileSync)(".data/spotify.data",JSON.stringify(d,void 0,4)),!0}a(ge,"refreshToken");async function qt(i){d=i,d.file=!1,await ge()}a(qt,"setSpotifyToken");var we=require("fs");var Ut=require("stream");var We=class We{constructor(e){this.name=e.title,this.id=e.id,this.url=e.uri,this.permalink=e.permalink_url,this.fetched=!0,this.type="track",this.durationInSec=Math.round(Number(e.duration)/1e3),this.durationInMs=Number(e.duration),e.publisher_metadata?this.publisher={name:e.publisher_metadata.publisher,id:e.publisher_metadata.id,artist:e.publisher_metadata.artist,contains_music:!!e.publisher_metadata.contains_music||!1,writer_composer:e.publisher_metadata.writer_composer}:this.publisher=null,this.formats=e.media.transcodings,this.user={name:e.user.username,id:e.user.id,type:"user",url:e.user.permalink_url,verified:!!e.user.verified||!1,description:e.user.description,first_name:e.user.first_name,full_name:e.user.full_name,last_name:e.user.last_name,thumbnail:e.user.avatar_url},this.thumbnail=e.artwork_url}toJSON(){return{name:this.name,id:this.id,url:this.url,permalink:this.permalink,fetched:this.fetched,durationInMs:this.durationInMs,durationInSec:this.durationInSec,publisher:this.publisher,formats:this.formats,thumbnail:this.thumbnail,user:this.user}}};a(We,"SoundCloudTrack");var T=We,Je=class Je{constructor(e,t){this.name=e.title,this.id=e.id,this.url=e.uri,this.client_id=t,this.type="playlist",this.sub_type=e.set_type,this.durationInSec=Math.round(Number(e.duration)/1e3),this.durationInMs=Number(e.duration),this.user={name:e.user.username,id:e.user.id,type:"user",url:e.user.permalink_url,verified:!!e.user.verified||!1,description:e.user.description,first_name:e.user.first_name,full_name:e.user.full_name,last_name:e.user.last_name,thumbnail:e.user.avatar_url},this.tracksCount=e.track_count;let r=[];e.tracks.forEach(s=>{s.title?r.push(new T(s)):r.push({id:s.id,fetched:!1,type:"track"})}),this.tracks=r}async fetch(){let e=[];for(let t=0;t{let s=t,n=await h(`https://api-v2.soundcloud.com/tracks/${this.tracks[t].id}?client_id=${this.client_id}`);this.tracks[s]=new T(JSON.parse(n)),r("")}));return await Promise.allSettled(e),this}get total_tracks(){let e=0;return this.tracks.forEach(t=>{if(t instanceof T)e++;else return}),e}async all_tracks(){return await this.fetch(),this.tracks}toJSON(){return{name:this.name,id:this.id,sub_type:this.sub_type,url:this.url,durationInMs:this.durationInMs,durationInSec:this.durationInSec,tracksCount:this.tracksCount,user:this.user,tracks:this.tracks}}};a(Je,"SoundCloudPlaylist");var $=Je,Ve=class Ve{constructor(e,t="arbitrary"){this.stream=new Ut.Readable({highWaterMark:5*1e3*1e3,read(){}}),this.type=t,this.url=e,this.downloaded_time=0,this.request=null,this.downloaded_segments=0,this.time=[],this.timer=new O(()=>{this.timer.reuse(),this.start()},280),this.segment_urls=[],this.stream.on("close",()=>{this.cleanup()}),this.start()}async parser(){let e=await h(this.url).catch(r=>r);if(e instanceof Error)throw e;e.split(` +`).forEach(r=>{r.startsWith("#EXTINF:")?this.time.push(parseFloat(r.replace("#EXTINF:",""))):r.startsWith("https")&&this.segment_urls.push(r)})}async start(){if(this.stream.destroyed){this.cleanup();return}this.time=[],this.segment_urls=[],this.downloaded_time=0,await this.parser(),this.segment_urls.splice(0,this.downloaded_segments),this.loop()}async loop(){if(this.stream.destroyed){this.cleanup();return}if(this.time.length===0||this.segment_urls.length===0){this.cleanup(),this.stream.push(null);return}this.downloaded_time+=this.time.shift(),this.downloaded_segments++;let e=await _(this.segment_urls.shift()).catch(t=>t);if(e instanceof Error){this.stream.emit("error",e),this.cleanup();return}this.request=e,e.on("data",t=>{this.stream.push(t)}),e.on("end",()=>{this.downloaded_time>=300||this.loop()}),e.once("error",t=>{this.stream.emit("error",t)})}cleanup(){this.timer.destroy(),this.request?.destroy(),this.url="",this.downloaded_time=0,this.downloaded_segments=0,this.request=null,this.time=[],this.segment_urls=[]}pause(){this.timer.pause()}resume(){this.timer.resume()}};a(Ve,"SoundCloudStream");var z=Ve;var C;(0,we.existsSync)(".data/soundcloud.data")&&(C=JSON.parse((0,we.readFileSync)(".data/soundcloud.data","utf-8")));var Bt=/^(?:(https?):\/\/)?(?:(?:www|m)\.)?(api\.soundcloud\.com|soundcloud\.com|snd\.sc)\/(.*)$/;async function _e(i){if(!C)throw new Error(`SoundCloud Data is missing +Did you forget to do authorization ?`);let e=i.trim();if(!e.match(Bt))throw new Error("This is not a SoundCloud URL");let t=await h(`https://api-v2.soundcloud.com/resolve?url=${e}&client_id=${C.client_id}`).catch(s=>s);if(t instanceof Error)throw t;let r=JSON.parse(t);if(r.kind!=="track"&&r.kind!=="playlist")throw new Error("This url is out of scope for play-dl.");return r.kind==="track"?new T(r):new $(r,C.client_id)}a(_e,"soundcloud");async function Yt(i,e,t=10){let r=await h(`https://api-v2.soundcloud.com/search/${e}?q=${i}&client_id=${C.client_id}&limit=${t}`),s=[];return JSON.parse(r).collection.forEach(o=>{e==="tracks"?s.push(new T(o)):s.push(new $(o,C.client_id))}),s}a(Yt,"so_search");async function Wt(i,e){let t=await _e(i);if(t instanceof $)throw new Error("Streams can't be created from playlist urls");let r=Ft(t.formats);typeof e!="number"?e=r.length-1:e<=0?e=0:e>=r.length&&(e=r.length-1);let s=r[e].url+"?client_id="+C.client_id,n=JSON.parse(await h(s)),o=r[e].format.mime_type.startsWith("audio/ogg")?"ogg/opus":"arbitrary";return new z(n.url,o)}a(Wt,"stream");async function Fe(){let i=await h("https://soundcloud.com/",{headers:{}}).catch(s=>s);if(i instanceof Error)throw new Error("Failed to get response from soundcloud.com: "+i.message);let e=i.split('')[0]\r\n .split(/(?<=}}});\\s*(var|const|let)\\s/)[0];\r\n if (!player_data) throw new Error('Initial Player Response Data is undefined.');\r\n const initial_data = body\r\n .split('var ytInitialData = ')?.[1]\r\n ?.split(';')[0]\r\n .split(/;\\s*(var|const|let)\\s/)[0];\r\n if (!initial_data) throw new Error('Initial Response Data is undefined.');\r\n const player_response = JSON.parse(player_data);\r\n const initial_response = JSON.parse(initial_data);\r\n const vid = player_response.videoDetails;\r\n\r\n let discretionAdvised = false;\r\n let upcoming = false;\r\n if (player_response.playabilityStatus.status !== 'OK') {\r\n if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') {\r\n if (options.htmldata)\r\n throw new Error(\r\n `Accepting the viewer discretion is not supported when using htmldata, video: ${vid.videoId}`\r\n );\r\n discretionAdvised = true;\r\n const cookies =\r\n initial_response.topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton\r\n .buttonRenderer.command.saveConsentAction;\r\n if (cookies) {\r\n Object.assign(cookieJar, {\r\n VISITOR_INFO1_LIVE: cookies.visitorCookie,\r\n CONSENT: cookies.consentCookie\r\n });\r\n }\r\n\r\n const updatedValues = await acceptViewerDiscretion(vid.videoId, cookieJar, body, true);\r\n player_response.streamingData = updatedValues.streamingData;\r\n initial_response.contents.twoColumnWatchNextResults.secondaryResults = updatedValues.relatedVideos;\r\n } else if (player_response.playabilityStatus.status === 'LIVE_STREAM_OFFLINE') upcoming = true;\r\n else\r\n throw new Error(\r\n `While getting info from url\\n${\r\n player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ??\r\n player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText ??\r\n player_response.playabilityStatus.reason\r\n }`\r\n );\r\n }\r\n const ownerInfo =\r\n initial_response.contents.twoColumnWatchNextResults.results?.results?.contents[1]?.videoSecondaryInfoRenderer\r\n ?.owner?.videoOwnerRenderer;\r\n const badge = ownerInfo?.badges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase();\r\n const html5player = `https://www.youtube.com${body.split('\"jsUrl\":\"')[1].split('\"')[0]}`;\r\n const related: string[] = [];\r\n initial_response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach(\r\n (res: any) => {\r\n if (res.compactVideoRenderer)\r\n related.push(`https://www.youtube.com/watch?v=${res.compactVideoRenderer.videoId}`);\r\n if (res.itemSectionRenderer?.contents)\r\n res.itemSectionRenderer.contents.forEach((x: any) => {\r\n if (x.compactVideoRenderer)\r\n related.push(`https://www.youtube.com/watch?v=${x.compactVideoRenderer.videoId}`);\r\n });\r\n }\r\n );\r\n const microformat = player_response.microformat.playerMicroformatRenderer;\r\n const musicInfo = initial_response.engagementPanels.find((item: any) => item?.engagementPanelSectionListRenderer?.panelIdentifier == 'engagement-panel-structured-description')?.engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items\r\n .find((el: any) => el.videoDescriptionMusicSectionRenderer)?.videoDescriptionMusicSectionRenderer.carouselLockups;\r\n\r\n const music: any[] = [];\r\n if (musicInfo) {\r\n musicInfo.forEach((x: any) => {\r\n if (!x.carouselLockupRenderer) return;\r\n const row = x.carouselLockupRenderer;\r\n\r\n const song = row.videoLockup?.compactVideoRenderer.title.simpleText ?? row.videoLockup?.compactVideoRenderer.title.runs?.find((x:any) => x.text)?.text;\r\n const metadata = row.infoRows?.map((info: any) => [info.infoRowRenderer.title.simpleText.toLowerCase(), ((info.infoRowRenderer.expandedMetadata ?? info.infoRowRenderer.defaultMetadata)?.runs?.map((i:any) => i.text).join(\"\")) ?? info.infoRowRenderer.defaultMetadata?.simpleText ?? info.infoRowRenderer.expandedMetadata?.simpleText ?? \"\"]);\r\n const contents = Object.fromEntries(metadata ?? {});\r\n const id = row.videoLockup?.compactVideoRenderer.navigationEndpoint?.watchEndpoint.videoId\r\n ?? row.infoRows?.find((x: any) => x.infoRowRenderer.title.simpleText.toLowerCase() == \"song\")?.infoRowRenderer.defaultMetadata.runs?.find((x: any) => x.navigationEndpoint)?.navigationEndpoint.watchEndpoint?.videoId;\r\n\r\n music.push({song, url: id ? `https://www.youtube.com/watch?v=${id}` : null, ...contents})\r\n });\r\n }\r\n const rawChapters =\r\n initial_response.playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer.playerBar?.multiMarkersPlayerBarRenderer.markersMap?.find(\r\n (m: any) => m.key === 'DESCRIPTION_CHAPTERS'\r\n )?.value?.chapters;\r\n const chapters: VideoChapter[] = [];\r\n if (rawChapters) {\r\n for (const { chapterRenderer } of rawChapters) {\r\n chapters.push({\r\n title: chapterRenderer.title.simpleText,\r\n timestamp: parseSeconds(chapterRenderer.timeRangeStartMillis / 1000),\r\n seconds: chapterRenderer.timeRangeStartMillis / 1000,\r\n thumbnails: chapterRenderer.thumbnail.thumbnails\r\n });\r\n }\r\n }\r\n let upcomingDate;\r\n if (upcoming) {\r\n if (microformat.liveBroadcastDetails.startTimestamp)\r\n upcomingDate = new Date(microformat.liveBroadcastDetails.startTimestamp);\r\n else {\r\n const timestamp =\r\n player_response.playabilityStatus.liveStreamability.liveStreamabilityRenderer.offlineSlate\r\n .liveStreamOfflineSlateRenderer.scheduledStartTime;\r\n upcomingDate = new Date(parseInt(timestamp) * 1000);\r\n }\r\n }\r\n\r\n const likeRenderer = initial_response.contents.twoColumnWatchNextResults.results.results.contents\r\n .find((content: any) => content.videoPrimaryInfoRenderer)\r\n ?.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons?.find(\r\n (button: any) => button.toggleButtonRenderer?.defaultIcon.iconType === 'LIKE' || button.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultIcon.iconType === 'LIKE'\r\n )\r\n\r\n const video_details = new YouTubeVideo({\r\n id: vid.videoId,\r\n title: vid.title,\r\n description: vid.shortDescription,\r\n duration: Number(vid.lengthSeconds),\r\n duration_raw: parseSeconds(vid.lengthSeconds),\r\n uploadedAt: microformat.publishDate,\r\n liveAt: microformat.liveBroadcastDetails?.startTimestamp,\r\n upcoming: upcomingDate,\r\n thumbnails: vid.thumbnail.thumbnails,\r\n channel: {\r\n name: vid.author,\r\n id: vid.channelId,\r\n url: `https://www.youtube.com/channel/${vid.channelId}`,\r\n verified: Boolean(badge?.includes('verified')),\r\n artist: Boolean(badge?.includes('artist')),\r\n icons: ownerInfo?.thumbnail?.thumbnails || undefined\r\n },\r\n views: vid.viewCount,\r\n tags: vid.keywords,\r\n likes: parseInt(\r\n likeRenderer?.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\\D+/g, '') ?? \r\n likeRenderer?.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\\D+/g, '') ?? 0\r\n ),\r\n live: vid.isLiveContent,\r\n private: vid.isPrivate,\r\n discretionAdvised,\r\n music,\r\n chapters\r\n });\r\n let format = [];\r\n if (!upcoming) {\r\n // TODO: Properly handle the formats, for now ignore and use iOS formats\r\n //format.push(...(player_response.streamingData.formats ?? []));\r\n //format.push(...(player_response.streamingData.adaptiveFormats ?? []));\r\n\r\n // get the formats for the android player for legacy videos\r\n // fixes the stream being closed because not enough data\r\n // arrived in time for ffmpeg to be able to extract audio data\r\n //if (parseAudioFormats(format).length === 0 && !options.htmldata) {\r\n // format = await getAndroidFormats(vid.videoId, cookieJar, body);\r\n //}\r\n format = await getIosFormats(vid.videoId, cookieJar, body);\r\n }\r\n const LiveStreamData = {\r\n isLive: video_details.live,\r\n dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null,\r\n hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null\r\n };\r\n return {\r\n LiveStreamData,\r\n html5player,\r\n format,\r\n video_details,\r\n related_videos: related\r\n };\r\n}\r\n/**\r\n * Gets the data required for streaming from YouTube url, ID or html body data and deciphers it.\r\n *\r\n * Internal function used by {@link stream} instead of {@link video_info}\r\n * because it only extracts the information required for streaming.\r\n *\r\n * @param url YouTube url or ID or html body data\r\n * @param options Video Info Options\r\n * - `boolean` htmldata : given data is html data or not\r\n * @returns Deciphered Video Info {@link StreamInfoData}.\r\n */\r\nexport async function video_stream_info(url: string, options: InfoOptions = {}): Promise {\r\n if (typeof url !== 'string') throw new Error('url parameter is not a URL string or a string of HTML');\r\n let body: string;\r\n const cookieJar = {};\r\n if (options.htmldata) {\r\n body = url;\r\n } else {\r\n const video_id = extractVideoId(url);\r\n if (!video_id) throw new Error('This is not a YouTube Watch URL');\r\n const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`;\r\n body = await request(new_url, {\r\n headers: { 'accept-language': 'en-US,en;q=0.9' },\r\n cookies: true,\r\n cookieJar\r\n });\r\n }\r\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\r\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\r\n const player_data = body\r\n .split('var ytInitialPlayerResponse = ')?.[1]\r\n ?.split(';')[0]\r\n .split(/(?<=}}});\\s*(var|const|let)\\s/)[0];\r\n if (!player_data) throw new Error('Initial Player Response Data is undefined.');\r\n const player_response = JSON.parse(player_data);\r\n let upcoming = false;\r\n if (player_response.playabilityStatus.status !== 'OK') {\r\n if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') {\r\n if (options.htmldata)\r\n throw new Error(\r\n `Accepting the viewer discretion is not supported when using htmldata, video: ${player_response.videoDetails.videoId}`\r\n );\r\n\r\n const initial_data = body\r\n .split('var ytInitialData = ')?.[1]\r\n ?.split(';')[0]\r\n .split(/;\\s*(var|const|let)\\s/)[0];\r\n if (!initial_data) throw new Error('Initial Response Data is undefined.');\r\n\r\n const cookies =\r\n JSON.parse(initial_data).topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton\r\n .buttonRenderer.command.saveConsentAction;\r\n if (cookies) {\r\n Object.assign(cookieJar, {\r\n VISITOR_INFO1_LIVE: cookies.visitorCookie,\r\n CONSENT: cookies.consentCookie\r\n });\r\n }\r\n\r\n const updatedValues = await acceptViewerDiscretion(\r\n player_response.videoDetails.videoId,\r\n cookieJar,\r\n body,\r\n false\r\n );\r\n player_response.streamingData = updatedValues.streamingData;\r\n } else if (player_response.playabilityStatus.status === 'LIVE_STREAM_OFFLINE') upcoming = true;\r\n else\r\n throw new Error(\r\n `While getting info from url\\n${\r\n player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ??\r\n player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText ??\r\n player_response.playabilityStatus.reason\r\n }`\r\n );\r\n }\r\n const html5player = `https://www.youtube.com${body.split('\"jsUrl\":\"')[1].split('\"')[0]}`;\r\n const duration = Number(player_response.videoDetails.lengthSeconds);\r\n const video_details = {\r\n url: `https://www.youtube.com/watch?v=${player_response.videoDetails.videoId}`,\r\n durationInSec: (duration < 0 ? 0 : duration) || 0\r\n };\r\n let format = [];\r\n if (!upcoming) {\r\n // TODO: Properly handle the formats, for now ignore and use iOS formats\r\n //format.push(...(player_response.streamingData.formats ?? []));\r\n //format.push(...(player_response.streamingData.adaptiveFormats ?? []));\r\n\r\n // get the formats for the android player for legacy videos\r\n // fixes the stream being closed because not enough data\r\n // arrived in time for ffmpeg to be able to extract audio data\r\n //if (parseAudioFormats(format).length === 0 && !options.htmldata) {\r\n // format = await getAndroidFormats(player_response.videoDetails.videoId, cookieJar, body);\r\n //}\r\n format = await getIosFormats(player_response.videoDetails.videoId, cookieJar, body);\r\n }\r\n\r\n const LiveStreamData = {\r\n isLive: player_response.videoDetails.isLiveContent,\r\n dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null,\r\n hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null\r\n };\r\n return await decipher_info(\r\n {\r\n LiveStreamData,\r\n html5player,\r\n format,\r\n video_details\r\n },\r\n true\r\n );\r\n}\r\n/**\r\n * Function to convert seconds to [hour : minutes : seconds] format\r\n * @param seconds seconds to convert\r\n * @returns [hour : minutes : seconds] format\r\n */\r\nfunction parseSeconds(seconds: number): string {\r\n const d = Number(seconds);\r\n const h = Math.floor(d / 3600);\r\n const m = Math.floor((d % 3600) / 60);\r\n const s = Math.floor((d % 3600) % 60);\r\n\r\n const hDisplay = h > 0 ? (h < 10 ? `0${h}` : h) + ':' : '';\r\n const mDisplay = m > 0 ? (m < 10 ? `0${m}` : m) + ':' : '00:';\r\n const sDisplay = s > 0 ? (s < 10 ? `0${s}` : s) : '00';\r\n return hDisplay + mDisplay + sDisplay;\r\n}\r\n/**\r\n * Gets data from YouTube url or ID or html body data and deciphers it.\r\n * ```\r\n * video_basic_info + decipher_info = video_info\r\n * ```\r\n *\r\n * Example\r\n * ```ts\r\n * const video = await play.video_info('youtube video url')\r\n *\r\n * const res = ... // Any https package get function.\r\n *\r\n * const video = await play.video_info(res.body, { htmldata : true })\r\n * ```\r\n * @param url YouTube url or ID or html body data\r\n * @param options Video Info Options\r\n * - `boolean` htmldata : given data is html data or not\r\n * @returns Deciphered Video Info {@link InfoData}.\r\n */\r\nexport async function video_info(url: string, options: InfoOptions = {}): Promise {\r\n const data = await video_basic_info(url.trim(), options);\r\n return await decipher_info(data);\r\n}\r\n/**\r\n * Function uses data from video_basic_info and deciphers it if it contains signatures.\r\n * @param data Data - {@link InfoData}\r\n * @param audio_only `boolean` - To decipher only audio formats only.\r\n * @returns Deciphered Video Info {@link InfoData}\r\n */\r\nexport async function decipher_info(\r\n data: T,\r\n audio_only: boolean = false\r\n): Promise {\r\n if (\r\n data.LiveStreamData.isLive === true &&\r\n data.LiveStreamData.dashManifestUrl !== null &&\r\n data.video_details.durationInSec === 0\r\n ) {\r\n return data;\r\n } else if (data.format.length > 0 && (data.format[0].signatureCipher || data.format[0].cipher)) {\r\n if (audio_only) data.format = parseAudioFormats(data.format);\r\n data.format = await format_decipher(data.format, data.html5player);\r\n return data;\r\n } else return data;\r\n}\r\n/**\r\n * Gets YouTube playlist info from a playlist url.\r\n *\r\n * Example\r\n * ```ts\r\n * const playlist = await play.playlist_info('youtube playlist url')\r\n *\r\n * const playlist = await play.playlist_info('youtube playlist url', { incomplete : true })\r\n * ```\r\n * @param url Playlist URL\r\n * @param options Playlist Info Options\r\n * - `boolean` incomplete : When this is set to `false` (default) this function will throw an error\r\n * if the playlist contains hidden videos.\r\n * If it is set to `true`, it parses the playlist skipping the hidden videos,\r\n * only visible videos are included in the resulting {@link YouTubePlaylist}.\r\n *\r\n * @returns YouTube Playlist\r\n */\r\nexport async function playlist_info(url: string, options: PlaylistOptions = {}): Promise {\r\n if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`);\r\n let url_ = url.trim();\r\n if (!url_.startsWith('https')) url_ = `https://www.youtube.com/playlist?list=${url_}`;\r\n if (url_.indexOf('list=') === -1) throw new Error('This is not a Playlist URL');\r\n\r\n if (url_.includes('music.youtube.com')) {\r\n const urlObj = new URL(url_);\r\n urlObj.hostname = 'www.youtube.com';\r\n url_ = urlObj.toString();\r\n }\r\n\r\n const body = await request(url_, {\r\n headers: {\r\n 'accept-language': options.language || 'en-US;q=0.9'\r\n }\r\n });\r\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\r\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\r\n const response = JSON.parse(\r\n body\r\n .split('var ytInitialData = ')[1]\r\n .split(';')[0]\r\n .split(/;\\s*(var|const|let)\\s/)[0]\r\n );\r\n if (response.alerts) {\r\n if (response.alerts[0].alertWithButtonRenderer?.type === 'INFO') {\r\n if (!options.incomplete)\r\n throw new Error(\r\n `While parsing playlist url\\n${response.alerts[0].alertWithButtonRenderer.text.simpleText}`\r\n );\r\n } else if (response.alerts[0].alertRenderer?.type === 'ERROR')\r\n throw new Error(`While parsing playlist url\\n${response.alerts[0].alertRenderer.text.runs[0].text}`);\r\n else throw new Error('While parsing playlist url\\nUnknown Playlist Error');\r\n }\r\n if (response.currentVideoEndpoint) {\r\n return getWatchPlaylist(response, body, url_);\r\n } else return getNormalPlaylist(response, body);\r\n}\r\n/**\r\n * Function to parse Playlist from YouTube search\r\n * @param data html data of that request\r\n * @param limit No. of videos to parse\r\n * @returns Array of YouTubeVideo.\r\n */\r\nexport function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {\r\n const videos = [];\r\n\r\n for (let i = 0; i < data.length; i++) {\r\n if (limit === videos.length) break;\r\n const info = data[i].playlistVideoRenderer;\r\n if (!info || !info.shortBylineText) continue;\r\n\r\n videos.push(\r\n new YouTubeVideo({\r\n id: info.videoId,\r\n duration: parseInt(info.lengthSeconds) || 0,\r\n duration_raw: info.lengthText?.simpleText ?? '0:00',\r\n thumbnails: info.thumbnail.thumbnails,\r\n title: info.title.runs[0].text,\r\n upcoming: info.upcomingEventData?.startTime\r\n ? new Date(parseInt(info.upcomingEventData.startTime) * 1000)\r\n : undefined,\r\n channel: {\r\n id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined,\r\n name: info.shortBylineText.runs[0].text || undefined,\r\n url: `https://www.youtube.com${\r\n info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\r\n info.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url\r\n }`,\r\n icon: undefined\r\n }\r\n })\r\n );\r\n }\r\n return videos;\r\n}\r\n/**\r\n * Function to get Continuation Token\r\n * @param data html data of playlist url\r\n * @returns token\r\n */\r\nexport function getContinuationToken(data: any): string {\r\n return data.find((x: any) => Object.keys(x)[0] === 'continuationItemRenderer')?.continuationItemRenderer\r\n .continuationEndpoint?.continuationCommand?.token;\r\n}\r\n\r\nasync function acceptViewerDiscretion(\r\n videoId: string,\r\n cookieJar: { [key: string]: string },\r\n body: string,\r\n extractRelated: boolean\r\n): Promise<{ streamingData: any; relatedVideos?: any }> {\r\n const apiKey =\r\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\r\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\r\n DEFAULT_API_KEY;\r\n const sessionToken =\r\n body.split('\"XSRF_TOKEN\":\"')[1]?.split('\"')[0].replaceAll('\\\\u003d', '=') ??\r\n body.split('\"xsrf_token\":\"')[1]?.split('\"')[0].replaceAll('\\\\u003d', '=');\r\n if (!sessionToken)\r\n throw new Error(`Unable to extract XSRF_TOKEN to accept the viewer discretion popup for video: ${videoId}.`);\r\n\r\n const verificationResponse = await request(`https://www.youtube.com/youtubei/v1/verify_age?key=${apiKey}&prettyPrint=false`, {\r\n method: 'POST',\r\n body: JSON.stringify({\r\n context: {\r\n client: {\r\n utcOffsetMinutes: 0,\r\n gl: 'US',\r\n hl: 'en',\r\n clientName: 'WEB',\r\n clientVersion:\r\n body.split('\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"')[1]?.split('\"')[0] ??\r\n body.split('\"innertube_context_client_version\":\"')[1]?.split('\"')[0] ??\r\n ''\r\n },\r\n user: {},\r\n request: {}\r\n },\r\n nextEndpoint: {\r\n urlEndpoint: {\r\n url: `/watch?v=${videoId}&has_verified=1`\r\n }\r\n },\r\n setControvercy: true\r\n }),\r\n cookies: true,\r\n cookieJar\r\n });\r\n\r\n const endpoint = JSON.parse(verificationResponse).actions[0].navigateAction.endpoint;\r\n\r\n const videoPage = await request(`https://www.youtube.com/${endpoint.urlEndpoint.url}&pbj=1`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded'\r\n },\r\n body: new URLSearchParams([\r\n ['command', JSON.stringify(endpoint)],\r\n ['session_token', sessionToken]\r\n ]).toString(),\r\n cookies: true,\r\n cookieJar\r\n });\r\n\r\n if (videoPage.includes('

Something went wrong

'))\r\n throw new Error(`Unable to accept the viewer discretion popup for video: ${videoId}`);\r\n\r\n const videoPageData = JSON.parse(videoPage);\r\n\r\n if (videoPageData[2].playerResponse.playabilityStatus.status !== 'OK')\r\n throw new Error(\r\n `While getting info from url after trying to accept the discretion popup for video ${videoId}\\n${\r\n videoPageData[2].playerResponse.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason\r\n .simpleText ??\r\n videoPageData[2].playerResponse.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText\r\n }`\r\n );\r\n\r\n const streamingData = videoPageData[2].playerResponse.streamingData;\r\n\r\n if (extractRelated)\r\n return {\r\n streamingData,\r\n relatedVideos: videoPageData[3].response.contents.twoColumnWatchNextResults.secondaryResults\r\n };\r\n\r\n return { streamingData };\r\n}\r\n\r\nasync function getIosFormats(videoId: string, cookieJar: { [key: string]: string }, body: string): Promise {\r\n const apiKey =\r\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\r\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\r\n DEFAULT_API_KEY;\r\n\r\n const response = await request(`https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`, {\r\n method: 'POST',\r\n body: JSON.stringify({\r\n context: {\r\n client: {\r\n clientName: 'IOS',\r\n clientVersion: '19.09.3',\r\n deviceModel: 'iPhone16,1',\r\n userAgent: 'com.google.ios.youtube/19.09.3 (iPhone; CPU iPhone OS 17_5 like Mac OS X)',\r\n hl: 'en',\r\n timeZone: 'UTC',\r\n utcOffsetMinutes: 0\r\n }\r\n },\r\n videoId: videoId,\r\n playbackContext: { contentPlaybackContext: { html5Preference: 'HTML5_PREF_WANTS' } },\r\n contentCheckOk: true,\r\n racyCheckOk: true\r\n }),\r\n cookies: true,\r\n cookieJar\r\n });\r\n\r\n return JSON.parse(response).streamingData.adaptiveFormats;\r\n //return JSON.parse(response).streamingData.formats;\r\n}\r\n\r\nfunction getWatchPlaylist(response: any, body: any, url: string): YouTubePlayList {\r\n const playlist_details = response.contents.twoColumnWatchNextResults.playlist?.playlist;\r\n if (!playlist_details)\r\n throw new Error(\"Watch playlist unavailable due to YouTube layout changes.\")\r\n\r\n const videos = getWatchPlaylistVideos(playlist_details.contents);\r\n const API_KEY =\r\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\r\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\r\n DEFAULT_API_KEY;\r\n\r\n const videoCount = playlist_details.totalVideos;\r\n const channel = playlist_details.shortBylineText?.runs?.[0];\r\n const badge = playlist_details.badges?.[0]?.metadataBadgeRenderer?.style.toLowerCase();\r\n\r\n return new YouTubePlayList({\r\n continuation: {\r\n api: API_KEY,\r\n token: getContinuationToken(playlist_details.contents),\r\n clientVersion:\r\n body.split('\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"')[1]?.split('\"')[0] ??\r\n body.split('\"innertube_context_client_version\":\"')[1]?.split('\"')[0] ??\r\n ''\r\n },\r\n id: playlist_details.playlistId || '',\r\n title: playlist_details.title || '',\r\n videoCount: parseInt(videoCount) || 0,\r\n videos: videos,\r\n url: url,\r\n channel: {\r\n id: channel?.navigationEndpoint?.browseEndpoint?.browseId || null,\r\n name: channel?.text || null,\r\n url: `https://www.youtube.com${\r\n channel?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl ||\r\n channel?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url\r\n }`,\r\n verified: Boolean(badge?.includes('verified')),\r\n artist: Boolean(badge?.includes('artist'))\r\n }\r\n });\r\n}\r\n\r\nfunction getNormalPlaylist(response: any, body: any): YouTubePlayList {\r\n const json_data =\r\n response.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0]\r\n .itemSectionRenderer.contents[0].playlistVideoListRenderer.contents;\r\n const playlist_details = response.sidebar.playlistSidebarRenderer.items;\r\n\r\n const API_KEY =\r\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\r\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\r\n DEFAULT_API_KEY;\r\n const videos = getPlaylistVideos(json_data, 100);\r\n\r\n const data = playlist_details[0].playlistSidebarPrimaryInfoRenderer;\r\n if (!data.title.runs || !data.title.runs.length) throw new Error('Failed to Parse Playlist info.');\r\n\r\n const author = playlist_details[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner;\r\n const views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/\\D/g, '') : 0;\r\n const lastUpdate =\r\n data.stats\r\n .find((x: any) => 'runs' in x && x['runs'].find((y: any) => y.text.toLowerCase().includes('last update')))\r\n ?.runs.pop()?.text ?? null;\r\n const videosCount = data.stats[0].runs[0].text.replace(/\\D/g, '') || 0;\r\n\r\n const res = new YouTubePlayList({\r\n continuation: {\r\n api: API_KEY,\r\n token: getContinuationToken(json_data),\r\n clientVersion:\r\n body.split('\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"')[1]?.split('\"')[0] ??\r\n body.split('\"innertube_context_client_version\":\"')[1]?.split('\"')[0] ??\r\n ''\r\n },\r\n id: data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId,\r\n title: data.title.runs[0].text,\r\n videoCount: parseInt(videosCount) || 0,\r\n lastUpdate: lastUpdate,\r\n views: parseInt(views) || 0,\r\n videos: videos,\r\n url: `https://www.youtube.com/playlist?list=${data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId}`,\r\n link: `https://www.youtube.com${data.title.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,\r\n channel: author\r\n ? {\r\n name: author.videoOwnerRenderer.title.runs[0].text,\r\n id: author.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId,\r\n url: `https://www.youtube.com${\r\n author.videoOwnerRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url ||\r\n author.videoOwnerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl\r\n }`,\r\n icons: author.videoOwnerRenderer.thumbnail.thumbnails ?? []\r\n }\r\n : {},\r\n thumbnail: data.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length\r\n ? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[\r\n data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length - 1\r\n ]\r\n : null\r\n });\r\n return res;\r\n}\r\n\r\nfunction getWatchPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {\r\n const videos: YouTubeVideo[] = [];\r\n\r\n for (let i = 0; i < data.length; i++) {\r\n if (limit === videos.length) break;\r\n const info = data[i].playlistPanelVideoRenderer;\r\n if (!info || !info.shortBylineText) continue;\r\n const channel_info = info.shortBylineText.runs[0];\r\n\r\n videos.push(\r\n new YouTubeVideo({\r\n id: info.videoId,\r\n duration: parseDuration(info.lengthText?.simpleText) || 0,\r\n duration_raw: info.lengthText?.simpleText ?? '0:00',\r\n thumbnails: info.thumbnail.thumbnails,\r\n title: info.title.simpleText,\r\n upcoming:\r\n info.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer?.style === 'UPCOMING' || undefined,\r\n channel: {\r\n id: channel_info.navigationEndpoint.browseEndpoint.browseId || undefined,\r\n name: channel_info.text || undefined,\r\n url: `https://www.youtube.com${\r\n channel_info.navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\r\n channel_info.navigationEndpoint.commandMetadata.webCommandMetadata.url\r\n }`,\r\n icon: undefined\r\n }\r\n })\r\n );\r\n }\r\n\r\n return videos;\r\n}\r\n\r\nfunction parseDuration(text: string): number {\r\n if (!text) return 0;\r\n const split = text.split(':');\r\n\r\n switch (split.length) {\r\n case 2:\r\n return parseInt(split[0]) * 60 + parseInt(split[1]);\r\n\r\n case 3:\r\n return parseInt(split[0]) * 60 * 60 + parseInt(split[1]) * 60 + parseInt(split[2]);\r\n\r\n default:\r\n return 0;\r\n }\r\n}","import { WebmElements, WebmHeader } from 'play-audio';\r\nimport { Duplex, DuplexOptions } from 'node:stream';\r\n\r\nenum DataType {\r\n master,\r\n string,\r\n uint,\r\n binary,\r\n float\r\n}\r\n\r\nexport enum WebmSeekerState {\r\n READING_HEAD = 'READING_HEAD',\r\n READING_DATA = 'READING_DATA'\r\n}\r\n\r\ninterface WebmSeekerOptions extends DuplexOptions {\r\n mode?: 'precise' | 'granular';\r\n}\r\n\r\nconst WEB_ELEMENT_KEYS = Object.keys(WebmElements);\r\n\r\nexport class WebmSeeker extends Duplex {\r\n remaining?: Buffer;\r\n state: WebmSeekerState;\r\n chunk?: Buffer;\r\n cursor: number;\r\n header: WebmHeader;\r\n headfound: boolean;\r\n headerparsed: boolean;\r\n seekfound: boolean;\r\n private data_size: number;\r\n private offset: number;\r\n private data_length: number;\r\n private sec: number;\r\n private time: number;\r\n\r\n constructor(sec: number, options: WebmSeekerOptions) {\r\n super(options);\r\n this.state = WebmSeekerState.READING_HEAD;\r\n this.cursor = 0;\r\n this.header = new WebmHeader();\r\n this.headfound = false;\r\n this.headerparsed = false;\r\n this.seekfound = false;\r\n this.data_length = 0;\r\n this.data_size = 0;\r\n this.offset = 0;\r\n this.sec = sec;\r\n this.time = Math.floor(sec / 10) * 10;\r\n }\r\n\r\n private get vint_length(): number {\r\n let i = 0;\r\n for (; i < 8; i++) {\r\n if ((1 << (7 - i)) & this.chunk![this.cursor]) break;\r\n }\r\n return ++i;\r\n }\r\n\r\n private vint_value(): boolean {\r\n if (!this.chunk) return false;\r\n const length = this.vint_length;\r\n if (this.chunk.length < this.cursor + length) return false;\r\n let value = this.chunk[this.cursor] & ((1 << (8 - length)) - 1);\r\n for (let i = this.cursor + 1; i < this.cursor + length; i++) value = (value << 8) + this.chunk[i];\r\n this.data_size = length;\r\n this.data_length = value;\r\n return true;\r\n }\r\n\r\n cleanup() {\r\n this.cursor = 0;\r\n this.chunk = undefined;\r\n this.remaining = undefined;\r\n }\r\n\r\n _read() {}\r\n\r\n seek(content_length: number): Error | number {\r\n let clusterlength = 0,\r\n position = 0;\r\n let time_left = (this.sec - this.time) * 1000 || 0;\r\n time_left = Math.round(time_left / 20) * 20;\r\n if (!this.header.segment.cues) return new Error('Failed to Parse Cues');\r\n\r\n for (let i = 0; i < this.header.segment.cues.length; i++) {\r\n const data = this.header.segment.cues[i];\r\n if (Math.floor((data.time as number) / 1000) === this.time) {\r\n position = data.position as number;\r\n clusterlength = (this.header.segment.cues[i + 1]?.position || content_length) - position - 1;\r\n break;\r\n } else continue;\r\n }\r\n if (clusterlength === 0) return position;\r\n return this.offset + Math.round(position + (time_left / 20) * (clusterlength / 500));\r\n }\r\n\r\n _write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void {\r\n if (this.remaining) {\r\n this.chunk = Buffer.concat([this.remaining, chunk]);\r\n this.remaining = undefined;\r\n } else this.chunk = chunk;\r\n\r\n let err: Error | undefined;\r\n\r\n if (this.state === WebmSeekerState.READING_HEAD) err = this.readHead();\r\n else if (!this.seekfound) err = this.getClosestBlock();\r\n else err = this.readTag();\r\n\r\n if (err) callback(err);\r\n else callback();\r\n }\r\n\r\n private readHead(): Error | undefined {\r\n if (!this.chunk) return new Error('Chunk is missing');\r\n\r\n while (this.chunk.length > this.cursor) {\r\n const oldCursor = this.cursor;\r\n const id = this.vint_length;\r\n if (this.chunk.length < this.cursor + id) break;\r\n\r\n const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'));\r\n this.cursor += id;\r\n\r\n if (!this.vint_value()) {\r\n this.cursor = oldCursor;\r\n break;\r\n }\r\n if (!ebmlID) {\r\n this.cursor += this.data_size + this.data_length;\r\n continue;\r\n }\r\n\r\n if (!this.headfound) {\r\n if (ebmlID.name === 'ebml') this.headfound = true;\r\n else return new Error('Failed to find EBML ID at start of stream.');\r\n }\r\n const data = this.chunk.slice(\r\n this.cursor + this.data_size,\r\n this.cursor + this.data_size + this.data_length\r\n );\r\n const parse = this.header.parse(ebmlID, data);\r\n if (parse instanceof Error) return parse;\r\n\r\n // stop parsing the header once we have found the correct cue\r\n\r\n if (ebmlID.name === 'seekHead') this.offset = oldCursor;\r\n\r\n if (\r\n ebmlID.name === 'cueClusterPosition' &&\r\n this.header.segment.cues!.length > 2 &&\r\n this.time === (this.header.segment.cues!.at(-2)!.time as number) / 1000\r\n )\r\n this.emit('headComplete');\r\n\r\n if (ebmlID.type === DataType.master) {\r\n this.cursor += this.data_size;\r\n continue;\r\n }\r\n\r\n if (this.chunk.length < this.cursor + this.data_size + this.data_length) {\r\n this.cursor = oldCursor;\r\n break;\r\n } else this.cursor += this.data_size + this.data_length;\r\n }\r\n this.remaining = this.chunk.slice(this.cursor);\r\n this.cursor = 0;\r\n }\r\n\r\n private readTag(): Error | undefined {\r\n if (!this.chunk) return new Error('Chunk is missing');\r\n\r\n while (this.chunk.length > this.cursor) {\r\n const oldCursor = this.cursor;\r\n const id = this.vint_length;\r\n if (this.chunk.length < this.cursor + id) break;\r\n\r\n const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'));\r\n this.cursor += id;\r\n\r\n if (!this.vint_value()) {\r\n this.cursor = oldCursor;\r\n break;\r\n }\r\n if (!ebmlID) {\r\n this.cursor += this.data_size + this.data_length;\r\n continue;\r\n }\r\n\r\n const data = this.chunk.slice(\r\n this.cursor + this.data_size,\r\n this.cursor + this.data_size + this.data_length\r\n );\r\n const parse = this.header.parse(ebmlID, data);\r\n if (parse instanceof Error) return parse;\r\n\r\n if (ebmlID.type === DataType.master) {\r\n this.cursor += this.data_size;\r\n continue;\r\n }\r\n\r\n if (this.chunk.length < this.cursor + this.data_size + this.data_length) {\r\n this.cursor = oldCursor;\r\n break;\r\n } else this.cursor += this.data_size + this.data_length;\r\n\r\n if (ebmlID.name === 'simpleBlock') {\r\n const track = this.header.segment.tracks![this.header.audioTrack];\r\n if (!track || track.trackType !== 2) return new Error('No audio Track in this webm file.');\r\n if ((data[0] & 0xf) === track.trackNumber) this.push(data.slice(4));\r\n }\r\n }\r\n this.remaining = this.chunk.slice(this.cursor);\r\n this.cursor = 0;\r\n }\r\n\r\n private getClosestBlock(): Error | undefined {\r\n if (this.sec === 0) {\r\n this.seekfound = true;\r\n return this.readTag();\r\n }\r\n if (!this.chunk) return new Error('Chunk is missing');\r\n this.cursor = 0;\r\n let positionFound = false;\r\n while (!positionFound && this.cursor < this.chunk.length) {\r\n this.cursor = this.chunk.indexOf('a3', this.cursor, 'hex');\r\n if (this.cursor === -1) return new Error('Failed to find nearest Block.');\r\n this.cursor++;\r\n if (!this.vint_value()) return new Error('Failed to find correct simpleBlock in first chunk');\r\n if (this.cursor + this.data_length + this.data_length > this.chunk.length) continue;\r\n const data = this.chunk.slice(\r\n this.cursor + this.data_size,\r\n this.cursor + this.data_size + this.data_length\r\n );\r\n const track = this.header.segment.tracks![this.header.audioTrack];\r\n if (!track || track.trackType !== 2) return new Error('No audio Track in this webm file.');\r\n if ((data[0] & 0xf) === track.trackNumber) {\r\n this.cursor += this.data_size + this.data_length;\r\n this.push(data.slice(4));\r\n positionFound = true;\r\n } else continue;\r\n }\r\n if (!positionFound) return new Error('Failed to find nearest correct simple Block.');\r\n this.seekfound = true;\r\n return this.readTag();\r\n }\r\n\r\n private parseEbmlID(ebmlID: string) {\r\n if (WEB_ELEMENT_KEYS.includes(ebmlID)) return WebmElements[ebmlID];\r\n else return false;\r\n }\r\n\r\n _destroy(error: Error | null, callback: (error: Error | null) => void): void {\r\n this.cleanup();\r\n callback(error);\r\n }\r\n\r\n _final(callback: (error?: Error | null) => void): void {\r\n this.cleanup();\r\n callback();\r\n }\r\n}\r\n","import { IncomingMessage } from 'node:http';\r\nimport { request_stream } from '../../Request';\r\nimport { parseAudioFormats, StreamOptions, StreamType } from '../stream';\r\nimport { video_stream_info } from '../utils/extractor';\r\nimport { Timer } from './LiveStream';\r\nimport { WebmSeeker, WebmSeekerState } from './WebmSeeker';\r\n\r\n/**\r\n * YouTube Stream Class for seeking audio to a timeStamp.\r\n */\r\nexport class SeekStream {\r\n /**\r\n * WebmSeeker Stream through which data passes\r\n */\r\n stream: WebmSeeker;\r\n /**\r\n * Type of audio data that we recieved from normal youtube url.\r\n */\r\n type: StreamType;\r\n /**\r\n * Audio Endpoint Format Url to get data from.\r\n */\r\n private url: string;\r\n /**\r\n * Used to calculate no of bytes data that we have recieved\r\n */\r\n private bytes_count: number;\r\n /**\r\n * Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds)\r\n */\r\n private per_sec_bytes: number;\r\n /**\r\n * Length of the header in bytes\r\n */\r\n private header_length: number;\r\n /**\r\n * Total length of audio file in bytes\r\n */\r\n private content_length: number;\r\n /**\r\n * YouTube video url. [ Used only for retrying purposes only. ]\r\n */\r\n private video_url: string;\r\n /**\r\n * Timer for looping data every 265 seconds.\r\n */\r\n private timer: Timer;\r\n /**\r\n * Quality given by user. [ Used only for retrying purposes only. ]\r\n */\r\n private quality: number;\r\n /**\r\n * Incoming message that we recieve.\r\n *\r\n * Storing this is essential.\r\n * This helps to destroy the TCP connection completely if you stopped player in between the stream\r\n */\r\n private request: IncomingMessage | null;\r\n /**\r\n * YouTube Stream Class constructor\r\n * @param url Audio Endpoint url.\r\n * @param type Type of Stream\r\n * @param duration Duration of audio playback [ in seconds ]\r\n * @param headerLength Length of the header in bytes.\r\n * @param contentLength Total length of Audio file in bytes.\r\n * @param bitrate Bitrate provided by YouTube.\r\n * @param video_url YouTube video url.\r\n * @param options Options provided to stream function.\r\n */\r\n constructor(\r\n url: string,\r\n duration: number,\r\n headerLength: number,\r\n contentLength: number,\r\n bitrate: number,\r\n video_url: string,\r\n options: StreamOptions\r\n ) {\r\n this.stream = new WebmSeeker(options.seek!, {\r\n highWaterMark: 5 * 1000 * 1000,\r\n readableObjectMode: true\r\n });\r\n this.url = url;\r\n this.quality = options.quality as number;\r\n this.type = StreamType.Opus;\r\n this.bytes_count = 0;\r\n this.video_url = video_url;\r\n this.per_sec_bytes = bitrate ? Math.ceil(bitrate / 8) : Math.ceil(contentLength / duration);\r\n this.header_length = headerLength;\r\n this.content_length = contentLength;\r\n this.request = null;\r\n this.timer = new Timer(() => {\r\n this.timer.reuse();\r\n this.loop();\r\n }, 265);\r\n this.stream.on('close', () => {\r\n this.timer.destroy();\r\n this.cleanup();\r\n });\r\n this.seek();\r\n }\r\n /**\r\n * **INTERNAL Function**\r\n *\r\n * Uses stream functions to parse Webm Head and gets Offset byte to seek to.\r\n * @returns Nothing\r\n */\r\n private async seek(): Promise {\r\n const parse = await new Promise(async (res, rej) => {\r\n if (!this.stream.headerparsed) {\r\n const stream = await request_stream(this.url, {\r\n headers: {\r\n range: `bytes=0-${this.header_length}`\r\n }\r\n }).catch((err: Error) => err);\r\n\r\n if (stream instanceof Error) {\r\n rej(stream);\r\n return;\r\n }\r\n if (Number(stream.statusCode) >= 400) {\r\n rej(400);\r\n return;\r\n }\r\n this.request = stream;\r\n stream.pipe(this.stream, { end: false });\r\n\r\n // headComplete should always be called, leaving this here just in case\r\n stream.once('end', () => {\r\n this.stream.state = WebmSeekerState.READING_DATA;\r\n res('');\r\n });\r\n\r\n this.stream.once('headComplete', () => {\r\n stream.unpipe(this.stream);\r\n stream.destroy();\r\n this.stream.state = WebmSeekerState.READING_DATA;\r\n res('');\r\n });\r\n } else res('');\r\n }).catch((err) => err);\r\n if (parse instanceof Error) {\r\n this.stream.emit('error', parse);\r\n this.bytes_count = 0;\r\n this.per_sec_bytes = 0;\r\n this.cleanup();\r\n return;\r\n } else if (parse === 400) {\r\n await this.retry();\r\n this.timer.reuse();\r\n return this.seek();\r\n }\r\n const bytes = this.stream.seek(this.content_length);\r\n if (bytes instanceof Error) {\r\n this.stream.emit('error', bytes);\r\n this.bytes_count = 0;\r\n this.per_sec_bytes = 0;\r\n this.cleanup();\r\n return;\r\n }\r\n\r\n this.stream.seekfound = false;\r\n this.bytes_count = bytes;\r\n this.timer.reuse();\r\n this.loop();\r\n }\r\n /**\r\n * Retry if we get 404 or 403 Errors.\r\n */\r\n private async retry() {\r\n const info = await video_stream_info(this.video_url);\r\n const audioFormat = parseAudioFormats(info.format);\r\n this.url = audioFormat[this.quality].url;\r\n }\r\n /**\r\n * This cleans every used variable in class.\r\n *\r\n * This is used to prevent re-use of this class and helping garbage collector to collect it.\r\n */\r\n private cleanup() {\r\n this.request?.destroy();\r\n this.request = null;\r\n this.url = '';\r\n }\r\n /**\r\n * Getting data from audio endpoint url and passing it to stream.\r\n *\r\n * If 404 or 403 occurs, it will retry again.\r\n */\r\n private async loop() {\r\n if (this.stream.destroyed) {\r\n this.timer.destroy();\r\n this.cleanup();\r\n return;\r\n }\r\n const end: number = this.bytes_count + this.per_sec_bytes * 300;\r\n const stream = await request_stream(this.url, {\r\n headers: {\r\n range: `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}`\r\n }\r\n }).catch((err: Error) => err);\r\n if (stream instanceof Error) {\r\n this.stream.emit('error', stream);\r\n this.bytes_count = 0;\r\n this.per_sec_bytes = 0;\r\n this.cleanup();\r\n return;\r\n }\r\n if (Number(stream.statusCode) >= 400) {\r\n this.cleanup();\r\n await this.retry();\r\n this.timer.reuse();\r\n this.loop();\r\n return;\r\n }\r\n this.request = stream;\r\n stream.pipe(this.stream, { end: false });\r\n\r\n stream.once('error', async () => {\r\n this.cleanup();\r\n await this.retry();\r\n this.timer.reuse();\r\n this.loop();\r\n });\r\n\r\n stream.on('data', (chunk: any) => {\r\n this.bytes_count += chunk.length;\r\n });\r\n\r\n stream.on('end', () => {\r\n if (end >= this.content_length) {\r\n this.timer.destroy();\r\n this.stream.end();\r\n this.cleanup();\r\n }\r\n });\r\n }\r\n /**\r\n * Pauses timer.\r\n * Stops running of loop.\r\n *\r\n * Useful if you don't want to get excess data to be stored in stream.\r\n */\r\n pause() {\r\n this.timer.pause();\r\n }\r\n /**\r\n * Resumes timer.\r\n * Starts running of loop.\r\n */\r\n resume() {\r\n this.timer.resume();\r\n }\r\n}\r\n","import { request_content_length, request_stream } from '../Request';\r\nimport { LiveStream, Stream } from './classes/LiveStream';\r\nimport { SeekStream } from './classes/SeekStream';\r\nimport { InfoData, StreamInfoData } from './utils/constants';\r\nimport { video_stream_info } from './utils/extractor';\r\nimport { URL } from 'node:url';\r\n\r\nexport enum StreamType {\r\n Arbitrary = 'arbitrary',\r\n Raw = 'raw',\r\n OggOpus = 'ogg/opus',\r\n WebmOpus = 'webm/opus',\r\n Opus = 'opus'\r\n}\r\n\r\nexport interface StreamOptions {\r\n seek?: number;\r\n quality?: number;\r\n language?: string;\r\n htmldata?: boolean;\r\n precache?: number;\r\n discordPlayerCompatibility?: boolean;\r\n}\r\n\r\n/**\r\n * Command to find audio formats from given format array\r\n * @param formats Formats to search from\r\n * @returns Audio Formats array\r\n */\r\nexport function parseAudioFormats(formats: any[]) {\r\n const result: any[] = [];\r\n formats.forEach((format) => {\r\n const type = format.mimeType as string;\r\n if (type.startsWith('audio')) {\r\n format.codec = type.split('codecs=\"')[1].split('\"')[0];\r\n format.container = type.split('audio/')[1].split(';')[0];\r\n result.push(format);\r\n }\r\n });\r\n return result;\r\n}\r\n/**\r\n * Type for YouTube Stream\r\n */\r\nexport type YouTubeStream = Stream | LiveStream | SeekStream;\r\n/**\r\n * Stream command for YouTube\r\n * @param url YouTube URL\r\n * @param options lets you add quality for stream\r\n * @returns Stream class with type and stream for playing.\r\n */\r\nexport async function stream(url: string, options: StreamOptions = {}): Promise {\r\n const info = await video_stream_info(url, { htmldata: options.htmldata, language: options.language });\r\n return await stream_from_info(info, options);\r\n}\r\n/**\r\n * Stream command for YouTube using info from video_info or decipher_info function.\r\n * @param info video_info data\r\n * @param options lets you add quality for stream\r\n * @returns Stream class with type and stream for playing.\r\n */\r\nexport async function stream_from_info(\r\n info: InfoData | StreamInfoData,\r\n options: StreamOptions = {}\r\n): Promise {\r\n if (info.format.length === 0)\r\n throw new Error('Upcoming and premiere videos that are not currently live cannot be streamed.');\r\n if (options.quality && !Number.isInteger(options.quality))\r\n throw new Error(\"Quality must be set to an integer.\")\r\n\r\n const final: any[] = [];\r\n if (\r\n info.LiveStreamData.isLive === true &&\r\n info.LiveStreamData.dashManifestUrl !== null &&\r\n info.video_details.durationInSec === 0\r\n ) {\r\n return new LiveStream(\r\n info.LiveStreamData.dashManifestUrl,\r\n info.format[info.format.length - 1].targetDurationSec as number,\r\n info.video_details.url,\r\n options.precache\r\n );\r\n }\r\n\r\n const audioFormat = parseAudioFormats(info.format);\r\n if (typeof options.quality !== 'number') options.quality = audioFormat.length - 1;\r\n else if (options.quality <= 0) options.quality = 0;\r\n else if (options.quality >= audioFormat.length) options.quality = audioFormat.length - 1;\r\n if (audioFormat.length !== 0) final.push(audioFormat[options.quality]);\r\n else final.push(info.format[info.format.length - 1]);\r\n let type: StreamType =\r\n final[0].codec === 'opus' && final[0].container === 'webm' ? StreamType.WebmOpus : StreamType.Arbitrary;\r\n await request_stream(`https://${new URL(final[0].url).host}/generate_204`);\r\n if (type === StreamType.WebmOpus) {\r\n if (!options.discordPlayerCompatibility) {\r\n options.seek ??= 0;\r\n if (options.seek >= info.video_details.durationInSec || options.seek < 0)\r\n throw new Error(`Seeking beyond limit. [ 0 - ${info.video_details.durationInSec - 1}]`);\r\n return new SeekStream(\r\n final[0].url,\r\n info.video_details.durationInSec,\r\n final[0].indexRange.end,\r\n Number(final[0].contentLength),\r\n Number(final[0].bitrate),\r\n info.video_details.url,\r\n options\r\n );\r\n } else if (options.seek) throw new Error('Can not seek with discordPlayerCompatibility set to true.');\r\n }\r\n\r\n let contentLength;\r\n if (final[0].contentLength) {\r\n contentLength = Number(final[0].contentLength);\r\n } else {\r\n contentLength = await request_content_length(final[0].url);\r\n }\r\n\r\n return new Stream(\r\n final[0].url,\r\n type,\r\n info.video_details.durationInSec,\r\n contentLength,\r\n info.video_details.url,\r\n options\r\n );\r\n}\r\n","import { YouTubeVideo } from '../classes/Video';\r\nimport { YouTubePlayList } from '../classes/Playlist';\r\nimport { YouTubeChannel } from '../classes/Channel';\r\nimport { YouTube } from '..';\r\nimport { YouTubeThumbnail } from '../classes/Thumbnail';\r\n\r\nconst BLURRED_THUMBNAILS = [\r\n '-oaymwEpCOADEI4CSFryq4qpAxsIARUAAAAAGAElAADIQj0AgKJDeAHtAZmZGUI=',\r\n '-oaymwEiCOADEI4CSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BmZkZQg==',\r\n '-oaymwEiCOgCEMoBSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmbmQQ==',\r\n '-oaymwEiCNAFEJQDSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmZmQg==',\r\n '-oaymwEdCNAFEJQDSFryq4qpAw8IARUAAIhCGAHtAWZmZkI=',\r\n '-oaymwEdCNACELwBSFryq4qpAw8IARUAAIhCGAHtAT0K10E='\r\n];\r\n\r\nexport interface ParseSearchInterface {\r\n type?: 'video' | 'playlist' | 'channel';\r\n limit?: number;\r\n language?: string;\r\n unblurNSFWThumbnails?: boolean;\r\n}\r\n\r\nexport interface thumbnail {\r\n width: string;\r\n height: string;\r\n url: string;\r\n}\r\n/**\r\n * Main command which converts html body data and returns the type of data requested.\r\n * @param html body of that request\r\n * @param options limit & type of YouTube search you want.\r\n * @returns Array of one of YouTube type.\r\n */\r\nexport function ParseSearchResult(html: string, options?: ParseSearchInterface): YouTube[] {\r\n if (!html) throw new Error(\"Can't parse Search result without data\");\r\n if (!options) options = { type: 'video', limit: 0 };\r\n else if (!options.type) options.type = 'video';\r\n const hasLimit = typeof options.limit === 'number' && options.limit > 0;\r\n options.unblurNSFWThumbnails ??= false;\r\n\r\n const data = html\r\n .split('var ytInitialData = ')?.[1]\r\n ?.split(';')[0]\r\n .split(/;\\s*(var|const|let)\\s/)[0];\r\n const json_data = JSON.parse(data);\r\n const results = [];\r\n const details =\r\n json_data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.flatMap(\r\n (s: any) => s.itemSectionRenderer?.contents\r\n );\r\n for (const detail of details) {\r\n if (hasLimit && results.length === options.limit) break;\r\n if (!detail || (!detail.videoRenderer && !detail.channelRenderer && !detail.playlistRenderer)) continue;\r\n switch (options.type) {\r\n case 'video': {\r\n const parsed = parseVideo(detail);\r\n if (parsed) {\r\n if (options.unblurNSFWThumbnails) parsed.thumbnails.forEach(unblurThumbnail);\r\n results.push(parsed);\r\n }\r\n break;\r\n }\r\n case 'channel': {\r\n const parsed = parseChannel(detail);\r\n if (parsed) results.push(parsed);\r\n break;\r\n }\r\n case 'playlist': {\r\n const parsed = parsePlaylist(detail);\r\n if (parsed) {\r\n if (options.unblurNSFWThumbnails && parsed.thumbnail) unblurThumbnail(parsed.thumbnail);\r\n results.push(parsed);\r\n }\r\n break;\r\n }\r\n default:\r\n throw new Error(`Unknown search type: ${options.type}`);\r\n }\r\n }\r\n return results;\r\n}\r\n/**\r\n * Function to convert [hour : minutes : seconds] format to seconds\r\n * @param duration hour : minutes : seconds format\r\n * @returns seconds\r\n */\r\nfunction parseDuration(duration: string): number {\r\n if (!duration) return 0;\r\n const args = duration.split(':');\r\n let dur = 0;\r\n\r\n switch (args.length) {\r\n case 3:\r\n dur = parseInt(args[0]) * 60 * 60 + parseInt(args[1]) * 60 + parseInt(args[2]);\r\n break;\r\n case 2:\r\n dur = parseInt(args[0]) * 60 + parseInt(args[1]);\r\n break;\r\n default:\r\n dur = parseInt(args[0]);\r\n }\r\n\r\n return dur;\r\n}\r\n/**\r\n * Function to parse Channel searches\r\n * @param data body of that channel request.\r\n * @returns YouTubeChannel class\r\n */\r\nexport function parseChannel(data?: any): YouTubeChannel {\r\n if (!data || !data.channelRenderer) throw new Error('Failed to Parse YouTube Channel');\r\n const badge = data.channelRenderer.ownerBadges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase();\r\n const url = `https://www.youtube.com${\r\n data.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\r\n data.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url\r\n }`;\r\n const thumbnail = data.channelRenderer.thumbnail.thumbnails[data.channelRenderer.thumbnail.thumbnails.length - 1];\r\n const res = new YouTubeChannel({\r\n id: data.channelRenderer.channelId,\r\n name: data.channelRenderer.title.simpleText,\r\n icon: {\r\n url: thumbnail.url.replace('//', 'https://'),\r\n width: thumbnail.width,\r\n height: thumbnail.height\r\n },\r\n url: url,\r\n verified: Boolean(badge?.includes('verified')),\r\n artist: Boolean(badge?.includes('artist')),\r\n subscribers: data.channelRenderer.subscriberCountText?.simpleText ?? '0 subscribers'\r\n });\r\n\r\n return res;\r\n}\r\n/**\r\n * Function to parse Video searches\r\n * @param data body of that video request.\r\n * @returns YouTubeVideo class\r\n */\r\nexport function parseVideo(data?: any): YouTubeVideo {\r\n if (!data || !data.videoRenderer) throw new Error('Failed to Parse YouTube Video');\r\n\r\n const channel = data.videoRenderer.ownerText.runs[0];\r\n const badge = data.videoRenderer.ownerBadges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase();\r\n const durationText = data.videoRenderer.lengthText;\r\n const res = new YouTubeVideo({\r\n id: data.videoRenderer.videoId,\r\n url: `https://www.youtube.com/watch?v=${data.videoRenderer.videoId}`,\r\n title: data.videoRenderer.title.runs[0].text,\r\n description: data.videoRenderer.detailedMetadataSnippets?.[0].snippetText.runs?.length\r\n ? data.videoRenderer.detailedMetadataSnippets[0].snippetText.runs.map((run: any) => run.text).join('')\r\n : '',\r\n duration: durationText ? parseDuration(durationText.simpleText) : 0,\r\n duration_raw: durationText ? durationText.simpleText : null,\r\n thumbnails: data.videoRenderer.thumbnail.thumbnails,\r\n channel: {\r\n id: channel.navigationEndpoint.browseEndpoint.browseId || null,\r\n name: channel.text || null,\r\n url: `https://www.youtube.com${\r\n channel.navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\r\n channel.navigationEndpoint.commandMetadata.webCommandMetadata.url\r\n }`,\r\n icons: data.videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail\r\n .thumbnails,\r\n verified: Boolean(badge?.includes('verified')),\r\n artist: Boolean(badge?.includes('artist'))\r\n },\r\n uploadedAt: data.videoRenderer.publishedTimeText?.simpleText ?? null,\r\n upcoming: data.videoRenderer.upcomingEventData?.startTime\r\n ? new Date(parseInt(data.videoRenderer.upcomingEventData.startTime) * 1000)\r\n : undefined,\r\n views: data.videoRenderer.viewCountText?.simpleText?.replace(/\\D/g, '') ?? 0,\r\n live: durationText ? false : true\r\n });\r\n\r\n return res;\r\n}\r\n/**\r\n * Function to parse Playlist searches\r\n * @param data body of that playlist request.\r\n * @returns YouTubePlaylist class\r\n */\r\nexport function parsePlaylist(data?: any): YouTubePlayList {\r\n if (!data || !data.playlistRenderer) throw new Error('Failed to Parse YouTube Playlist');\r\n\r\n const thumbnail =\r\n data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1];\r\n const channel = data.playlistRenderer.shortBylineText.runs?.[0];\r\n\r\n const res = new YouTubePlayList(\r\n {\r\n id: data.playlistRenderer.playlistId,\r\n title: data.playlistRenderer.title.simpleText,\r\n thumbnail: {\r\n id: data.playlistRenderer.playlistId,\r\n url: thumbnail.url,\r\n height: thumbnail.height,\r\n width: thumbnail.width\r\n },\r\n channel: {\r\n id: channel?.navigationEndpoint.browseEndpoint.browseId,\r\n name: channel?.text,\r\n url: `https://www.youtube.com${channel?.navigationEndpoint.commandMetadata.webCommandMetadata.url}`\r\n },\r\n videos: parseInt(data.playlistRenderer.videoCount.replace(/\\D/g, ''))\r\n },\r\n true\r\n );\r\n\r\n return res;\r\n}\r\n\r\nfunction unblurThumbnail(thumbnail: YouTubeThumbnail) {\r\n if (BLURRED_THUMBNAILS.find((sqp) => thumbnail.url.includes(sqp))) {\r\n thumbnail.url = thumbnail.url.split('?')[0];\r\n\r\n // we need to update the size parameters as the sqp parameter also included a cropped size\r\n switch (thumbnail.url.split('/').at(-1)!.split('.')[0]) {\r\n case 'hq2':\r\n case 'hqdefault':\r\n thumbnail.width = 480;\r\n thumbnail.height = 360;\r\n break;\r\n case 'hq720':\r\n thumbnail.width = 1280;\r\n thumbnail.height = 720;\r\n break;\r\n case 'sddefault':\r\n thumbnail.width = 640;\r\n thumbnail.height = 480;\r\n break;\r\n case 'mqdefault':\r\n thumbnail.width = 320;\r\n thumbnail.height = 180;\r\n break;\r\n case 'default':\r\n thumbnail.width = 120;\r\n thumbnail.height = 90;\r\n break;\r\n default:\r\n thumbnail.width = thumbnail.height = NaN;\r\n }\r\n }\r\n}\r\n","import { request } from './../Request';\r\nimport { ParseSearchInterface, ParseSearchResult } from './utils/parser';\r\nimport { YouTubeVideo } from './classes/Video';\r\nimport { YouTubeChannel } from './classes/Channel';\r\nimport { YouTubePlayList } from './classes/Playlist';\r\n\r\nenum SearchType {\r\n Video = 'EgIQAQ%253D%253D',\r\n PlayList = 'EgIQAw%253D%253D',\r\n Channel = 'EgIQAg%253D%253D'\r\n}\r\n\r\n/**\r\n * Type for YouTube returns\r\n */\r\nexport type YouTube = YouTubeVideo | YouTubeChannel | YouTubePlayList;\r\n/**\r\n * Command to search from YouTube\r\n * @param search The query to search\r\n * @param options limit & type of YouTube search you want.\r\n * @returns YouTube type.\r\n */\r\nexport async function yt_search(search: string, options: ParseSearchInterface = {}): Promise {\r\n let url = 'https://www.youtube.com/results?search_query=' + search;\r\n options.type ??= 'video';\r\n if (url.indexOf('&sp=') === -1) {\r\n url += '&sp=';\r\n switch (options.type) {\r\n case 'channel':\r\n url += SearchType.Channel;\r\n break;\r\n case 'playlist':\r\n url += SearchType.PlayList;\r\n break;\r\n case 'video':\r\n url += SearchType.Video;\r\n break;\r\n default:\r\n throw new Error(`Unknown search type: ${options.type}`);\r\n }\r\n }\r\n const body = await request(url, {\r\n headers: {\r\n 'accept-language': options.language || 'en-US;q=0.9'\r\n }\r\n });\r\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\r\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\r\n return ParseSearchResult(body, options);\r\n}\r\n","import { request } from '../Request';\r\nimport { SpotifyDataOptions } from '.';\r\nimport { AlbumJSON, PlaylistJSON, TrackJSON } from './constants';\r\n\r\nexport interface SpotifyTrackAlbum {\r\n /**\r\n * Spotify Track Album name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Track Album url\r\n */\r\n url: string;\r\n /**\r\n * Spotify Track Album id\r\n */\r\n id: string;\r\n /**\r\n * Spotify Track Album release date\r\n */\r\n release_date: string;\r\n /**\r\n * Spotify Track Album release date **precise**\r\n */\r\n release_date_precision: string;\r\n /**\r\n * Spotify Track Album total tracks number\r\n */\r\n total_tracks: number;\r\n}\r\n\r\nexport interface SpotifyArtists {\r\n /**\r\n * Spotify Artist Name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Artist Url\r\n */\r\n url: string;\r\n /**\r\n * Spotify Artist ID\r\n */\r\n id: string;\r\n}\r\n\r\nexport interface SpotifyThumbnail {\r\n /**\r\n * Spotify Thumbnail height\r\n */\r\n height: number;\r\n /**\r\n * Spotify Thumbnail width\r\n */\r\n width: number;\r\n /**\r\n * Spotify Thumbnail url\r\n */\r\n url: string;\r\n}\r\n\r\nexport interface SpotifyCopyright {\r\n /**\r\n * Spotify Copyright Text\r\n */\r\n text: string;\r\n /**\r\n * Spotify Copyright Type\r\n */\r\n type: string;\r\n}\r\n/**\r\n * Spotify Track Class\r\n */\r\nexport class SpotifyTrack {\r\n /**\r\n * Spotify Track Name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Class type. == \"track\"\r\n */\r\n type: 'track' | 'playlist' | 'album';\r\n /**\r\n * Spotify Track ID\r\n */\r\n id: string;\r\n /**\r\n * Spotify Track ISRC\r\n */\r\n isrc: string;\r\n /**\r\n * Spotify Track url\r\n */\r\n url: string;\r\n /**\r\n * Spotify Track explicit info.\r\n */\r\n explicit: boolean;\r\n /**\r\n * Spotify Track playability info.\r\n */\r\n playable: boolean;\r\n /**\r\n * Spotify Track Duration in seconds\r\n */\r\n durationInSec: number;\r\n /**\r\n * Spotify Track Duration in milli seconds\r\n */\r\n durationInMs: number;\r\n /**\r\n * Spotify Track Artists data [ array ]\r\n */\r\n artists: SpotifyArtists[];\r\n /**\r\n * Spotify Track Album data\r\n */\r\n album: SpotifyTrackAlbum | undefined;\r\n /**\r\n * Spotify Track Thumbnail Data\r\n */\r\n thumbnail: SpotifyThumbnail | undefined;\r\n /**\r\n * Constructor for Spotify Track\r\n * @param data\r\n */\r\n constructor(data: any) {\r\n this.name = data.name;\r\n this.id = data.id;\r\n this.isrc = data.external_ids?.isrc || '';\r\n this.type = 'track';\r\n this.url = data.external_urls.spotify;\r\n this.explicit = data.explicit;\r\n this.playable = data.is_playable;\r\n this.durationInMs = data.duration_ms;\r\n this.durationInSec = Math.round(this.durationInMs / 1000);\r\n const artists: SpotifyArtists[] = [];\r\n data.artists.forEach((v: any) => {\r\n artists.push({\r\n name: v.name,\r\n id: v.id,\r\n url: v.external_urls.spotify\r\n });\r\n });\r\n this.artists = artists;\r\n if (!data.album?.name) this.album = undefined;\r\n else {\r\n this.album = {\r\n name: data.album.name,\r\n url: data.external_urls.spotify,\r\n id: data.album.id,\r\n release_date: data.album.release_date,\r\n release_date_precision: data.album.release_date_precision,\r\n total_tracks: data.album.total_tracks\r\n };\r\n }\r\n if (!data.album?.images?.[0]) this.thumbnail = undefined;\r\n else this.thumbnail = data.album.images[0];\r\n }\r\n\r\n toJSON(): TrackJSON {\r\n return {\r\n name: this.name,\r\n id: this.id,\r\n url: this.url,\r\n explicit: this.explicit,\r\n durationInMs: this.durationInMs,\r\n durationInSec: this.durationInSec,\r\n artists: this.artists,\r\n album: this.album,\r\n thumbnail: this.thumbnail\r\n };\r\n }\r\n}\r\n/**\r\n * Spotify Playlist Class\r\n */\r\nexport class SpotifyPlaylist {\r\n /**\r\n * Spotify Playlist Name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Class type. == \"playlist\"\r\n */\r\n type: 'track' | 'playlist' | 'album';\r\n /**\r\n * Spotify Playlist collaborative boolean.\r\n */\r\n collaborative: boolean;\r\n /**\r\n * Spotify Playlist Description\r\n */\r\n description: string;\r\n /**\r\n * Spotify Playlist URL\r\n */\r\n url: string;\r\n /**\r\n * Spotify Playlist ID\r\n */\r\n id: string;\r\n /**\r\n * Spotify Playlist Thumbnail Data\r\n */\r\n thumbnail: SpotifyThumbnail;\r\n /**\r\n * Spotify Playlist Owner Artist data\r\n */\r\n owner: SpotifyArtists;\r\n /**\r\n * Spotify Playlist total tracks Count\r\n */\r\n tracksCount: number;\r\n /**\r\n * Spotify Playlist Spotify data\r\n *\r\n * @private\r\n */\r\n private spotifyData: SpotifyDataOptions;\r\n /**\r\n * Spotify Playlist fetched tracks Map\r\n *\r\n * @private\r\n */\r\n private fetched_tracks: Map;\r\n /**\r\n * Boolean to tell whether it is a searched result or not.\r\n */\r\n private readonly search: boolean;\r\n /**\r\n * Constructor for Spotify Playlist Class\r\n * @param data JSON parsed data of playlist\r\n * @param spotifyData Data about sporify token for furhter fetching.\r\n */\r\n constructor(data: any, spotifyData: SpotifyDataOptions, search: boolean) {\r\n this.name = data.name;\r\n this.type = 'playlist';\r\n this.search = search;\r\n this.collaborative = data.collaborative;\r\n this.description = data.description;\r\n this.url = data.external_urls.spotify;\r\n this.id = data.id;\r\n this.thumbnail = data.images[0];\r\n this.owner = {\r\n name: data.owner.display_name,\r\n url: data.owner.external_urls.spotify,\r\n id: data.owner.id\r\n };\r\n this.tracksCount = Number(data.tracks.total);\r\n const videos: SpotifyTrack[] = [];\r\n if (!this.search)\r\n data.tracks.items.forEach((v: any) => {\r\n if (v.track) videos.push(new SpotifyTrack(v.track));\r\n });\r\n this.fetched_tracks = new Map();\r\n this.fetched_tracks.set('1', videos);\r\n this.spotifyData = spotifyData;\r\n }\r\n /**\r\n * Fetches Spotify Playlist tracks more than 100 tracks.\r\n *\r\n * For getting all tracks in playlist, see `total_pages` property.\r\n * @returns Playlist Class.\r\n */\r\n async fetch() {\r\n if (this.search) return this;\r\n let fetching: number;\r\n if (this.tracksCount > 1000) fetching = 1000;\r\n else fetching = this.tracksCount;\r\n if (fetching <= 100) return this;\r\n const work = [];\r\n for (let i = 2; i <= Math.ceil(fetching / 100); i++) {\r\n work.push(\r\n new Promise(async (resolve, reject) => {\r\n const response = await request(\r\n `https://api.spotify.com/v1/playlists/${this.id}/tracks?offset=${\r\n (i - 1) * 100\r\n }&limit=100&market=${this.spotifyData.market}`,\r\n {\r\n headers: {\r\n Authorization: `${this.spotifyData.token_type} ${this.spotifyData.access_token}`\r\n }\r\n }\r\n ).catch((err) => reject(`Response Error : \\n${err}`));\r\n const videos: SpotifyTrack[] = [];\r\n if (typeof response !== 'string') return;\r\n const json_data = JSON.parse(response);\r\n json_data.items.forEach((v: any) => {\r\n if (v.track) videos.push(new SpotifyTrack(v.track));\r\n });\r\n this.fetched_tracks.set(`${i}`, videos);\r\n resolve('Success');\r\n })\r\n );\r\n }\r\n await Promise.allSettled(work);\r\n return this;\r\n }\r\n /**\r\n * Spotify Playlist tracks are divided in pages.\r\n *\r\n * For example getting data of 101 - 200 videos in a playlist,\r\n *\r\n * ```ts\r\n * const playlist = await play.spotify('playlist url')\r\n *\r\n * await playlist.fetch()\r\n *\r\n * const result = playlist.page(2)\r\n * ```\r\n * @param num Page Number\r\n * @returns\r\n */\r\n page(num: number) {\r\n if (!num) throw new Error('Page number is not provided');\r\n if (!this.fetched_tracks.has(`${num}`)) throw new Error('Given Page number is invalid');\r\n return this.fetched_tracks.get(`${num}`) as SpotifyTrack[];\r\n }\r\n /**\r\n * Gets total number of pages in that playlist class.\r\n * @see {@link SpotifyPlaylist.all_tracks}\r\n */\r\n get total_pages() {\r\n return this.fetched_tracks.size;\r\n }\r\n /**\r\n * Spotify Playlist total no of tracks that have been fetched so far.\r\n */\r\n get total_tracks() {\r\n if (this.search) return this.tracksCount;\r\n const page_number: number = this.total_pages;\r\n return (page_number - 1) * 100 + (this.fetched_tracks.get(`${page_number}`) as SpotifyTrack[]).length;\r\n }\r\n /**\r\n * Fetches all the tracks in the playlist and returns them\r\n *\r\n * ```ts\r\n * const playlist = await play.spotify('playlist url')\r\n *\r\n * const tracks = await playlist.all_tracks()\r\n * ```\r\n * @returns An array of {@link SpotifyTrack}\r\n */\r\n async all_tracks(): Promise {\r\n await this.fetch();\r\n\r\n const tracks: SpotifyTrack[] = [];\r\n\r\n for (const page of this.fetched_tracks.values()) tracks.push(...page);\r\n\r\n return tracks;\r\n }\r\n /**\r\n * Converts Class to JSON\r\n * @returns JSON data\r\n */\r\n toJSON(): PlaylistJSON {\r\n return {\r\n name: this.name,\r\n collaborative: this.collaborative,\r\n description: this.description,\r\n url: this.url,\r\n id: this.id,\r\n thumbnail: this.thumbnail,\r\n owner: this.owner,\r\n tracksCount: this.tracksCount\r\n };\r\n }\r\n}\r\n/**\r\n * Spotify Album Class\r\n */\r\nexport class SpotifyAlbum {\r\n /**\r\n * Spotify Album Name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Class type. == \"album\"\r\n */\r\n type: 'track' | 'playlist' | 'album';\r\n /**\r\n * Spotify Album url\r\n */\r\n url: string;\r\n /**\r\n * Spotify Album id\r\n */\r\n id: string;\r\n /**\r\n * Spotify Album Thumbnail data\r\n */\r\n thumbnail: SpotifyThumbnail;\r\n /**\r\n * Spotify Album artists [ array ]\r\n */\r\n artists: SpotifyArtists[];\r\n /**\r\n * Spotify Album copyright data [ array ]\r\n */\r\n copyrights: SpotifyCopyright[];\r\n /**\r\n * Spotify Album Release date\r\n */\r\n release_date: string;\r\n /**\r\n * Spotify Album Release Date **precise**\r\n */\r\n release_date_precision: string;\r\n /**\r\n * Spotify Album total no of tracks\r\n */\r\n tracksCount: number;\r\n /**\r\n * Spotify Album Spotify data\r\n *\r\n * @private\r\n */\r\n private spotifyData: SpotifyDataOptions;\r\n /**\r\n * Spotify Album fetched tracks Map\r\n *\r\n * @private\r\n */\r\n private fetched_tracks: Map;\r\n /**\r\n * Boolean to tell whether it is a searched result or not.\r\n */\r\n private readonly search: boolean;\r\n /**\r\n * Constructor for Spotify Album Class\r\n * @param data Json parsed album data\r\n * @param spotifyData Spotify credentials\r\n */\r\n constructor(data: any, spotifyData: SpotifyDataOptions, search: boolean) {\r\n this.name = data.name;\r\n this.type = 'album';\r\n this.id = data.id;\r\n this.search = search;\r\n this.url = data.external_urls.spotify;\r\n this.thumbnail = data.images[0];\r\n const artists: SpotifyArtists[] = [];\r\n data.artists.forEach((v: any) => {\r\n artists.push({\r\n name: v.name,\r\n id: v.id,\r\n url: v.external_urls.spotify\r\n });\r\n });\r\n this.artists = artists;\r\n this.copyrights = data.copyrights;\r\n this.release_date = data.release_date;\r\n this.release_date_precision = data.release_date_precision;\r\n this.tracksCount = data.total_tracks;\r\n const videos: SpotifyTrack[] = [];\r\n if (!this.search)\r\n data.tracks.items.forEach((v: any) => {\r\n videos.push(new SpotifyTrack(v));\r\n });\r\n this.fetched_tracks = new Map();\r\n this.fetched_tracks.set('1', videos);\r\n this.spotifyData = spotifyData;\r\n }\r\n /**\r\n * Fetches Spotify Album tracks more than 50 tracks.\r\n *\r\n * For getting all tracks in album, see `total_pages` property.\r\n * @returns Album Class.\r\n */\r\n async fetch() {\r\n if (this.search) return this;\r\n let fetching: number;\r\n if (this.tracksCount > 500) fetching = 500;\r\n else fetching = this.tracksCount;\r\n if (fetching <= 50) return this;\r\n const work = [];\r\n for (let i = 2; i <= Math.ceil(fetching / 50); i++) {\r\n work.push(\r\n new Promise(async (resolve, reject) => {\r\n const response = await request(\r\n `https://api.spotify.com/v1/albums/${this.id}/tracks?offset=${(i - 1) * 50}&limit=50&market=${\r\n this.spotifyData.market\r\n }`,\r\n {\r\n headers: {\r\n Authorization: `${this.spotifyData.token_type} ${this.spotifyData.access_token}`\r\n }\r\n }\r\n ).catch((err) => reject(`Response Error : \\n${err}`));\r\n const videos: SpotifyTrack[] = [];\r\n if (typeof response !== 'string') return;\r\n const json_data = JSON.parse(response);\r\n json_data.items.forEach((v: any) => {\r\n if (v) videos.push(new SpotifyTrack(v));\r\n });\r\n this.fetched_tracks.set(`${i}`, videos);\r\n resolve('Success');\r\n })\r\n );\r\n }\r\n await Promise.allSettled(work);\r\n return this;\r\n }\r\n /**\r\n * Spotify Album tracks are divided in pages.\r\n *\r\n * For example getting data of 51 - 100 videos in a album,\r\n *\r\n * ```ts\r\n * const album = await play.spotify('album url')\r\n *\r\n * await album.fetch()\r\n *\r\n * const result = album.page(2)\r\n * ```\r\n * @param num Page Number\r\n * @returns\r\n */\r\n page(num: number) {\r\n if (!num) throw new Error('Page number is not provided');\r\n if (!this.fetched_tracks.has(`${num}`)) throw new Error('Given Page number is invalid');\r\n return this.fetched_tracks.get(`${num}`);\r\n }\r\n /**\r\n * Gets total number of pages in that album class.\r\n * @see {@link SpotifyAlbum.all_tracks}\r\n */\r\n get total_pages() {\r\n return this.fetched_tracks.size;\r\n }\r\n /**\r\n * Spotify Album total no of tracks that have been fetched so far.\r\n */\r\n get total_tracks() {\r\n if (this.search) return this.tracksCount;\r\n const page_number: number = this.total_pages;\r\n return (page_number - 1) * 100 + (this.fetched_tracks.get(`${page_number}`) as SpotifyTrack[]).length;\r\n }\r\n /**\r\n * Fetches all the tracks in the album and returns them\r\n *\r\n * ```ts\r\n * const album = await play.spotify('album url')\r\n *\r\n * const tracks = await album.all_tracks()\r\n * ```\r\n * @returns An array of {@link SpotifyTrack}\r\n */\r\n async all_tracks(): Promise {\r\n await this.fetch();\r\n\r\n const tracks: SpotifyTrack[] = [];\r\n\r\n for (const page of this.fetched_tracks.values()) tracks.push(...page);\r\n\r\n return tracks;\r\n }\r\n /**\r\n * Converts Class to JSON\r\n * @returns JSON data\r\n */\r\n toJSON(): AlbumJSON {\r\n return {\r\n name: this.name,\r\n id: this.id,\r\n type: this.type,\r\n url: this.url,\r\n thumbnail: this.thumbnail,\r\n artists: this.artists,\r\n copyrights: this.copyrights,\r\n release_date: this.release_date,\r\n release_date_precision: this.release_date_precision,\r\n tracksCount: this.tracksCount\r\n };\r\n }\r\n}\r\n","import { request } from '../Request';\r\nimport { SpotifyAlbum, SpotifyPlaylist, SpotifyTrack } from './classes';\r\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs';\r\n\r\nlet spotifyData: SpotifyDataOptions;\r\nif (existsSync('.data/spotify.data')) {\r\n spotifyData = JSON.parse(readFileSync('.data/spotify.data', 'utf-8'));\r\n spotifyData.file = true;\r\n}\r\n/**\r\n * Spotify Data options that are stored in spotify.data file.\r\n */\r\nexport interface SpotifyDataOptions {\r\n client_id: string;\r\n client_secret: string;\r\n redirect_url?: string;\r\n authorization_code?: string;\r\n access_token?: string;\r\n refresh_token?: string;\r\n token_type?: string;\r\n expires_in?: number;\r\n expiry?: number;\r\n market?: string;\r\n file?: boolean;\r\n}\r\n\r\nconst pattern = /^((https:)?\\/\\/)?open\\.spotify\\.com\\/(?:intl\\-.{2}\\/)?(track|album|playlist)\\//;\r\n/**\r\n * Gets Spotify url details.\r\n *\r\n * ```ts\r\n * let spot = await play.spotify('spotify url')\r\n *\r\n * // spot.type === \"track\" | \"playlist\" | \"album\"\r\n *\r\n * if (spot.type === \"track\") {\r\n * spot = spot as play.SpotifyTrack\r\n * // Code with spotify track class.\r\n * }\r\n * ```\r\n * @param url Spotify Url\r\n * @returns A {@link SpotifyTrack} or {@link SpotifyPlaylist} or {@link SpotifyAlbum}\r\n */\r\nexport async function spotify(url: string): Promise {\r\n if (!spotifyData) throw new Error('Spotify Data is missing\\nDid you forgot to do authorization ?');\r\n const url_ = url.trim();\r\n if (!url_.match(pattern)) throw new Error('This is not a Spotify URL');\r\n if (url_.indexOf('track/') !== -1) {\r\n const trackID = url_.split('track/')[1].split('&')[0].split('?')[0];\r\n const response = await request(`https://api.spotify.com/v1/tracks/${trackID}?market=${spotifyData.market}`, {\r\n headers: {\r\n Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`\r\n }\r\n }).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const resObj = JSON.parse(response);\r\n if (resObj.error) throw new Error(`Got ${resObj.error.status} from the spotify request: ${resObj.error.message}`);\r\n return new SpotifyTrack(resObj);\r\n } else if (url_.indexOf('album/') !== -1) {\r\n const albumID = url.split('album/')[1].split('&')[0].split('?')[0];\r\n const response = await request(`https://api.spotify.com/v1/albums/${albumID}?market=${spotifyData.market}`, {\r\n headers: {\r\n Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`\r\n }\r\n }).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const resObj = JSON.parse(response);\r\n if (resObj.error) throw new Error(`Got ${resObj.error.status} from the spotify request: ${resObj.error.message}`);\r\n return new SpotifyAlbum(resObj, spotifyData, false);\r\n } else if (url_.indexOf('playlist/') !== -1) {\r\n const playlistID = url.split('playlist/')[1].split('&')[0].split('?')[0];\r\n const response = await request(\r\n `https://api.spotify.com/v1/playlists/${playlistID}?market=${spotifyData.market}`,\r\n {\r\n headers: {\r\n Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`\r\n }\r\n }\r\n ).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const resObj = JSON.parse(response);\r\n if (resObj.error) throw new Error(`Got ${resObj.error.status} from the spotify request: ${resObj.error.message}`);\r\n return new SpotifyPlaylist(resObj, spotifyData, false);\r\n } else throw new Error('URL is out of scope for play-dl.');\r\n}\r\n/**\r\n * Validate Spotify url\r\n * @param url Spotify URL\r\n * @returns\r\n * ```ts\r\n * 'track' | 'playlist' | 'album' | 'search' | false\r\n * ```\r\n */\r\nexport function sp_validate(url: string): 'track' | 'playlist' | 'album' | 'search' | false {\r\n const url_ = url.trim();\r\n if (!url_.startsWith('https')) return 'search';\r\n if (!url_.match(pattern)) return false;\r\n if (url_.indexOf('track/') !== -1) {\r\n return 'track';\r\n } else if (url_.indexOf('album/') !== -1) {\r\n return 'album';\r\n } else if (url_.indexOf('playlist/') !== -1) {\r\n return 'playlist';\r\n } else return false;\r\n}\r\n/**\r\n * Fuction for authorizing for spotify data.\r\n * @param data Sportify Data options to validate\r\n * @returns boolean.\r\n */\r\nexport async function SpotifyAuthorize(data: SpotifyDataOptions, file: boolean): Promise {\r\n const response = await request(`https://accounts.spotify.com/api/token`, {\r\n headers: {\r\n 'Authorization': `Basic ${Buffer.from(`${data.client_id}:${data.client_secret}`).toString('base64')}`,\r\n 'Content-Type': 'application/x-www-form-urlencoded'\r\n },\r\n body: `grant_type=authorization_code&code=${data.authorization_code}&redirect_uri=${encodeURI(\r\n data.redirect_url as string\r\n )}`,\r\n method: 'POST'\r\n }).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const resp_json = JSON.parse(response);\r\n spotifyData = {\r\n client_id: data.client_id,\r\n client_secret: data.client_secret,\r\n redirect_url: data.redirect_url,\r\n access_token: resp_json.access_token,\r\n refresh_token: resp_json.refresh_token,\r\n expires_in: Number(resp_json.expires_in),\r\n expiry: Date.now() + (resp_json.expires_in - 1) * 1000,\r\n token_type: resp_json.token_type,\r\n market: data.market\r\n };\r\n if (file) writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4));\r\n else {\r\n console.log(`Client ID : ${spotifyData.client_id}`);\r\n console.log(`Client Secret : ${spotifyData.client_secret}`);\r\n console.log(`Refresh Token : ${spotifyData.refresh_token}`);\r\n console.log(`Market : ${spotifyData.market}`);\r\n console.log(`\\nPaste above info in setToken function.`);\r\n }\r\n return true;\r\n}\r\n/**\r\n * Checks if spotify token is expired or not.\r\n *\r\n * Update token if returned false.\r\n * ```ts\r\n * if (play.is_expired()) {\r\n * await play.refreshToken()\r\n * }\r\n * ```\r\n * @returns boolean\r\n */\r\nexport function is_expired(): boolean {\r\n if (Date.now() >= (spotifyData.expiry as number)) return true;\r\n else return false;\r\n}\r\n/**\r\n * type for Spotify Classes\r\n */\r\nexport type Spotify = SpotifyAlbum | SpotifyPlaylist | SpotifyTrack;\r\n/**\r\n * Function for searching songs on Spotify\r\n * @param query searching query\r\n * @param type \"album\" | \"playlist\" | \"track\"\r\n * @param limit max no of results\r\n * @returns Spotify type.\r\n */\r\nexport async function sp_search(\r\n query: string,\r\n type: 'album' | 'playlist' | 'track',\r\n limit: number = 10\r\n): Promise {\r\n const results: Spotify[] = [];\r\n if (!spotifyData) throw new Error('Spotify Data is missing\\nDid you forget to do authorization ?');\r\n if (query.length === 0) throw new Error('Pass some query to search.');\r\n if (limit > 50 || limit < 0) throw new Error(`You crossed limit range of Spotify [ 0 - 50 ]`);\r\n const response = await request(\r\n `https://api.spotify.com/v1/search?type=${type}&q=${query}&limit=${limit}&market=${spotifyData.market}`,\r\n {\r\n headers: {\r\n Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`\r\n }\r\n }\r\n ).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const json_data = JSON.parse(response);\r\n if (type === 'track') {\r\n json_data.tracks.items.forEach((track: any) => {\r\n results.push(new SpotifyTrack(track));\r\n });\r\n } else if (type === 'album') {\r\n json_data.albums.items.forEach((album: any) => {\r\n results.push(new SpotifyAlbum(album, spotifyData, true));\r\n });\r\n } else if (type === 'playlist') {\r\n json_data.playlists.items.forEach((playlist: any) => {\r\n results.push(new SpotifyPlaylist(playlist, spotifyData, true));\r\n });\r\n }\r\n return results;\r\n}\r\n/**\r\n * Refreshes Token\r\n *\r\n * ```ts\r\n * if (play.is_expired()) {\r\n * await play.refreshToken()\r\n * }\r\n * ```\r\n * @returns boolean\r\n */\r\nexport async function refreshToken(): Promise {\r\n const response = await request(`https://accounts.spotify.com/api/token`, {\r\n headers: {\r\n 'Authorization': `Basic ${Buffer.from(`${spotifyData.client_id}:${spotifyData.client_secret}`).toString(\r\n 'base64'\r\n )}`,\r\n 'Content-Type': 'application/x-www-form-urlencoded'\r\n },\r\n body: `grant_type=refresh_token&refresh_token=${spotifyData.refresh_token}`,\r\n method: 'POST'\r\n }).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) return false;\r\n const resp_json = JSON.parse(response);\r\n spotifyData.access_token = resp_json.access_token;\r\n spotifyData.expires_in = Number(resp_json.expires_in);\r\n spotifyData.expiry = Date.now() + (resp_json.expires_in - 1) * 1000;\r\n spotifyData.token_type = resp_json.token_type;\r\n if (spotifyData.file) writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4));\r\n return true;\r\n}\r\n\r\nexport async function setSpotifyToken(options: SpotifyDataOptions) {\r\n spotifyData = options;\r\n spotifyData.file = false;\r\n await refreshToken();\r\n}\r\n\r\nexport { SpotifyTrack, SpotifyAlbum, SpotifyPlaylist };\r\n","import { existsSync, readFileSync } from 'node:fs';\r\nimport { StreamType } from '../YouTube/stream';\r\nimport { request } from '../Request';\r\nimport { SoundCloudPlaylist, SoundCloudTrack, SoundCloudTrackFormat, SoundCloudStream } from './classes';\r\nlet soundData: SoundDataOptions;\r\nif (existsSync('.data/soundcloud.data')) {\r\n soundData = JSON.parse(readFileSync('.data/soundcloud.data', 'utf-8'));\r\n}\r\n\r\ninterface SoundDataOptions {\r\n client_id: string;\r\n}\r\n\r\nconst pattern = /^(?:(https?):\\/\\/)?(?:(?:www|m)\\.)?(api\\.soundcloud\\.com|soundcloud\\.com|snd\\.sc)\\/(.*)$/;\r\n/**\r\n * Gets info from a soundcloud url.\r\n *\r\n * ```ts\r\n * let sound = await play.soundcloud('soundcloud url')\r\n *\r\n * // sound.type === \"track\" | \"playlist\" | \"user\"\r\n *\r\n * if (sound.type === \"track\") {\r\n * spot = spot as play.SoundCloudTrack\r\n * // Code with SoundCloud track class.\r\n * }\r\n * ```\r\n * @param url soundcloud url\r\n * @returns A {@link SoundCloudTrack} or {@link SoundCloudPlaylist}\r\n */\r\nexport async function soundcloud(url: string): Promise {\r\n if (!soundData) throw new Error('SoundCloud Data is missing\\nDid you forget to do authorization ?');\r\n const url_ = url.trim();\r\n if (!url_.match(pattern)) throw new Error('This is not a SoundCloud URL');\r\n\r\n const data = await request(\r\n `https://api-v2.soundcloud.com/resolve?url=${url_}&client_id=${soundData.client_id}`\r\n ).catch((err: Error) => err);\r\n\r\n if (data instanceof Error) throw data;\r\n\r\n const json_data = JSON.parse(data);\r\n\r\n if (json_data.kind !== 'track' && json_data.kind !== 'playlist')\r\n throw new Error('This url is out of scope for play-dl.');\r\n\r\n if (json_data.kind === 'track') return new SoundCloudTrack(json_data);\r\n else return new SoundCloudPlaylist(json_data, soundData.client_id);\r\n}\r\n/**\r\n * Type of SoundCloud\r\n */\r\nexport type SoundCloud = SoundCloudTrack | SoundCloudPlaylist;\r\n/**\r\n * Function for searching in SoundCloud\r\n * @param query query to search\r\n * @param type 'tracks' | 'playlists' | 'albums'\r\n * @param limit max no. of results\r\n * @returns Array of SoundCloud type.\r\n */\r\nexport async function so_search(\r\n query: string,\r\n type: 'tracks' | 'playlists' | 'albums',\r\n limit: number = 10\r\n): Promise {\r\n const response = await request(\r\n `https://api-v2.soundcloud.com/search/${type}?q=${query}&client_id=${soundData.client_id}&limit=${limit}`\r\n );\r\n const results: (SoundCloudPlaylist | SoundCloudTrack)[] = [];\r\n const json_data = JSON.parse(response);\r\n json_data.collection.forEach((x: any) => {\r\n if (type === 'tracks') results.push(new SoundCloudTrack(x));\r\n else results.push(new SoundCloudPlaylist(x, soundData.client_id));\r\n });\r\n return results;\r\n}\r\n/**\r\n * Main Function for creating a Stream of soundcloud\r\n * @param url soundcloud url\r\n * @param quality Quality to select from\r\n * @returns SoundCloud Stream\r\n */\r\nexport async function stream(url: string, quality?: number): Promise {\r\n const data = await soundcloud(url);\r\n\r\n if (data instanceof SoundCloudPlaylist) throw new Error(\"Streams can't be created from playlist urls\");\r\n\r\n const HLSformats = parseHlsFormats(data.formats);\r\n if (typeof quality !== 'number') quality = HLSformats.length - 1;\r\n else if (quality <= 0) quality = 0;\r\n else if (quality >= HLSformats.length) quality = HLSformats.length - 1;\r\n const req_url = HLSformats[quality].url + '?client_id=' + soundData.client_id;\r\n const s_data = JSON.parse(await request(req_url));\r\n const type = HLSformats[quality].format.mime_type.startsWith('audio/ogg')\r\n ? StreamType.OggOpus\r\n : StreamType.Arbitrary;\r\n return new SoundCloudStream(s_data.url, type);\r\n}\r\n/**\r\n * Gets Free SoundCloud Client ID.\r\n *\r\n * Use this in beginning of your code to add SoundCloud support.\r\n *\r\n * ```ts\r\n * play.getFreeClientID().then((clientID) => play.setToken({\r\n * soundcloud : {\r\n * client_id : clientID\r\n * }\r\n * }))\r\n * ```\r\n * @returns client ID\r\n */\r\nexport async function getFreeClientID(): Promise {\r\n const data: any = await request('https://soundcloud.com/', {headers: {}}).catch(err => err);\r\n\r\n if (data instanceof Error)\r\n throw new Error(\"Failed to get response from soundcloud.com: \" + data.message);\r\n\r\n const splitted = data.split('")[0].split(/(?<=}}});\s*(var|const|let)\s/)[0];if(!n)throw new Error("Initial Player Response Data is undefined.");let o=i.split("var ytInitialData = ")?.[1]?.split(";")[0].split(/;\s*(var|const|let)\s/)[0];if(!o)throw new Error("Initial Response Data is undefined.");let l=JSON.parse(n),c=JSON.parse(o),u=l.videoDetails,m=!1,y=!1;if(l.playabilityStatus.status!=="OK")if(l.playabilityStatus.status==="CONTENT_CHECK_REQUIRED"){if(e.htmldata)throw new Error(`Accepting the viewer discretion is not supported when using htmldata, video: ${u.videoId}`);m=!0;let p=c.topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton.buttonRenderer.command.saveConsentAction;p&&Object.assign(s,{VISITOR_INFO1_LIVE:p.visitorCookie,CONSENT:p.consentCookie});let g=await dt(u.videoId,s,i,!0);l.streamingData=g.streamingData,c.contents.twoColumnWatchNextResults.secondaryResults=g.relatedVideos}else if(l.playabilityStatus.status==="LIVE_STREAM_OFFLINE")y=!0;else throw new Error(`While getting info from url +${l.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText??l.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText??l.playabilityStatus.reason}`);let f=c.contents.twoColumnWatchNextResults.results?.results?.contents[1]?.videoSecondaryInfoRenderer?.owner?.videoOwnerRenderer,b=f?.badges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase(),I=`https://www.youtube.com${i.split('"jsUrl":"')[1].split('"')[0]}`,V=[];c.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach(p=>{p.compactVideoRenderer&&V.push(`https://www.youtube.com/watch?v=${p.compactVideoRenderer.videoId}`),p.itemSectionRenderer?.contents&&p.itemSectionRenderer.contents.forEach(g=>{g.compactVideoRenderer&&V.push(`https://www.youtube.com/watch?v=${g.compactVideoRenderer.videoId}`)})});let F=l.microformat.playerMicroformatRenderer,We=c.engagementPanels.find(p=>p?.engagementPanelSectionListRenderer?.panelIdentifier=="engagement-panel-structured-description")?.engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items.find(p=>p.videoDescriptionMusicSectionRenderer)?.videoDescriptionMusicSectionRenderer.carouselLockups,Je=[];We&&We.forEach(p=>{if(!p.carouselLockupRenderer)return;let g=p.carouselLockupRenderer,qt=g.videoLockup?.compactVideoRenderer.title.simpleText??g.videoLockup?.compactVideoRenderer.title.runs?.find(v=>v.text)?.text,Ut=g.infoRows?.map(v=>[v.infoRowRenderer.title.simpleText.toLowerCase(),(v.infoRowRenderer.expandedMetadata??v.infoRowRenderer.defaultMetadata)?.runs?.map(Yt=>Yt.text).join("")??v.infoRowRenderer.defaultMetadata?.simpleText??v.infoRowRenderer.expandedMetadata?.simpleText??""]),Bt=Object.fromEntries(Ut??{}),He=g.videoLockup?.compactVideoRenderer.navigationEndpoint?.watchEndpoint.videoId??g.infoRows?.find(v=>v.infoRowRenderer.title.simpleText.toLowerCase()=="song")?.infoRowRenderer.defaultMetadata.runs?.find(v=>v.navigationEndpoint)?.navigationEndpoint.watchEndpoint?.videoId;Je.push({song:qt,url:He?`https://www.youtube.com/watch?v=${He}`:null,...Bt})});let Ve=c.playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer.playerBar?.multiMarkersPlayerBarRenderer.markersMap?.find(p=>p.key==="DESCRIPTION_CHAPTERS")?.value?.chapters,Fe=[];if(Ve)for(let{chapterRenderer:p}of Ve)Fe.push({title:p.title.simpleText,timestamp:ct(p.timeRangeStartMillis/1e3),seconds:p.timeRangeStartMillis/1e3,thumbnails:p.thumbnail.thumbnails});let oe;if(y)if(F.liveBroadcastDetails.startTimestamp)oe=new Date(F.liveBroadcastDetails.startTimestamp);else{let p=l.playabilityStatus.liveStreamability.liveStreamabilityRenderer.offlineSlate.liveStreamOfflineSlateRenderer.scheduledStartTime;oe=new Date(parseInt(p)*1e3)}let je=c.contents.twoColumnWatchNextResults.results.results.contents.find(p=>p.videoPrimaryInfoRenderer)?.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons?.find(p=>p.toggleButtonRenderer?.defaultIcon.iconType==="LIKE"||p.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultIcon.iconType==="LIKE"),Ke=new E({id:u.videoId,title:u.title,description:u.shortDescription,duration:Number(u.lengthSeconds),duration_raw:ct(u.lengthSeconds),uploadedAt:F.publishDate,liveAt:F.liveBroadcastDetails?.startTimestamp,upcoming:oe,thumbnails:u.thumbnail.thumbnails,channel:{name:u.author,id:u.channelId,url:`https://www.youtube.com/channel/${u.channelId}`,verified:!!b?.includes("verified"),artist:!!b?.includes("artist"),icons:f?.thumbnail?.thumbnails||void 0},views:u.viewCount,tags:u.keywords,likes:parseInt(je?.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\D+/g,"")??je?.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\D+/g,"")??0),live:u.isLiveContent,private:u.isPrivate,discretionAdvised:m,music:Je,chapters:Fe}),Ge=[];return y||(Ge=await pt(u.videoId,s,i)),{LiveStreamData:{isLive:Ke.live,dashManifestUrl:l.streamingData?.dashManifestUrl??null,hlsManifestUrl:l.streamingData?.hlsManifestUrl??null},html5player:I,format:Ge,video_details:Ke,related_videos:V}}a(X,"video_basic_info");async function A(r,e={}){if(typeof r!="string")throw new Error("url parameter is not a URL string or a string of HTML");let t,i={};if(e.htmldata)t=r;else{let f=pe(r);if(!f)throw new Error("This is not a YouTube Watch URL");let b=`https://www.youtube.com/watch?v=${f}&has_verified=1`;t=await h(b,{headers:{"accept-language":"en-US,en;q=0.9"},cookies:!0,cookieJar:i})}if(t.indexOf("Our systems have detected unusual traffic from your computer network.")!==-1)throw new Error("Captcha page: YouTube has detected that you are a bot!");let s=t.split("var ytInitialPlayerResponse = ")?.[1]?.split(";")[0].split(/(?<=}}});\s*(var|const|let)\s/)[0];if(!s)throw new Error("Initial Player Response Data is undefined.");let n=JSON.parse(s),o=!1;if(n.playabilityStatus.status!=="OK")if(n.playabilityStatus.status==="CONTENT_CHECK_REQUIRED"){if(e.htmldata)throw new Error(`Accepting the viewer discretion is not supported when using htmldata, video: ${n.videoDetails.videoId}`);let f=t.split("var ytInitialData = ")?.[1]?.split(";")[0].split(/;\s*(var|const|let)\s/)[0];if(!f)throw new Error("Initial Response Data is undefined.");let b=JSON.parse(f).topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton.buttonRenderer.command.saveConsentAction;b&&Object.assign(i,{VISITOR_INFO1_LIVE:b.visitorCookie,CONSENT:b.consentCookie});let I=await dt(n.videoDetails.videoId,i,t,!1);n.streamingData=I.streamingData}else if(n.playabilityStatus.status==="LIVE_STREAM_OFFLINE")o=!0;else throw new Error(`While getting info from url +${n.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText??n.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText??n.playabilityStatus.reason}`);let l=`https://www.youtube.com${t.split('"jsUrl":"')[1].split('"')[0]}`,c=Number(n.videoDetails.lengthSeconds),u={url:`https://www.youtube.com/watch?v=${n.videoDetails.videoId}`,durationInSec:(c<0?0:c)||0},m=[];o||(m=await pt(n.videoDetails.videoId,i,t));let y={isLive:n.videoDetails.isLiveContent,dashManifestUrl:n.streamingData?.dashManifestUrl??null,hlsManifestUrl:n.streamingData?.hlsManifestUrl??null};return await J({LiveStreamData:y,html5player:l,format:m,video_details:u},!0)}a(A,"video_stream_info");function ct(r){let e=Number(r),t=Math.floor(e/3600),i=Math.floor(e%3600/60),s=Math.floor(e%3600%60),n=t>0?(t<10?`0${t}`:t)+":":"",o=i>0?(i<10?`0${i}`:i)+":":"00:",l=s>0?s<10?`0${s}`:s:"00";return n+o+l}a(ct,"parseSeconds");async function fe(r,e={}){let t=await X(r.trim(),e);return await J(t)}a(fe,"video_info");async function J(r,e=!1){return r.LiveStreamData.isLive===!0&&r.LiveStreamData.dashManifestUrl!==null&&r.video_details.durationInSec===0||r.format.length>0&&(r.format[0].signatureCipher||r.format[0].cipher)&&(e&&(r.format=z(r.format)),r.format=await ut(r.format,r.html5player)),r}a(J,"decipher_info");async function ye(r,e={}){if(!r||typeof r!="string")throw new Error(`Expected playlist url, received ${typeof r}!`);let t=r.trim();if(t.startsWith("https")||(t=`https://www.youtube.com/playlist?list=${t}`),t.indexOf("list=")===-1)throw new Error("This is not a Playlist URL");if(t.includes("music.youtube.com")){let n=new gi(t);n.hostname="www.youtube.com",t=n.toString()}let i=await h(t,{headers:{"accept-language":e.language||"en-US;q=0.9"}});if(i.indexOf("Our systems have detected unusual traffic from your computer network.")!==-1)throw new Error("Captcha page: YouTube has detected that you are a bot!");let s=JSON.parse(i.split("var ytInitialData = ")[1].split(";")[0].split(/;\s*(var|const|let)\s/)[0]);if(s.alerts)if(s.alerts[0].alertWithButtonRenderer?.type==="INFO"){if(!e.incomplete)throw new Error(`While parsing playlist url +${s.alerts[0].alertWithButtonRenderer.text.simpleText}`)}else throw s.alerts[0].alertRenderer?.type==="ERROR"?new Error(`While parsing playlist url +${s.alerts[0].alertRenderer.text.runs[0].text}`):new Error(`While parsing playlist url +Unknown Playlist Error`);return s.currentVideoEndpoint?vi(s,i,t):Si(s,i)}a(ye,"playlist_info");function de(r,e=1/0){let t=[];for(let i=0;iObject.keys(e)[0]==="continuationItemRenderer")?.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token}a(H,"getContinuationToken");async function dt(r,e,t,i){let s=t.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0]??t.split('innertubeApiKey":"')[1]?.split('"')[0]??Z,n=t.split('"XSRF_TOKEN":"')[1]?.split('"')[0].replaceAll("\\u003d","=")??t.split('"xsrf_token":"')[1]?.split('"')[0].replaceAll("\\u003d","=");if(!n)throw new Error(`Unable to extract XSRF_TOKEN to accept the viewer discretion popup for video: ${r}.`);let o=await h(`https://www.youtube.com/youtubei/v1/verify_age?key=${s}&prettyPrint=false`,{method:"POST",body:JSON.stringify({context:{client:{utcOffsetMinutes:0,gl:"US",hl:"en",clientName:"WEB",clientVersion:t.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0]??t.split('"innertube_context_client_version":"')[1]?.split('"')[0]??""},user:{},request:{}},nextEndpoint:{urlEndpoint:{url:`/watch?v=${r}&has_verified=1`}},setControvercy:!0}),cookies:!0,cookieJar:e}),l=JSON.parse(o).actions[0].navigateAction.endpoint,c=await h(`https://www.youtube.com/${l.urlEndpoint.url}&pbj=1`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new wi([["command",JSON.stringify(l)],["session_token",n]]).toString(),cookies:!0,cookieJar:e});if(c.includes("

Something went wrong

"))throw new Error(`Unable to accept the viewer discretion popup for video: ${r}`);let u=JSON.parse(c);if(u[2].playerResponse.playabilityStatus.status!=="OK")throw new Error(`While getting info from url after trying to accept the discretion popup for video ${r} +${u[2].playerResponse.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText??u[2].playerResponse.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText}`);let m=u[2].playerResponse.streamingData;return i?{streamingData:m,relatedVideos:u[3].response.contents.twoColumnWatchNextResults.secondaryResults}:{streamingData:m}}a(dt,"acceptViewerDiscretion");async function pt(r,e,t){let i=t.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0]??t.split('innertubeApiKey":"')[1]?.split('"')[0]??Z,s=await h(`https://www.youtube.com/youtubei/v1/player?key=${i}&prettyPrint=false`,{method:"POST",body:JSON.stringify({context:{client:{clientName:"IOS",clientVersion:"19.09.3",deviceModel:"iPhone16,1",userAgent:"com.google.ios.youtube/19.09.3 (iPhone; CPU iPhone OS 17_5 like Mac OS X)",hl:"en",timeZone:"UTC",utcOffsetMinutes:0}},videoId:r,playbackContext:{contentPlaybackContext:{html5Preference:"HTML5_PREF_WANTS"}},contentCheckOk:!0,racyCheckOk:!0}),cookies:!0,cookieJar:e});return JSON.parse(s).streamingData.adaptiveFormats}a(pt,"getIosFormats");function vi(r,e,t){let i=r.contents.twoColumnWatchNextResults.playlist?.playlist;if(!i)throw new Error("Watch playlist unavailable due to YouTube layout changes.");let s=Ei(i.contents),n=e.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0]??e.split('innertubeApiKey":"')[1]?.split('"')[0]??Z,o=i.totalVideos,l=i.shortBylineText?.runs?.[0],c=i.badges?.[0]?.metadataBadgeRenderer?.style.toLowerCase();return new D({continuation:{api:n,token:H(i.contents),clientVersion:e.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0]??e.split('"innertube_context_client_version":"')[1]?.split('"')[0]??""},id:i.playlistId||"",title:i.title||"",videoCount:parseInt(o)||0,videos:s,url:t,channel:{id:l?.navigationEndpoint?.browseEndpoint?.browseId||null,name:l?.text||null,url:`https://www.youtube.com${l?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl||l?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url}`,verified:!!c?.includes("verified"),artist:!!c?.includes("artist")}})}a(vi,"getWatchPlaylist");function Si(r,e){let t=r.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].playlistVideoListRenderer.contents,i=r.sidebar.playlistSidebarRenderer.items,s=e.split('INNERTUBE_API_KEY":"')[1]?.split('"')[0]??e.split('innertubeApiKey":"')[1]?.split('"')[0]??Z,n=de(t,100),o=i[0].playlistSidebarPrimaryInfoRenderer;if(!o.title.runs||!o.title.runs.length)throw new Error("Failed to Parse Playlist info.");let l=i[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner,c=o.stats.length===3?o.stats[1].simpleText.replace(/\D/g,""):0,u=o.stats.find(f=>"runs"in f&&f.runs.find(b=>b.text.toLowerCase().includes("last update")))?.runs.pop()?.text??null,m=o.stats[0].runs[0].text.replace(/\D/g,"")||0;return new D({continuation:{api:s,token:H(t),clientVersion:e.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]?.split('"')[0]??e.split('"innertube_context_client_version":"')[1]?.split('"')[0]??""},id:o.title.runs[0].navigationEndpoint.watchEndpoint.playlistId,title:o.title.runs[0].text,videoCount:parseInt(m)||0,lastUpdate:u,views:parseInt(c)||0,videos:n,url:`https://www.youtube.com/playlist?list=${o.title.runs[0].navigationEndpoint.watchEndpoint.playlistId}`,link:`https://www.youtube.com${o.title.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,channel:l?{name:l.videoOwnerRenderer.title.runs[0].text,id:l.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId,url:`https://www.youtube.com${l.videoOwnerRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url||l.videoOwnerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl}`,icons:l.videoOwnerRenderer.thumbnail.thumbnails??[]}:{},thumbnail:o.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length?o.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[o.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length-1]:null})}a(Si,"getNormalPlaylist");function Ei(r,e=1/0){let t=[];for(let i=0;i{this.dash_updater(),this.dash_timer.reuse()},1800),this.stream.on("close",()=>{this.cleanup()}),this.initialize_dash()}cleanup(){this.normal_timer?.destroy(),this.dash_timer.destroy(),this.request?.destroy(),this.video_url="",this.request=void 0,this.dash_url="",this.base_url="",this.interval=0}async dash_updater(){let e=await A(this.video_url);return e.LiveStreamData.dashManifestUrl&&(this.dash_url=e.LiveStreamData.dashManifestUrl),this.initialize_dash()}async initialize_dash(){let t=(await h(this.dash_url)).split('")[0].split("");if(t[t.length-1]===""&&t.pop(),this.base_url=t[t.length-1].split("")[1].split("")[0],await w(`https://${new xi(this.base_url).host}/generate_204`),this.sequence===0){let i=t[t.length-1].split("")[1].split("")[0].replaceAll('');i[i.length-1]===""&&i.pop(),i.length>this.precache&&i.splice(0,i.length-this.precache),this.sequence=Number(i[0].split("sq/")[1].split("/")[0]),this.first_data(i.length)}}async first_data(e){for(let t=1;t<=e;t++)await new Promise(async i=>{let s=await w(this.base_url+"sq/"+this.sequence).catch(n=>n);if(s instanceof Error){this.stream.emit("error",s);return}this.request=s,s.on("data",n=>{this.stream.push(n)}),s.on("end",()=>{this.sequence++,i("")}),s.once("error",n=>{this.stream.emit("error",n)})});this.normal_timer=new C(()=>{this.loop(),this.normal_timer?.reuse()},this.interval)}loop(){return new Promise(async e=>{let t=await w(this.base_url+"sq/"+this.sequence).catch(i=>i);if(t instanceof Error){this.stream.emit("error",t);return}this.request=t,t.on("data",i=>{this.stream.push(i)}),t.on("end",()=>{this.sequence++,e("")}),t.once("error",i=>{this.stream.emit("error",i)})})}pause(){}resume(){}};a(be,"LiveStream");var ee=be,ge=class ge{constructor(e,t,i,s,n,o){this.stream=new mt({highWaterMark:5*1e3*1e3,read(){}}),this.url=e,this.quality=o.quality,this.type=t,this.bytes_count=0,this.video_url=n,this.per_sec_bytes=Math.ceil(s/i),this.content_length=s,this.request=null,this.timer=new C(()=>{this.timer.reuse(),this.loop()},265),this.stream.on("close",()=>{this.timer.destroy(),this.cleanup()}),this.loop()}async retry(){let e=await A(this.video_url),t=z(e.format);this.url=t[this.quality].url}cleanup(){this.request?.destroy(),this.request=null,this.url=""}async loop(){if(this.stream.destroyed){this.timer.destroy(),this.cleanup();return}let e=this.bytes_count+this.per_sec_bytes*300,t=await w(this.url,{headers:{range:`bytes=${this.bytes_count}-${e>=this.content_length?"":e}`}}).catch(i=>i);if(t instanceof Error){this.stream.emit("error",t),this.bytes_count=0,this.per_sec_bytes=0,this.cleanup();return}if(Number(t.statusCode)>=400){this.cleanup(),await this.retry(),this.timer.reuse(),this.loop();return}this.request=t,t.on("data",i=>{this.stream.push(i)}),t.once("error",async()=>{this.cleanup(),await this.retry(),this.timer.reuse(),this.loop()}),t.on("data",i=>{this.bytes_count+=i.length}),t.on("end",()=>{e>=this.content_length&&(this.timer.destroy(),this.stream.push(null),this.cleanup())})}pause(){this.timer.pause()}resume(){this.timer.resume()}};a(ge,"Stream");var te=ge,we=class we{constructor(e,t){this.callback=e,this.time_total=t,this.time_left=t,this.paused=!1,this.destroyed=!1,this.time_start=process.hrtime()[0],this.timer=setTimeout(this.callback,this.time_total*1e3)}pause(){return!this.paused&&!this.destroyed?(this.paused=!0,clearTimeout(this.timer),this.time_left=this.time_left-(process.hrtime()[0]-this.time_start),!0):!1}resume(){return this.paused&&!this.destroyed?(this.paused=!1,this.time_start=process.hrtime()[0],this.timer=setTimeout(this.callback,this.time_left*1e3),!0):!1}reuse(){return this.destroyed?!1:(clearTimeout(this.timer),this.time_left=this.time_total,this.paused=!1,this.time_start=process.hrtime()[0],this.timer=setTimeout(this.callback,this.time_total*1e3),!0)}destroy(){clearTimeout(this.timer),this.destroyed=!0,this.callback=()=>{},this.time_total=0,this.time_left=0,this.paused=!1,this.time_start=0}};a(we,"Timer");var C=we;import{WebmElements as ft,WebmHeader as Ii}from"play-audio";import{Duplex as Di}from"node:stream";var Ri=Object.keys(ft),ke=class ke extends Di{constructor(e,t){super(t),this.state="READING_HEAD",this.cursor=0,this.header=new Ii,this.headfound=!1,this.headerparsed=!1,this.seekfound=!1,this.data_length=0,this.data_size=0,this.offset=0,this.sec=e,this.time=Math.floor(e/10)*10}get vint_length(){let e=0;for(;e<8&&!(1<<7-e&this.chunk[this.cursor]);e++);return++e}vint_value(){if(!this.chunk)return!1;let e=this.vint_length;if(this.chunk.lengththis.cursor;){let e=this.cursor,t=this.vint_length;if(this.chunk.length2&&this.time===this.header.segment.cues.at(-2).time/1e3&&this.emit("headComplete"),i.type===0){this.cursor+=this.data_size;continue}if(this.chunk.lengththis.cursor;){let e=this.cursor,t=this.vint_length;if(this.chunk.lengththis.chunk.length)continue;let t=this.chunk.slice(this.cursor+this.data_size,this.cursor+this.data_size+this.data_length),i=this.header.segment.tracks[this.header.audioTrack];if(!i||i.trackType!==2)return new Error("No audio Track in this webm file.");if((t[0]&15)===i.trackNumber)this.cursor+=this.data_size+this.data_length,this.push(t.slice(4)),e=!0;else continue}return e?(this.seekfound=!0,this.readTag()):new Error("Failed to find nearest correct simple Block.")}parseEbmlID(e){return Ri.includes(e)?ft[e]:!1}_destroy(e,t){this.cleanup(),t(e)}_final(e){this.cleanup(),e()}};a(ke,"WebmSeeker");var ie=ke;var ve=class ve{constructor(e,t,i,s,n,o,l){this.stream=new ie(l.seek,{highWaterMark:5*1e3*1e3,readableObjectMode:!0}),this.url=e,this.quality=l.quality,this.type="opus",this.bytes_count=0,this.video_url=o,this.per_sec_bytes=Math.ceil(n?n/8:s/t),this.header_length=i,this.content_length=s,this.request=null,this.timer=new C(()=>{this.timer.reuse(),this.loop()},265),this.stream.on("close",()=>{this.timer.destroy(),this.cleanup()}),this.seek()}async seek(){let e=await new Promise(async(i,s)=>{if(this.stream.headerparsed)i("");else{let n=await w(this.url,{headers:{range:`bytes=0-${this.header_length}`}}).catch(o=>o);if(n instanceof Error){s(n);return}if(Number(n.statusCode)>=400){s(400);return}this.request=n,n.pipe(this.stream,{end:!1}),n.once("end",()=>{this.stream.state="READING_DATA",i("")}),this.stream.once("headComplete",()=>{n.unpipe(this.stream),n.destroy(),this.stream.state="READING_DATA",i("")})}}).catch(i=>i);if(e instanceof Error){this.stream.emit("error",e),this.bytes_count=0,this.per_sec_bytes=0,this.cleanup();return}else if(e===400)return await this.retry(),this.timer.reuse(),this.seek();let t=this.stream.seek(this.content_length);if(t instanceof Error){this.stream.emit("error",t),this.bytes_count=0,this.per_sec_bytes=0,this.cleanup();return}this.stream.seekfound=!1,this.bytes_count=t,this.timer.reuse(),this.loop()}async retry(){let e=await A(this.video_url),t=z(e.format);this.url=t[this.quality].url}cleanup(){this.request?.destroy(),this.request=null,this.url=""}async loop(){if(this.stream.destroyed){this.timer.destroy(),this.cleanup();return}let e=this.bytes_count+this.per_sec_bytes*300,t=await w(this.url,{headers:{range:`bytes=${this.bytes_count}-${e>=this.content_length?"":e}`}}).catch(i=>i);if(t instanceof Error){this.stream.emit("error",t),this.bytes_count=0,this.per_sec_bytes=0,this.cleanup();return}if(Number(t.statusCode)>=400){this.cleanup(),await this.retry(),this.timer.reuse(),this.loop();return}this.request=t,t.pipe(this.stream,{end:!1}),t.once("error",async()=>{this.cleanup(),await this.retry(),this.timer.reuse(),this.loop()}),t.on("data",i=>{this.bytes_count+=i.length}),t.on("end",()=>{e>=this.content_length&&(this.timer.destroy(),this.stream.end(),this.cleanup())})}pause(){this.timer.pause()}resume(){this.timer.resume()}};a(ve,"SeekStream");var re=ve;import{URL as Ci}from"node:url";function z(r){let e=[];return r.forEach(t=>{let i=t.mimeType;i.startsWith("audio")&&(t.codec=i.split('codecs="')[1].split('"')[0],t.container=i.split("audio/")[1].split(";")[0],e.push(t))}),e}a(z,"parseAudioFormats");async function se(r,e={}){let t=await A(r,{htmldata:e.htmldata,language:e.language});return await ne(t,e)}a(se,"stream");async function ne(r,e={}){if(r.format.length===0)throw new Error("Upcoming and premiere videos that are not currently live cannot be streamed.");if(e.quality&&!Number.isInteger(e.quality))throw new Error("Quality must be set to an integer.");let t=[];if(r.LiveStreamData.isLive===!0&&r.LiveStreamData.dashManifestUrl!==null&&r.video_details.durationInSec===0)return new ee(r.LiveStreamData.dashManifestUrl,r.format[r.format.length-1].targetDurationSec,r.video_details.url,e.precache);let i=z(r.format);typeof e.quality!="number"?e.quality=i.length-1:e.quality<=0?e.quality=0:e.quality>=i.length&&(e.quality=i.length-1),i.length!==0?t.push(i[e.quality]):t.push(r.format[r.format.length-1]);let s=t[0].codec==="opus"&&t[0].container==="webm"?"webm/opus":"arbitrary";if(await w(`https://${new Ci(t[0].url).host}/generate_204`),s==="webm/opus")if(e.discordPlayerCompatibility){if(e.seek)throw new Error("Can not seek with discordPlayerCompatibility set to true.")}else{if(e.seek??=0,e.seek>=r.video_details.durationInSec||e.seek<0)throw new Error(`Seeking beyond limit. [ 0 - ${r.video_details.durationInSec-1}]`);return new re(t[0].url,r.video_details.durationInSec,t[0].indexRange.end,Number(t[0].contentLength),Number(t[0].bitrate),r.video_details.url,e)}let n;return t[0].contentLength?n=Number(t[0].contentLength):n=await ae(t[0].url),new te(t[0].url,s,r.video_details.durationInSec,n,r.video_details.url,e)}a(ne,"stream_from_info");var Oi=["-oaymwEpCOADEI4CSFryq4qpAxsIARUAAAAAGAElAADIQj0AgKJDeAHtAZmZGUI=","-oaymwEiCOADEI4CSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BmZkZQg==","-oaymwEiCOgCEMoBSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmbmQQ==","-oaymwEiCNAFEJQDSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmZmQg==","-oaymwEdCNAFEJQDSFryq4qpAw8IARUAAIhCGAHtAWZmZkI=","-oaymwEdCNACELwBSFryq4qpAw8IARUAAIhCGAHtAT0K10E="];function bt(r,e){if(!r)throw new Error("Can't parse Search result without data");e?e.type||(e.type="video"):e={type:"video",limit:0};let t=typeof e.limit=="number"&&e.limit>0;e.unblurNSFWThumbnails??=!1;let i=r.split("var ytInitialData = ")?.[1]?.split(";")[0].split(/;\s*(var|const|let)\s/)[0],s=JSON.parse(i),n=[],o=s.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.flatMap(l=>l.itemSectionRenderer?.contents);for(let l of o){if(t&&n.length===e.limit)break;if(!(!l||!l.videoRenderer&&!l.channelRenderer&&!l.playlistRenderer))switch(e.type){case"video":{let c=Ni(l);c&&(e.unblurNSFWThumbnails&&c.thumbnails.forEach(yt),n.push(c));break}case"channel":{let c=Pi(l);c&&n.push(c);break}case"playlist":{let c=Ai(l);c&&(e.unblurNSFWThumbnails&&c.thumbnail&&yt(c.thumbnail),n.push(c));break}default:throw new Error(`Unknown search type: ${e.type}`)}}return n}a(bt,"ParseSearchResult");function $i(r){if(!r)return 0;let e=r.split(":"),t=0;switch(e.length){case 3:t=parseInt(e[0])*60*60+parseInt(e[1])*60+parseInt(e[2]);break;case 2:t=parseInt(e[0])*60+parseInt(e[1]);break;default:t=parseInt(e[0])}return t}a($i,"parseDuration");function Pi(r){if(!r||!r.channelRenderer)throw new Error("Failed to Parse YouTube Channel");let e=r.channelRenderer.ownerBadges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase(),t=`https://www.youtube.com${r.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl||r.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url}`,i=r.channelRenderer.thumbnail.thumbnails[r.channelRenderer.thumbnail.thumbnails.length-1];return new S({id:r.channelRenderer.channelId,name:r.channelRenderer.title.simpleText,icon:{url:i.url.replace("//","https://"),width:i.width,height:i.height},url:t,verified:!!e?.includes("verified"),artist:!!e?.includes("artist"),subscribers:r.channelRenderer.subscriberCountText?.simpleText??"0 subscribers"})}a(Pi,"parseChannel");function Ni(r){if(!r||!r.videoRenderer)throw new Error("Failed to Parse YouTube Video");let e=r.videoRenderer.ownerText.runs[0],t=r.videoRenderer.ownerBadges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase(),i=r.videoRenderer.lengthText;return new E({id:r.videoRenderer.videoId,url:`https://www.youtube.com/watch?v=${r.videoRenderer.videoId}`,title:r.videoRenderer.title.runs[0].text,description:r.videoRenderer.detailedMetadataSnippets?.[0].snippetText.runs?.length?r.videoRenderer.detailedMetadataSnippets[0].snippetText.runs.map(n=>n.text).join(""):"",duration:i?$i(i.simpleText):0,duration_raw:i?i.simpleText:null,thumbnails:r.videoRenderer.thumbnail.thumbnails,channel:{id:e.navigationEndpoint.browseEndpoint.browseId||null,name:e.text||null,url:`https://www.youtube.com${e.navigationEndpoint.browseEndpoint.canonicalBaseUrl||e.navigationEndpoint.commandMetadata.webCommandMetadata.url}`,icons:r.videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails,verified:!!t?.includes("verified"),artist:!!t?.includes("artist")},uploadedAt:r.videoRenderer.publishedTimeText?.simpleText??null,upcoming:r.videoRenderer.upcomingEventData?.startTime?new Date(parseInt(r.videoRenderer.upcomingEventData.startTime)*1e3):void 0,views:r.videoRenderer.viewCountText?.simpleText?.replace(/\D/g,"")??0,live:!i})}a(Ni,"parseVideo");function Ai(r){if(!r||!r.playlistRenderer)throw new Error("Failed to Parse YouTube Playlist");let e=r.playlistRenderer.thumbnails[0].thumbnails[r.playlistRenderer.thumbnails[0].thumbnails.length-1],t=r.playlistRenderer.shortBylineText.runs?.[0];return new D({id:r.playlistRenderer.playlistId,title:r.playlistRenderer.title.simpleText,thumbnail:{id:r.playlistRenderer.playlistId,url:e.url,height:e.height,width:e.width},channel:{id:t?.navigationEndpoint.browseEndpoint.browseId,name:t?.text,url:`https://www.youtube.com${t?.navigationEndpoint.commandMetadata.webCommandMetadata.url}`},videos:parseInt(r.playlistRenderer.videoCount.replace(/\D/g,""))},!0)}a(Ai,"parsePlaylist");function yt(r){if(Oi.find(e=>r.url.includes(e)))switch(r.url=r.url.split("?")[0],r.url.split("/").at(-1).split(".")[0]){case"hq2":case"hqdefault":r.width=480,r.height=360;break;case"hq720":r.width=1280,r.height=720;break;case"sddefault":r.width=640,r.height=480;break;case"mqdefault":r.width=320,r.height=180;break;case"default":r.width=120,r.height=90;break;default:r.width=r.height=NaN}}a(yt,"unblurThumbnail");async function gt(r,e={}){let t="https://www.youtube.com/results?search_query="+r;if(e.type??="video",t.indexOf("&sp=")===-1)switch(t+="&sp=",e.type){case"channel":t+="EgIQAg%253D%253D";break;case"playlist":t+="EgIQAw%253D%253D";break;case"video":t+="EgIQAQ%253D%253D";break;default:throw new Error(`Unknown search type: ${e.type}`)}let i=await h(t,{headers:{"accept-language":e.language||"en-US;q=0.9"}});if(i.indexOf("Our systems have detected unusual traffic from your computer network.")!==-1)throw new Error("Captcha page: YouTube has detected that you are a bot!");return bt(i,e)}a(gt,"yt_search");var Se=class Se{constructor(e){this.name=e.name,this.id=e.id,this.isrc=e.external_ids?.isrc||"",this.type="track",this.url=e.external_urls.spotify,this.explicit=e.explicit,this.playable=e.is_playable,this.durationInMs=e.duration_ms,this.durationInSec=Math.round(this.durationInMs/1e3);let t=[];e.artists.forEach(i=>{t.push({name:i.name,id:i.id,url:i.external_urls.spotify})}),this.artists=t,e.album?.name?this.album={name:e.album.name,url:e.external_urls.spotify,id:e.album.id,release_date:e.album.release_date,release_date_precision:e.album.release_date_precision,total_tracks:e.album.total_tracks}:this.album=void 0,e.album?.images?.[0]?this.thumbnail=e.album.images[0]:this.thumbnail=void 0}toJSON(){return{name:this.name,id:this.id,url:this.url,explicit:this.explicit,durationInMs:this.durationInMs,durationInSec:this.durationInSec,artists:this.artists,album:this.album,thumbnail:this.thumbnail}}};a(Se,"SpotifyTrack");var T=Se,Ee=class Ee{constructor(e,t,i){this.name=e.name,this.type="playlist",this.search=i,this.collaborative=e.collaborative,this.description=e.description,this.url=e.external_urls.spotify,this.id=e.id,this.thumbnail=e.images[0],this.owner={name:e.owner.display_name,url:e.owner.external_urls.spotify,id:e.owner.id},this.tracksCount=Number(e.tracks.total);let s=[];this.search||e.tracks.items.forEach(n=>{n.track&&s.push(new T(n.track))}),this.fetched_tracks=new Map,this.fetched_tracks.set("1",s),this.spotifyData=t}async fetch(){if(this.search)return this;let e;if(this.tracksCount>1e3?e=1e3:e=this.tracksCount,e<=100)return this;let t=[];for(let i=2;i<=Math.ceil(e/100);i++)t.push(new Promise(async(s,n)=>{let o=await h(`https://api.spotify.com/v1/playlists/${this.id}/tracks?offset=${(i-1)*100}&limit=100&market=${this.spotifyData.market}`,{headers:{Authorization:`${this.spotifyData.token_type} ${this.spotifyData.access_token}`}}).catch(u=>n(`Response Error : +${u}`)),l=[];if(typeof o!="string")return;JSON.parse(o).items.forEach(u=>{u.track&&l.push(new T(u.track))}),this.fetched_tracks.set(`${i}`,l),s("Success")}));return await Promise.allSettled(t),this}page(e){if(!e)throw new Error("Page number is not provided");if(!this.fetched_tracks.has(`${e}`))throw new Error("Given Page number is invalid");return this.fetched_tracks.get(`${e}`)}get total_pages(){return this.fetched_tracks.size}get total_tracks(){if(this.search)return this.tracksCount;let e=this.total_pages;return(e-1)*100+this.fetched_tracks.get(`${e}`).length}async all_tracks(){await this.fetch();let e=[];for(let t of this.fetched_tracks.values())e.push(...t);return e}toJSON(){return{name:this.name,collaborative:this.collaborative,description:this.description,url:this.url,id:this.id,thumbnail:this.thumbnail,owner:this.owner,tracksCount:this.tracksCount}}};a(Ee,"SpotifyPlaylist");var L=Ee,Te=class Te{constructor(e,t,i){this.name=e.name,this.type="album",this.id=e.id,this.search=i,this.url=e.external_urls.spotify,this.thumbnail=e.images[0];let s=[];e.artists.forEach(o=>{s.push({name:o.name,id:o.id,url:o.external_urls.spotify})}),this.artists=s,this.copyrights=e.copyrights,this.release_date=e.release_date,this.release_date_precision=e.release_date_precision,this.tracksCount=e.total_tracks;let n=[];this.search||e.tracks.items.forEach(o=>{n.push(new T(o))}),this.fetched_tracks=new Map,this.fetched_tracks.set("1",n),this.spotifyData=t}async fetch(){if(this.search)return this;let e;if(this.tracksCount>500?e=500:e=this.tracksCount,e<=50)return this;let t=[];for(let i=2;i<=Math.ceil(e/50);i++)t.push(new Promise(async(s,n)=>{let o=await h(`https://api.spotify.com/v1/albums/${this.id}/tracks?offset=${(i-1)*50}&limit=50&market=${this.spotifyData.market}`,{headers:{Authorization:`${this.spotifyData.token_type} ${this.spotifyData.access_token}`}}).catch(u=>n(`Response Error : +${u}`)),l=[];if(typeof o!="string")return;JSON.parse(o).items.forEach(u=>{u&&l.push(new T(u))}),this.fetched_tracks.set(`${i}`,l),s("Success")}));return await Promise.allSettled(t),this}page(e){if(!e)throw new Error("Page number is not provided");if(!this.fetched_tracks.has(`${e}`))throw new Error("Given Page number is invalid");return this.fetched_tracks.get(`${e}`)}get total_pages(){return this.fetched_tracks.size}get total_tracks(){if(this.search)return this.tracksCount;let e=this.total_pages;return(e-1)*100+this.fetched_tracks.get(`${e}`).length}async all_tracks(){await this.fetch();let e=[];for(let t of this.fetched_tracks.values())e.push(...t);return e}toJSON(){return{name:this.name,id:this.id,type:this.type,url:this.url,thumbnail:this.thumbnail,artists:this.artists,copyrights:this.copyrights,release_date:this.release_date,release_date_precision:this.release_date_precision,tracksCount:this.tracksCount}}};a(Te,"SpotifyAlbum");var M=Te;import{existsSync as zi,readFileSync as Li,writeFileSync as wt}from"node:fs";var d;zi(".data/spotify.data")&&(d=JSON.parse(Li(".data/spotify.data","utf-8")),d.file=!0);var _t=/^((https:)?\/\/)?open\.spotify\.com\/(?:intl\-.{2}\/)?(track|album|playlist)\//;async function kt(r){if(!d)throw new Error(`Spotify Data is missing +Did you forgot to do authorization ?`);let e=r.trim();if(!e.match(_t))throw new Error("This is not a Spotify URL");if(e.indexOf("track/")!==-1){let t=e.split("track/")[1].split("&")[0].split("?")[0],i=await h(`https://api.spotify.com/v1/tracks/${t}?market=${d.market}`,{headers:{Authorization:`${d.token_type} ${d.access_token}`}}).catch(n=>n);if(i instanceof Error)throw i;let s=JSON.parse(i);if(s.error)throw new Error(`Got ${s.error.status} from the spotify request: ${s.error.message}`);return new T(s)}else if(e.indexOf("album/")!==-1){let t=r.split("album/")[1].split("&")[0].split("?")[0],i=await h(`https://api.spotify.com/v1/albums/${t}?market=${d.market}`,{headers:{Authorization:`${d.token_type} ${d.access_token}`}}).catch(n=>n);if(i instanceof Error)throw i;let s=JSON.parse(i);if(s.error)throw new Error(`Got ${s.error.status} from the spotify request: ${s.error.message}`);return new M(s,d,!1)}else if(e.indexOf("playlist/")!==-1){let t=r.split("playlist/")[1].split("&")[0].split("?")[0],i=await h(`https://api.spotify.com/v1/playlists/${t}?market=${d.market}`,{headers:{Authorization:`${d.token_type} ${d.access_token}`}}).catch(n=>n);if(i instanceof Error)throw i;let s=JSON.parse(i);if(s.error)throw new Error(`Got ${s.error.status} from the spotify request: ${s.error.message}`);return new L(s,d,!1)}else throw new Error("URL is out of scope for play-dl.")}a(kt,"spotify");function xe(r){let e=r.trim();return e.startsWith("https")?e.match(_t)?e.indexOf("track/")!==-1?"track":e.indexOf("album/")!==-1?"album":e.indexOf("playlist/")!==-1?"playlist":!1:!1:"search"}a(xe,"sp_validate");async function vt(r,e){let t=await h("https://accounts.spotify.com/api/token",{headers:{Authorization:`Basic ${Buffer.from(`${r.client_id}:${r.client_secret}`).toString("base64")}`,"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=authorization_code&code=${r.authorization_code}&redirect_uri=${encodeURI(r.redirect_url)}`,method:"POST"}).catch(s=>s);if(t instanceof Error)throw t;let i=JSON.parse(t);return d={client_id:r.client_id,client_secret:r.client_secret,redirect_url:r.redirect_url,access_token:i.access_token,refresh_token:i.refresh_token,expires_in:Number(i.expires_in),expiry:Date.now()+(i.expires_in-1)*1e3,token_type:i.token_type,market:r.market},e?wt(".data/spotify.data",JSON.stringify(d,void 0,4)):(console.log(`Client ID : ${d.client_id}`),console.log(`Client Secret : ${d.client_secret}`),console.log(`Refresh Token : ${d.refresh_token}`),console.log(`Market : ${d.market}`),console.log(` +Paste above info in setToken function.`)),!0}a(vt,"SpotifyAuthorize");function St(){return Date.now()>=d.expiry}a(St,"is_expired");async function Et(r,e,t=10){let i=[];if(!d)throw new Error(`Spotify Data is missing +Did you forget to do authorization ?`);if(r.length===0)throw new Error("Pass some query to search.");if(t>50||t<0)throw new Error("You crossed limit range of Spotify [ 0 - 50 ]");let s=await h(`https://api.spotify.com/v1/search?type=${e}&q=${r}&limit=${t}&market=${d.market}`,{headers:{Authorization:`${d.token_type} ${d.access_token}`}}).catch(o=>o);if(s instanceof Error)throw s;let n=JSON.parse(s);return e==="track"?n.tracks.items.forEach(o=>{i.push(new T(o))}):e==="album"?n.albums.items.forEach(o=>{i.push(new M(o,d,!0))}):e==="playlist"&&n.playlists.items.forEach(o=>{i.push(new L(o,d,!0))}),i}a(Et,"sp_search");async function Ie(){let r=await h("https://accounts.spotify.com/api/token",{headers:{Authorization:`Basic ${Buffer.from(`${d.client_id}:${d.client_secret}`).toString("base64")}`,"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=refresh_token&refresh_token=${d.refresh_token}`,method:"POST"}).catch(t=>t);if(r instanceof Error)return!1;let e=JSON.parse(r);return d.access_token=e.access_token,d.expires_in=Number(e.expires_in),d.expiry=Date.now()+(e.expires_in-1)*1e3,d.token_type=e.token_type,d.file&&wt(".data/spotify.data",JSON.stringify(d,void 0,4)),!0}a(Ie,"refreshToken");async function Tt(r){d=r,d.file=!1,await Ie()}a(Tt,"setSpotifyToken");import{existsSync as qi,readFileSync as Ui}from"node:fs";import{Readable as Mi}from"node:stream";var De=class De{constructor(e){this.name=e.title,this.id=e.id,this.url=e.uri,this.permalink=e.permalink_url,this.fetched=!0,this.type="track",this.durationInSec=Math.round(Number(e.duration)/1e3),this.durationInMs=Number(e.duration),e.publisher_metadata?this.publisher={name:e.publisher_metadata.publisher,id:e.publisher_metadata.id,artist:e.publisher_metadata.artist,contains_music:!!e.publisher_metadata.contains_music||!1,writer_composer:e.publisher_metadata.writer_composer}:this.publisher=null,this.formats=e.media.transcodings,this.user={name:e.user.username,id:e.user.id,type:"user",url:e.user.permalink_url,verified:!!e.user.verified||!1,description:e.user.description,first_name:e.user.first_name,full_name:e.user.full_name,last_name:e.user.last_name,thumbnail:e.user.avatar_url},this.thumbnail=e.artwork_url}toJSON(){return{name:this.name,id:this.id,url:this.url,permalink:this.permalink,fetched:this.fetched,durationInMs:this.durationInMs,durationInSec:this.durationInSec,publisher:this.publisher,formats:this.formats,thumbnail:this.thumbnail,user:this.user}}};a(De,"SoundCloudTrack");var x=De,Re=class Re{constructor(e,t){this.name=e.title,this.id=e.id,this.url=e.uri,this.client_id=t,this.type="playlist",this.sub_type=e.set_type,this.durationInSec=Math.round(Number(e.duration)/1e3),this.durationInMs=Number(e.duration),this.user={name:e.user.username,id:e.user.id,type:"user",url:e.user.permalink_url,verified:!!e.user.verified||!1,description:e.user.description,first_name:e.user.first_name,full_name:e.user.full_name,last_name:e.user.last_name,thumbnail:e.user.avatar_url},this.tracksCount=e.track_count;let i=[];e.tracks.forEach(s=>{s.title?i.push(new x(s)):i.push({id:s.id,fetched:!1,type:"track"})}),this.tracks=i}async fetch(){let e=[];for(let t=0;t{let s=t,n=await h(`https://api-v2.soundcloud.com/tracks/${this.tracks[t].id}?client_id=${this.client_id}`);this.tracks[s]=new x(JSON.parse(n)),i("")}));return await Promise.allSettled(e),this}get total_tracks(){let e=0;return this.tracks.forEach(t=>{if(t instanceof x)e++;else return}),e}async all_tracks(){return await this.fetch(),this.tracks}toJSON(){return{name:this.name,id:this.id,sub_type:this.sub_type,url:this.url,durationInMs:this.durationInMs,durationInSec:this.durationInSec,tracksCount:this.tracksCount,user:this.user,tracks:this.tracks}}};a(Re,"SoundCloudPlaylist");var $=Re,Ce=class Ce{constructor(e,t="arbitrary"){this.stream=new Mi({highWaterMark:5*1e3*1e3,read(){}}),this.type=t,this.url=e,this.downloaded_time=0,this.request=null,this.downloaded_segments=0,this.time=[],this.timer=new C(()=>{this.timer.reuse(),this.start()},280),this.segment_urls=[],this.stream.on("close",()=>{this.cleanup()}),this.start()}async parser(){let e=await h(this.url).catch(i=>i);if(e instanceof Error)throw e;e.split(` +`).forEach(i=>{i.startsWith("#EXTINF:")?this.time.push(parseFloat(i.replace("#EXTINF:",""))):i.startsWith("https")&&this.segment_urls.push(i)})}async start(){if(this.stream.destroyed){this.cleanup();return}this.time=[],this.segment_urls=[],this.downloaded_time=0,await this.parser(),this.segment_urls.splice(0,this.downloaded_segments),this.loop()}async loop(){if(this.stream.destroyed){this.cleanup();return}if(this.time.length===0||this.segment_urls.length===0){this.cleanup(),this.stream.push(null);return}this.downloaded_time+=this.time.shift(),this.downloaded_segments++;let e=await w(this.segment_urls.shift()).catch(t=>t);if(e instanceof Error){this.stream.emit("error",e),this.cleanup();return}this.request=e,e.on("data",t=>{this.stream.push(t)}),e.on("end",()=>{this.downloaded_time>=300||this.loop()}),e.once("error",t=>{this.stream.emit("error",t)})}cleanup(){this.timer.destroy(),this.request?.destroy(),this.url="",this.downloaded_time=0,this.downloaded_segments=0,this.request=null,this.time=[],this.segment_urls=[]}pause(){this.timer.pause()}resume(){this.timer.resume()}};a(Ce,"SoundCloudStream");var q=Ce;var R;qi(".data/soundcloud.data")&&(R=JSON.parse(Ui(".data/soundcloud.data","utf-8")));var xt=/^(?:(https?):\/\/)?(?:(?:www|m)\.)?(api\.soundcloud\.com|soundcloud\.com|snd\.sc)\/(.*)$/;async function Oe(r){if(!R)throw new Error(`SoundCloud Data is missing +Did you forget to do authorization ?`);let e=r.trim();if(!e.match(xt))throw new Error("This is not a SoundCloud URL");let t=await h(`https://api-v2.soundcloud.com/resolve?url=${e}&client_id=${R.client_id}`).catch(s=>s);if(t instanceof Error)throw t;let i=JSON.parse(t);if(i.kind!=="track"&&i.kind!=="playlist")throw new Error("This url is out of scope for play-dl.");return i.kind==="track"?new x(i):new $(i,R.client_id)}a(Oe,"soundcloud");async function It(r,e,t=10){let i=await h(`https://api-v2.soundcloud.com/search/${e}?q=${r}&client_id=${R.client_id}&limit=${t}`),s=[];return JSON.parse(i).collection.forEach(o=>{e==="tracks"?s.push(new x(o)):s.push(new $(o,R.client_id))}),s}a(It,"so_search");async function Dt(r,e){let t=await Oe(r);if(t instanceof $)throw new Error("Streams can't be created from playlist urls");let i=$t(t.formats);typeof e!="number"?e=i.length-1:e<=0?e=0:e>=i.length&&(e=i.length-1);let s=i[e].url+"?client_id="+R.client_id,n=JSON.parse(await h(s)),o=i[e].format.mime_type.startsWith("audio/ogg")?"ogg/opus":"arbitrary";return new q(n.url,o)}a(Dt,"stream");async function Rt(){let r=await h("https://soundcloud.com/",{headers:{}}).catch(s=>s);if(r instanceof Error)throw new Error("Failed to get response from soundcloud.com: "+r.message);let e=r.split('')[0]\r\n .split(/(?<=}}});\\s*(var|const|let)\\s/)[0];\r\n if (!player_data) throw new Error('Initial Player Response Data is undefined.');\r\n const initial_data = body\r\n .split('var ytInitialData = ')?.[1]\r\n ?.split(';')[0]\r\n .split(/;\\s*(var|const|let)\\s/)[0];\r\n if (!initial_data) throw new Error('Initial Response Data is undefined.');\r\n const player_response = JSON.parse(player_data);\r\n const initial_response = JSON.parse(initial_data);\r\n const vid = player_response.videoDetails;\r\n\r\n let discretionAdvised = false;\r\n let upcoming = false;\r\n if (player_response.playabilityStatus.status !== 'OK') {\r\n if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') {\r\n if (options.htmldata)\r\n throw new Error(\r\n `Accepting the viewer discretion is not supported when using htmldata, video: ${vid.videoId}`\r\n );\r\n discretionAdvised = true;\r\n const cookies =\r\n initial_response.topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton\r\n .buttonRenderer.command.saveConsentAction;\r\n if (cookies) {\r\n Object.assign(cookieJar, {\r\n VISITOR_INFO1_LIVE: cookies.visitorCookie,\r\n CONSENT: cookies.consentCookie\r\n });\r\n }\r\n\r\n const updatedValues = await acceptViewerDiscretion(vid.videoId, cookieJar, body, true);\r\n player_response.streamingData = updatedValues.streamingData;\r\n initial_response.contents.twoColumnWatchNextResults.secondaryResults = updatedValues.relatedVideos;\r\n } else if (player_response.playabilityStatus.status === 'LIVE_STREAM_OFFLINE') upcoming = true;\r\n else\r\n throw new Error(\r\n `While getting info from url\\n${\r\n player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ??\r\n player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText ??\r\n player_response.playabilityStatus.reason\r\n }`\r\n );\r\n }\r\n const ownerInfo =\r\n initial_response.contents.twoColumnWatchNextResults.results?.results?.contents[1]?.videoSecondaryInfoRenderer\r\n ?.owner?.videoOwnerRenderer;\r\n const badge = ownerInfo?.badges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase();\r\n const html5player = `https://www.youtube.com${body.split('\"jsUrl\":\"')[1].split('\"')[0]}`;\r\n const related: string[] = [];\r\n initial_response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach(\r\n (res: any) => {\r\n if (res.compactVideoRenderer)\r\n related.push(`https://www.youtube.com/watch?v=${res.compactVideoRenderer.videoId}`);\r\n if (res.itemSectionRenderer?.contents)\r\n res.itemSectionRenderer.contents.forEach((x: any) => {\r\n if (x.compactVideoRenderer)\r\n related.push(`https://www.youtube.com/watch?v=${x.compactVideoRenderer.videoId}`);\r\n });\r\n }\r\n );\r\n const microformat = player_response.microformat.playerMicroformatRenderer;\r\n const musicInfo = initial_response.engagementPanels.find((item: any) => item?.engagementPanelSectionListRenderer?.panelIdentifier == 'engagement-panel-structured-description')?.engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items\r\n .find((el: any) => el.videoDescriptionMusicSectionRenderer)?.videoDescriptionMusicSectionRenderer.carouselLockups;\r\n\r\n const music: any[] = [];\r\n if (musicInfo) {\r\n musicInfo.forEach((x: any) => {\r\n if (!x.carouselLockupRenderer) return;\r\n const row = x.carouselLockupRenderer;\r\n\r\n const song = row.videoLockup?.compactVideoRenderer.title.simpleText ?? row.videoLockup?.compactVideoRenderer.title.runs?.find((x:any) => x.text)?.text;\r\n const metadata = row.infoRows?.map((info: any) => [info.infoRowRenderer.title.simpleText.toLowerCase(), ((info.infoRowRenderer.expandedMetadata ?? info.infoRowRenderer.defaultMetadata)?.runs?.map((i:any) => i.text).join(\"\")) ?? info.infoRowRenderer.defaultMetadata?.simpleText ?? info.infoRowRenderer.expandedMetadata?.simpleText ?? \"\"]);\r\n const contents = Object.fromEntries(metadata ?? {});\r\n const id = row.videoLockup?.compactVideoRenderer.navigationEndpoint?.watchEndpoint.videoId\r\n ?? row.infoRows?.find((x: any) => x.infoRowRenderer.title.simpleText.toLowerCase() == \"song\")?.infoRowRenderer.defaultMetadata.runs?.find((x: any) => x.navigationEndpoint)?.navigationEndpoint.watchEndpoint?.videoId;\r\n\r\n music.push({song, url: id ? `https://www.youtube.com/watch?v=${id}` : null, ...contents})\r\n });\r\n }\r\n const rawChapters =\r\n initial_response.playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer.playerBar?.multiMarkersPlayerBarRenderer.markersMap?.find(\r\n (m: any) => m.key === 'DESCRIPTION_CHAPTERS'\r\n )?.value?.chapters;\r\n const chapters: VideoChapter[] = [];\r\n if (rawChapters) {\r\n for (const { chapterRenderer } of rawChapters) {\r\n chapters.push({\r\n title: chapterRenderer.title.simpleText,\r\n timestamp: parseSeconds(chapterRenderer.timeRangeStartMillis / 1000),\r\n seconds: chapterRenderer.timeRangeStartMillis / 1000,\r\n thumbnails: chapterRenderer.thumbnail.thumbnails\r\n });\r\n }\r\n }\r\n let upcomingDate;\r\n if (upcoming) {\r\n if (microformat.liveBroadcastDetails.startTimestamp)\r\n upcomingDate = new Date(microformat.liveBroadcastDetails.startTimestamp);\r\n else {\r\n const timestamp =\r\n player_response.playabilityStatus.liveStreamability.liveStreamabilityRenderer.offlineSlate\r\n .liveStreamOfflineSlateRenderer.scheduledStartTime;\r\n upcomingDate = new Date(parseInt(timestamp) * 1000);\r\n }\r\n }\r\n\r\n const likeRenderer = initial_response.contents.twoColumnWatchNextResults.results.results.contents\r\n .find((content: any) => content.videoPrimaryInfoRenderer)\r\n ?.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons?.find(\r\n (button: any) => button.toggleButtonRenderer?.defaultIcon.iconType === 'LIKE' || button.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultIcon.iconType === 'LIKE'\r\n )\r\n\r\n const video_details = new YouTubeVideo({\r\n id: vid.videoId,\r\n title: vid.title,\r\n description: vid.shortDescription,\r\n duration: Number(vid.lengthSeconds),\r\n duration_raw: parseSeconds(vid.lengthSeconds),\r\n uploadedAt: microformat.publishDate,\r\n liveAt: microformat.liveBroadcastDetails?.startTimestamp,\r\n upcoming: upcomingDate,\r\n thumbnails: vid.thumbnail.thumbnails,\r\n channel: {\r\n name: vid.author,\r\n id: vid.channelId,\r\n url: `https://www.youtube.com/channel/${vid.channelId}`,\r\n verified: Boolean(badge?.includes('verified')),\r\n artist: Boolean(badge?.includes('artist')),\r\n icons: ownerInfo?.thumbnail?.thumbnails || undefined\r\n },\r\n views: vid.viewCount,\r\n tags: vid.keywords,\r\n likes: parseInt(\r\n likeRenderer?.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\\D+/g, '') ?? \r\n likeRenderer?.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\\D+/g, '') ?? 0\r\n ),\r\n live: vid.isLiveContent,\r\n private: vid.isPrivate,\r\n discretionAdvised,\r\n music,\r\n chapters\r\n });\r\n let format = [];\r\n if (!upcoming) {\r\n // TODO: Properly handle the formats, for now ignore and use iOS formats\r\n //format.push(...(player_response.streamingData.formats ?? []));\r\n //format.push(...(player_response.streamingData.adaptiveFormats ?? []));\r\n\r\n // get the formats for the android player for legacy videos\r\n // fixes the stream being closed because not enough data\r\n // arrived in time for ffmpeg to be able to extract audio data\r\n //if (parseAudioFormats(format).length === 0 && !options.htmldata) {\r\n // format = await getAndroidFormats(vid.videoId, cookieJar, body);\r\n //}\r\n format = await getIosFormats(vid.videoId, cookieJar, body);\r\n }\r\n const LiveStreamData = {\r\n isLive: video_details.live,\r\n dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null,\r\n hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null\r\n };\r\n return {\r\n LiveStreamData,\r\n html5player,\r\n format,\r\n video_details,\r\n related_videos: related\r\n };\r\n}\r\n/**\r\n * Gets the data required for streaming from YouTube url, ID or html body data and deciphers it.\r\n *\r\n * Internal function used by {@link stream} instead of {@link video_info}\r\n * because it only extracts the information required for streaming.\r\n *\r\n * @param url YouTube url or ID or html body data\r\n * @param options Video Info Options\r\n * - `boolean` htmldata : given data is html data or not\r\n * @returns Deciphered Video Info {@link StreamInfoData}.\r\n */\r\nexport async function video_stream_info(url: string, options: InfoOptions = {}): Promise {\r\n if (typeof url !== 'string') throw new Error('url parameter is not a URL string or a string of HTML');\r\n let body: string;\r\n const cookieJar = {};\r\n if (options.htmldata) {\r\n body = url;\r\n } else {\r\n const video_id = extractVideoId(url);\r\n if (!video_id) throw new Error('This is not a YouTube Watch URL');\r\n const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`;\r\n body = await request(new_url, {\r\n headers: { 'accept-language': 'en-US,en;q=0.9' },\r\n cookies: true,\r\n cookieJar\r\n });\r\n }\r\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\r\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\r\n const player_data = body\r\n .split('var ytInitialPlayerResponse = ')?.[1]\r\n ?.split(';')[0]\r\n .split(/(?<=}}});\\s*(var|const|let)\\s/)[0];\r\n if (!player_data) throw new Error('Initial Player Response Data is undefined.');\r\n const player_response = JSON.parse(player_data);\r\n let upcoming = false;\r\n if (player_response.playabilityStatus.status !== 'OK') {\r\n if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') {\r\n if (options.htmldata)\r\n throw new Error(\r\n `Accepting the viewer discretion is not supported when using htmldata, video: ${player_response.videoDetails.videoId}`\r\n );\r\n\r\n const initial_data = body\r\n .split('var ytInitialData = ')?.[1]\r\n ?.split(';')[0]\r\n .split(/;\\s*(var|const|let)\\s/)[0];\r\n if (!initial_data) throw new Error('Initial Response Data is undefined.');\r\n\r\n const cookies =\r\n JSON.parse(initial_data).topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton\r\n .buttonRenderer.command.saveConsentAction;\r\n if (cookies) {\r\n Object.assign(cookieJar, {\r\n VISITOR_INFO1_LIVE: cookies.visitorCookie,\r\n CONSENT: cookies.consentCookie\r\n });\r\n }\r\n\r\n const updatedValues = await acceptViewerDiscretion(\r\n player_response.videoDetails.videoId,\r\n cookieJar,\r\n body,\r\n false\r\n );\r\n player_response.streamingData = updatedValues.streamingData;\r\n } else if (player_response.playabilityStatus.status === 'LIVE_STREAM_OFFLINE') upcoming = true;\r\n else\r\n throw new Error(\r\n `While getting info from url\\n${\r\n player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ??\r\n player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText ??\r\n player_response.playabilityStatus.reason\r\n }`\r\n );\r\n }\r\n const html5player = `https://www.youtube.com${body.split('\"jsUrl\":\"')[1].split('\"')[0]}`;\r\n const duration = Number(player_response.videoDetails.lengthSeconds);\r\n const video_details = {\r\n url: `https://www.youtube.com/watch?v=${player_response.videoDetails.videoId}`,\r\n durationInSec: (duration < 0 ? 0 : duration) || 0\r\n };\r\n let format = [];\r\n if (!upcoming) {\r\n // TODO: Properly handle the formats, for now ignore and use iOS formats\r\n //format.push(...(player_response.streamingData.formats ?? []));\r\n //format.push(...(player_response.streamingData.adaptiveFormats ?? []));\r\n\r\n // get the formats for the android player for legacy videos\r\n // fixes the stream being closed because not enough data\r\n // arrived in time for ffmpeg to be able to extract audio data\r\n //if (parseAudioFormats(format).length === 0 && !options.htmldata) {\r\n // format = await getAndroidFormats(player_response.videoDetails.videoId, cookieJar, body);\r\n //}\r\n format = await getIosFormats(player_response.videoDetails.videoId, cookieJar, body);\r\n }\r\n\r\n const LiveStreamData = {\r\n isLive: player_response.videoDetails.isLiveContent,\r\n dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null,\r\n hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null\r\n };\r\n return await decipher_info(\r\n {\r\n LiveStreamData,\r\n html5player,\r\n format,\r\n video_details\r\n },\r\n true\r\n );\r\n}\r\n/**\r\n * Function to convert seconds to [hour : minutes : seconds] format\r\n * @param seconds seconds to convert\r\n * @returns [hour : minutes : seconds] format\r\n */\r\nfunction parseSeconds(seconds: number): string {\r\n const d = Number(seconds);\r\n const h = Math.floor(d / 3600);\r\n const m = Math.floor((d % 3600) / 60);\r\n const s = Math.floor((d % 3600) % 60);\r\n\r\n const hDisplay = h > 0 ? (h < 10 ? `0${h}` : h) + ':' : '';\r\n const mDisplay = m > 0 ? (m < 10 ? `0${m}` : m) + ':' : '00:';\r\n const sDisplay = s > 0 ? (s < 10 ? `0${s}` : s) : '00';\r\n return hDisplay + mDisplay + sDisplay;\r\n}\r\n/**\r\n * Gets data from YouTube url or ID or html body data and deciphers it.\r\n * ```\r\n * video_basic_info + decipher_info = video_info\r\n * ```\r\n *\r\n * Example\r\n * ```ts\r\n * const video = await play.video_info('youtube video url')\r\n *\r\n * const res = ... // Any https package get function.\r\n *\r\n * const video = await play.video_info(res.body, { htmldata : true })\r\n * ```\r\n * @param url YouTube url or ID or html body data\r\n * @param options Video Info Options\r\n * - `boolean` htmldata : given data is html data or not\r\n * @returns Deciphered Video Info {@link InfoData}.\r\n */\r\nexport async function video_info(url: string, options: InfoOptions = {}): Promise {\r\n const data = await video_basic_info(url.trim(), options);\r\n return await decipher_info(data);\r\n}\r\n/**\r\n * Function uses data from video_basic_info and deciphers it if it contains signatures.\r\n * @param data Data - {@link InfoData}\r\n * @param audio_only `boolean` - To decipher only audio formats only.\r\n * @returns Deciphered Video Info {@link InfoData}\r\n */\r\nexport async function decipher_info(\r\n data: T,\r\n audio_only: boolean = false\r\n): Promise {\r\n if (\r\n data.LiveStreamData.isLive === true &&\r\n data.LiveStreamData.dashManifestUrl !== null &&\r\n data.video_details.durationInSec === 0\r\n ) {\r\n return data;\r\n } else if (data.format.length > 0 && (data.format[0].signatureCipher || data.format[0].cipher)) {\r\n if (audio_only) data.format = parseAudioFormats(data.format);\r\n data.format = await format_decipher(data.format, data.html5player);\r\n return data;\r\n } else return data;\r\n}\r\n/**\r\n * Gets YouTube playlist info from a playlist url.\r\n *\r\n * Example\r\n * ```ts\r\n * const playlist = await play.playlist_info('youtube playlist url')\r\n *\r\n * const playlist = await play.playlist_info('youtube playlist url', { incomplete : true })\r\n * ```\r\n * @param url Playlist URL\r\n * @param options Playlist Info Options\r\n * - `boolean` incomplete : When this is set to `false` (default) this function will throw an error\r\n * if the playlist contains hidden videos.\r\n * If it is set to `true`, it parses the playlist skipping the hidden videos,\r\n * only visible videos are included in the resulting {@link YouTubePlaylist}.\r\n *\r\n * @returns YouTube Playlist\r\n */\r\nexport async function playlist_info(url: string, options: PlaylistOptions = {}): Promise {\r\n if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`);\r\n let url_ = url.trim();\r\n if (!url_.startsWith('https')) url_ = `https://www.youtube.com/playlist?list=${url_}`;\r\n if (url_.indexOf('list=') === -1) throw new Error('This is not a Playlist URL');\r\n\r\n if (url_.includes('music.youtube.com')) {\r\n const urlObj = new URL(url_);\r\n urlObj.hostname = 'www.youtube.com';\r\n url_ = urlObj.toString();\r\n }\r\n\r\n const body = await request(url_, {\r\n headers: {\r\n 'accept-language': options.language || 'en-US;q=0.9'\r\n }\r\n });\r\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\r\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\r\n const response = JSON.parse(\r\n body\r\n .split('var ytInitialData = ')[1]\r\n .split(';')[0]\r\n .split(/;\\s*(var|const|let)\\s/)[0]\r\n );\r\n if (response.alerts) {\r\n if (response.alerts[0].alertWithButtonRenderer?.type === 'INFO') {\r\n if (!options.incomplete)\r\n throw new Error(\r\n `While parsing playlist url\\n${response.alerts[0].alertWithButtonRenderer.text.simpleText}`\r\n );\r\n } else if (response.alerts[0].alertRenderer?.type === 'ERROR')\r\n throw new Error(`While parsing playlist url\\n${response.alerts[0].alertRenderer.text.runs[0].text}`);\r\n else throw new Error('While parsing playlist url\\nUnknown Playlist Error');\r\n }\r\n if (response.currentVideoEndpoint) {\r\n return getWatchPlaylist(response, body, url_);\r\n } else return getNormalPlaylist(response, body);\r\n}\r\n/**\r\n * Function to parse Playlist from YouTube search\r\n * @param data html data of that request\r\n * @param limit No. of videos to parse\r\n * @returns Array of YouTubeVideo.\r\n */\r\nexport function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {\r\n const videos = [];\r\n\r\n for (let i = 0; i < data.length; i++) {\r\n if (limit === videos.length) break;\r\n const info = data[i].playlistVideoRenderer;\r\n if (!info || !info.shortBylineText) continue;\r\n\r\n videos.push(\r\n new YouTubeVideo({\r\n id: info.videoId,\r\n duration: parseInt(info.lengthSeconds) || 0,\r\n duration_raw: info.lengthText?.simpleText ?? '0:00',\r\n thumbnails: info.thumbnail.thumbnails,\r\n title: info.title.runs[0].text,\r\n upcoming: info.upcomingEventData?.startTime\r\n ? new Date(parseInt(info.upcomingEventData.startTime) * 1000)\r\n : undefined,\r\n channel: {\r\n id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined,\r\n name: info.shortBylineText.runs[0].text || undefined,\r\n url: `https://www.youtube.com${\r\n info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\r\n info.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url\r\n }`,\r\n icon: undefined\r\n }\r\n })\r\n );\r\n }\r\n return videos;\r\n}\r\n/**\r\n * Function to get Continuation Token\r\n * @param data html data of playlist url\r\n * @returns token\r\n */\r\nexport function getContinuationToken(data: any): string {\r\n return data.find((x: any) => Object.keys(x)[0] === 'continuationItemRenderer')?.continuationItemRenderer\r\n .continuationEndpoint?.continuationCommand?.token;\r\n}\r\n\r\nasync function acceptViewerDiscretion(\r\n videoId: string,\r\n cookieJar: { [key: string]: string },\r\n body: string,\r\n extractRelated: boolean\r\n): Promise<{ streamingData: any; relatedVideos?: any }> {\r\n const apiKey =\r\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\r\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\r\n DEFAULT_API_KEY;\r\n const sessionToken =\r\n body.split('\"XSRF_TOKEN\":\"')[1]?.split('\"')[0].replaceAll('\\\\u003d', '=') ??\r\n body.split('\"xsrf_token\":\"')[1]?.split('\"')[0].replaceAll('\\\\u003d', '=');\r\n if (!sessionToken)\r\n throw new Error(`Unable to extract XSRF_TOKEN to accept the viewer discretion popup for video: ${videoId}.`);\r\n\r\n const verificationResponse = await request(`https://www.youtube.com/youtubei/v1/verify_age?key=${apiKey}&prettyPrint=false`, {\r\n method: 'POST',\r\n body: JSON.stringify({\r\n context: {\r\n client: {\r\n utcOffsetMinutes: 0,\r\n gl: 'US',\r\n hl: 'en',\r\n clientName: 'WEB',\r\n clientVersion:\r\n body.split('\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"')[1]?.split('\"')[0] ??\r\n body.split('\"innertube_context_client_version\":\"')[1]?.split('\"')[0] ??\r\n ''\r\n },\r\n user: {},\r\n request: {}\r\n },\r\n nextEndpoint: {\r\n urlEndpoint: {\r\n url: `/watch?v=${videoId}&has_verified=1`\r\n }\r\n },\r\n setControvercy: true\r\n }),\r\n cookies: true,\r\n cookieJar\r\n });\r\n\r\n const endpoint = JSON.parse(verificationResponse).actions[0].navigateAction.endpoint;\r\n\r\n const videoPage = await request(`https://www.youtube.com/${endpoint.urlEndpoint.url}&pbj=1`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/x-www-form-urlencoded'\r\n },\r\n body: new URLSearchParams([\r\n ['command', JSON.stringify(endpoint)],\r\n ['session_token', sessionToken]\r\n ]).toString(),\r\n cookies: true,\r\n cookieJar\r\n });\r\n\r\n if (videoPage.includes('

Something went wrong

'))\r\n throw new Error(`Unable to accept the viewer discretion popup for video: ${videoId}`);\r\n\r\n const videoPageData = JSON.parse(videoPage);\r\n\r\n if (videoPageData[2].playerResponse.playabilityStatus.status !== 'OK')\r\n throw new Error(\r\n `While getting info from url after trying to accept the discretion popup for video ${videoId}\\n${\r\n videoPageData[2].playerResponse.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason\r\n .simpleText ??\r\n videoPageData[2].playerResponse.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText\r\n }`\r\n );\r\n\r\n const streamingData = videoPageData[2].playerResponse.streamingData;\r\n\r\n if (extractRelated)\r\n return {\r\n streamingData,\r\n relatedVideos: videoPageData[3].response.contents.twoColumnWatchNextResults.secondaryResults\r\n };\r\n\r\n return { streamingData };\r\n}\r\n\r\nasync function getIosFormats(videoId: string, cookieJar: { [key: string]: string }, body: string): Promise {\r\n const apiKey =\r\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\r\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\r\n DEFAULT_API_KEY;\r\n\r\n const response = await request(`https://www.youtube.com/youtubei/v1/player?key=${apiKey}&prettyPrint=false`, {\r\n method: 'POST',\r\n body: JSON.stringify({\r\n context: {\r\n client: {\r\n clientName: 'IOS',\r\n clientVersion: '19.09.3',\r\n deviceModel: 'iPhone16,1',\r\n userAgent: 'com.google.ios.youtube/19.09.3 (iPhone; CPU iPhone OS 17_5 like Mac OS X)',\r\n hl: 'en',\r\n timeZone: 'UTC',\r\n utcOffsetMinutes: 0\r\n }\r\n },\r\n videoId: videoId,\r\n playbackContext: { contentPlaybackContext: { html5Preference: 'HTML5_PREF_WANTS' } },\r\n contentCheckOk: true,\r\n racyCheckOk: true\r\n }),\r\n cookies: true,\r\n cookieJar\r\n });\r\n\r\n return JSON.parse(response).streamingData.adaptiveFormats;\r\n //return JSON.parse(response).streamingData.formats;\r\n}\r\n\r\nfunction getWatchPlaylist(response: any, body: any, url: string): YouTubePlayList {\r\n const playlist_details = response.contents.twoColumnWatchNextResults.playlist?.playlist;\r\n if (!playlist_details)\r\n throw new Error(\"Watch playlist unavailable due to YouTube layout changes.\")\r\n\r\n const videos = getWatchPlaylistVideos(playlist_details.contents);\r\n const API_KEY =\r\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\r\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\r\n DEFAULT_API_KEY;\r\n\r\n const videoCount = playlist_details.totalVideos;\r\n const channel = playlist_details.shortBylineText?.runs?.[0];\r\n const badge = playlist_details.badges?.[0]?.metadataBadgeRenderer?.style.toLowerCase();\r\n\r\n return new YouTubePlayList({\r\n continuation: {\r\n api: API_KEY,\r\n token: getContinuationToken(playlist_details.contents),\r\n clientVersion:\r\n body.split('\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"')[1]?.split('\"')[0] ??\r\n body.split('\"innertube_context_client_version\":\"')[1]?.split('\"')[0] ??\r\n ''\r\n },\r\n id: playlist_details.playlistId || '',\r\n title: playlist_details.title || '',\r\n videoCount: parseInt(videoCount) || 0,\r\n videos: videos,\r\n url: url,\r\n channel: {\r\n id: channel?.navigationEndpoint?.browseEndpoint?.browseId || null,\r\n name: channel?.text || null,\r\n url: `https://www.youtube.com${\r\n channel?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl ||\r\n channel?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.url\r\n }`,\r\n verified: Boolean(badge?.includes('verified')),\r\n artist: Boolean(badge?.includes('artist'))\r\n }\r\n });\r\n}\r\n\r\nfunction getNormalPlaylist(response: any, body: any): YouTubePlayList {\r\n const json_data =\r\n response.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0]\r\n .itemSectionRenderer.contents[0].playlistVideoListRenderer.contents;\r\n const playlist_details = response.sidebar.playlistSidebarRenderer.items;\r\n\r\n const API_KEY =\r\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\r\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\r\n DEFAULT_API_KEY;\r\n const videos = getPlaylistVideos(json_data, 100);\r\n\r\n const data = playlist_details[0].playlistSidebarPrimaryInfoRenderer;\r\n if (!data.title.runs || !data.title.runs.length) throw new Error('Failed to Parse Playlist info.');\r\n\r\n const author = playlist_details[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner;\r\n const views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/\\D/g, '') : 0;\r\n const lastUpdate =\r\n data.stats\r\n .find((x: any) => 'runs' in x && x['runs'].find((y: any) => y.text.toLowerCase().includes('last update')))\r\n ?.runs.pop()?.text ?? null;\r\n const videosCount = data.stats[0].runs[0].text.replace(/\\D/g, '') || 0;\r\n\r\n const res = new YouTubePlayList({\r\n continuation: {\r\n api: API_KEY,\r\n token: getContinuationToken(json_data),\r\n clientVersion:\r\n body.split('\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"')[1]?.split('\"')[0] ??\r\n body.split('\"innertube_context_client_version\":\"')[1]?.split('\"')[0] ??\r\n ''\r\n },\r\n id: data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId,\r\n title: data.title.runs[0].text,\r\n videoCount: parseInt(videosCount) || 0,\r\n lastUpdate: lastUpdate,\r\n views: parseInt(views) || 0,\r\n videos: videos,\r\n url: `https://www.youtube.com/playlist?list=${data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId}`,\r\n link: `https://www.youtube.com${data.title.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,\r\n channel: author\r\n ? {\r\n name: author.videoOwnerRenderer.title.runs[0].text,\r\n id: author.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId,\r\n url: `https://www.youtube.com${\r\n author.videoOwnerRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url ||\r\n author.videoOwnerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl\r\n }`,\r\n icons: author.videoOwnerRenderer.thumbnail.thumbnails ?? []\r\n }\r\n : {},\r\n thumbnail: data.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length\r\n ? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails[\r\n data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.length - 1\r\n ]\r\n : null\r\n });\r\n return res;\r\n}\r\n\r\nfunction getWatchPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {\r\n const videos: YouTubeVideo[] = [];\r\n\r\n for (let i = 0; i < data.length; i++) {\r\n if (limit === videos.length) break;\r\n const info = data[i].playlistPanelVideoRenderer;\r\n if (!info || !info.shortBylineText) continue;\r\n const channel_info = info.shortBylineText.runs[0];\r\n\r\n videos.push(\r\n new YouTubeVideo({\r\n id: info.videoId,\r\n duration: parseDuration(info.lengthText?.simpleText) || 0,\r\n duration_raw: info.lengthText?.simpleText ?? '0:00',\r\n thumbnails: info.thumbnail.thumbnails,\r\n title: info.title.simpleText,\r\n upcoming:\r\n info.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer?.style === 'UPCOMING' || undefined,\r\n channel: {\r\n id: channel_info.navigationEndpoint.browseEndpoint.browseId || undefined,\r\n name: channel_info.text || undefined,\r\n url: `https://www.youtube.com${\r\n channel_info.navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\r\n channel_info.navigationEndpoint.commandMetadata.webCommandMetadata.url\r\n }`,\r\n icon: undefined\r\n }\r\n })\r\n );\r\n }\r\n\r\n return videos;\r\n}\r\n\r\nfunction parseDuration(text: string): number {\r\n if (!text) return 0;\r\n const split = text.split(':');\r\n\r\n switch (split.length) {\r\n case 2:\r\n return parseInt(split[0]) * 60 + parseInt(split[1]);\r\n\r\n case 3:\r\n return parseInt(split[0]) * 60 * 60 + parseInt(split[1]) * 60 + parseInt(split[2]);\r\n\r\n default:\r\n return 0;\r\n }\r\n}","import { WebmElements, WebmHeader } from 'play-audio';\r\nimport { Duplex, DuplexOptions } from 'node:stream';\r\n\r\nenum DataType {\r\n master,\r\n string,\r\n uint,\r\n binary,\r\n float\r\n}\r\n\r\nexport enum WebmSeekerState {\r\n READING_HEAD = 'READING_HEAD',\r\n READING_DATA = 'READING_DATA'\r\n}\r\n\r\ninterface WebmSeekerOptions extends DuplexOptions {\r\n mode?: 'precise' | 'granular';\r\n}\r\n\r\nconst WEB_ELEMENT_KEYS = Object.keys(WebmElements);\r\n\r\nexport class WebmSeeker extends Duplex {\r\n remaining?: Buffer;\r\n state: WebmSeekerState;\r\n chunk?: Buffer;\r\n cursor: number;\r\n header: WebmHeader;\r\n headfound: boolean;\r\n headerparsed: boolean;\r\n seekfound: boolean;\r\n private data_size: number;\r\n private offset: number;\r\n private data_length: number;\r\n private sec: number;\r\n private time: number;\r\n\r\n constructor(sec: number, options: WebmSeekerOptions) {\r\n super(options);\r\n this.state = WebmSeekerState.READING_HEAD;\r\n this.cursor = 0;\r\n this.header = new WebmHeader();\r\n this.headfound = false;\r\n this.headerparsed = false;\r\n this.seekfound = false;\r\n this.data_length = 0;\r\n this.data_size = 0;\r\n this.offset = 0;\r\n this.sec = sec;\r\n this.time = Math.floor(sec / 10) * 10;\r\n }\r\n\r\n private get vint_length(): number {\r\n let i = 0;\r\n for (; i < 8; i++) {\r\n if ((1 << (7 - i)) & this.chunk![this.cursor]) break;\r\n }\r\n return ++i;\r\n }\r\n\r\n private vint_value(): boolean {\r\n if (!this.chunk) return false;\r\n const length = this.vint_length;\r\n if (this.chunk.length < this.cursor + length) return false;\r\n let value = this.chunk[this.cursor] & ((1 << (8 - length)) - 1);\r\n for (let i = this.cursor + 1; i < this.cursor + length; i++) value = (value << 8) + this.chunk[i];\r\n this.data_size = length;\r\n this.data_length = value;\r\n return true;\r\n }\r\n\r\n cleanup() {\r\n this.cursor = 0;\r\n this.chunk = undefined;\r\n this.remaining = undefined;\r\n }\r\n\r\n _read() {}\r\n\r\n seek(content_length: number): Error | number {\r\n let clusterlength = 0,\r\n position = 0;\r\n let time_left = (this.sec - this.time) * 1000 || 0;\r\n time_left = Math.round(time_left / 20) * 20;\r\n if (!this.header.segment.cues) return new Error('Failed to Parse Cues');\r\n\r\n for (let i = 0; i < this.header.segment.cues.length; i++) {\r\n const data = this.header.segment.cues[i];\r\n if (Math.floor((data.time as number) / 1000) === this.time) {\r\n position = data.position as number;\r\n clusterlength = (this.header.segment.cues[i + 1]?.position || content_length) - position - 1;\r\n break;\r\n } else continue;\r\n }\r\n if (clusterlength === 0) return position;\r\n return this.offset + Math.round(position + (time_left / 20) * (clusterlength / 500));\r\n }\r\n\r\n _write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void {\r\n if (this.remaining) {\r\n this.chunk = Buffer.concat([this.remaining, chunk]);\r\n this.remaining = undefined;\r\n } else this.chunk = chunk;\r\n\r\n let err: Error | undefined;\r\n\r\n if (this.state === WebmSeekerState.READING_HEAD) err = this.readHead();\r\n else if (!this.seekfound) err = this.getClosestBlock();\r\n else err = this.readTag();\r\n\r\n if (err) callback(err);\r\n else callback();\r\n }\r\n\r\n private readHead(): Error | undefined {\r\n if (!this.chunk) return new Error('Chunk is missing');\r\n\r\n while (this.chunk.length > this.cursor) {\r\n const oldCursor = this.cursor;\r\n const id = this.vint_length;\r\n if (this.chunk.length < this.cursor + id) break;\r\n\r\n const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'));\r\n this.cursor += id;\r\n\r\n if (!this.vint_value()) {\r\n this.cursor = oldCursor;\r\n break;\r\n }\r\n if (!ebmlID) {\r\n this.cursor += this.data_size + this.data_length;\r\n continue;\r\n }\r\n\r\n if (!this.headfound) {\r\n if (ebmlID.name === 'ebml') this.headfound = true;\r\n else return new Error('Failed to find EBML ID at start of stream.');\r\n }\r\n const data = this.chunk.slice(\r\n this.cursor + this.data_size,\r\n this.cursor + this.data_size + this.data_length\r\n );\r\n const parse = this.header.parse(ebmlID, data);\r\n if (parse instanceof Error) return parse;\r\n\r\n // stop parsing the header once we have found the correct cue\r\n\r\n if (ebmlID.name === 'seekHead') this.offset = oldCursor;\r\n\r\n if (\r\n ebmlID.name === 'cueClusterPosition' &&\r\n this.header.segment.cues!.length > 2 &&\r\n this.time === (this.header.segment.cues!.at(-2)!.time as number) / 1000\r\n )\r\n this.emit('headComplete');\r\n\r\n if (ebmlID.type === DataType.master) {\r\n this.cursor += this.data_size;\r\n continue;\r\n }\r\n\r\n if (this.chunk.length < this.cursor + this.data_size + this.data_length) {\r\n this.cursor = oldCursor;\r\n break;\r\n } else this.cursor += this.data_size + this.data_length;\r\n }\r\n this.remaining = this.chunk.slice(this.cursor);\r\n this.cursor = 0;\r\n }\r\n\r\n private readTag(): Error | undefined {\r\n if (!this.chunk) return new Error('Chunk is missing');\r\n\r\n while (this.chunk.length > this.cursor) {\r\n const oldCursor = this.cursor;\r\n const id = this.vint_length;\r\n if (this.chunk.length < this.cursor + id) break;\r\n\r\n const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'));\r\n this.cursor += id;\r\n\r\n if (!this.vint_value()) {\r\n this.cursor = oldCursor;\r\n break;\r\n }\r\n if (!ebmlID) {\r\n this.cursor += this.data_size + this.data_length;\r\n continue;\r\n }\r\n\r\n const data = this.chunk.slice(\r\n this.cursor + this.data_size,\r\n this.cursor + this.data_size + this.data_length\r\n );\r\n const parse = this.header.parse(ebmlID, data);\r\n if (parse instanceof Error) return parse;\r\n\r\n if (ebmlID.type === DataType.master) {\r\n this.cursor += this.data_size;\r\n continue;\r\n }\r\n\r\n if (this.chunk.length < this.cursor + this.data_size + this.data_length) {\r\n this.cursor = oldCursor;\r\n break;\r\n } else this.cursor += this.data_size + this.data_length;\r\n\r\n if (ebmlID.name === 'simpleBlock') {\r\n const track = this.header.segment.tracks![this.header.audioTrack];\r\n if (!track || track.trackType !== 2) return new Error('No audio Track in this webm file.');\r\n if ((data[0] & 0xf) === track.trackNumber) this.push(data.slice(4));\r\n }\r\n }\r\n this.remaining = this.chunk.slice(this.cursor);\r\n this.cursor = 0;\r\n }\r\n\r\n private getClosestBlock(): Error | undefined {\r\n if (this.sec === 0) {\r\n this.seekfound = true;\r\n return this.readTag();\r\n }\r\n if (!this.chunk) return new Error('Chunk is missing');\r\n this.cursor = 0;\r\n let positionFound = false;\r\n while (!positionFound && this.cursor < this.chunk.length) {\r\n this.cursor = this.chunk.indexOf('a3', this.cursor, 'hex');\r\n if (this.cursor === -1) return new Error('Failed to find nearest Block.');\r\n this.cursor++;\r\n if (!this.vint_value()) return new Error('Failed to find correct simpleBlock in first chunk');\r\n if (this.cursor + this.data_length + this.data_length > this.chunk.length) continue;\r\n const data = this.chunk.slice(\r\n this.cursor + this.data_size,\r\n this.cursor + this.data_size + this.data_length\r\n );\r\n const track = this.header.segment.tracks![this.header.audioTrack];\r\n if (!track || track.trackType !== 2) return new Error('No audio Track in this webm file.');\r\n if ((data[0] & 0xf) === track.trackNumber) {\r\n this.cursor += this.data_size + this.data_length;\r\n this.push(data.slice(4));\r\n positionFound = true;\r\n } else continue;\r\n }\r\n if (!positionFound) return new Error('Failed to find nearest correct simple Block.');\r\n this.seekfound = true;\r\n return this.readTag();\r\n }\r\n\r\n private parseEbmlID(ebmlID: string) {\r\n if (WEB_ELEMENT_KEYS.includes(ebmlID)) return WebmElements[ebmlID];\r\n else return false;\r\n }\r\n\r\n _destroy(error: Error | null, callback: (error: Error | null) => void): void {\r\n this.cleanup();\r\n callback(error);\r\n }\r\n\r\n _final(callback: (error?: Error | null) => void): void {\r\n this.cleanup();\r\n callback();\r\n }\r\n}\r\n","import { IncomingMessage } from 'node:http';\r\nimport { request_stream } from '../../Request';\r\nimport { parseAudioFormats, StreamOptions, StreamType } from '../stream';\r\nimport { video_stream_info } from '../utils/extractor';\r\nimport { Timer } from './LiveStream';\r\nimport { WebmSeeker, WebmSeekerState } from './WebmSeeker';\r\n\r\n/**\r\n * YouTube Stream Class for seeking audio to a timeStamp.\r\n */\r\nexport class SeekStream {\r\n /**\r\n * WebmSeeker Stream through which data passes\r\n */\r\n stream: WebmSeeker;\r\n /**\r\n * Type of audio data that we recieved from normal youtube url.\r\n */\r\n type: StreamType;\r\n /**\r\n * Audio Endpoint Format Url to get data from.\r\n */\r\n private url: string;\r\n /**\r\n * Used to calculate no of bytes data that we have recieved\r\n */\r\n private bytes_count: number;\r\n /**\r\n * Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds)\r\n */\r\n private per_sec_bytes: number;\r\n /**\r\n * Length of the header in bytes\r\n */\r\n private header_length: number;\r\n /**\r\n * Total length of audio file in bytes\r\n */\r\n private content_length: number;\r\n /**\r\n * YouTube video url. [ Used only for retrying purposes only. ]\r\n */\r\n private video_url: string;\r\n /**\r\n * Timer for looping data every 265 seconds.\r\n */\r\n private timer: Timer;\r\n /**\r\n * Quality given by user. [ Used only for retrying purposes only. ]\r\n */\r\n private quality: number;\r\n /**\r\n * Incoming message that we recieve.\r\n *\r\n * Storing this is essential.\r\n * This helps to destroy the TCP connection completely if you stopped player in between the stream\r\n */\r\n private request: IncomingMessage | null;\r\n /**\r\n * YouTube Stream Class constructor\r\n * @param url Audio Endpoint url.\r\n * @param type Type of Stream\r\n * @param duration Duration of audio playback [ in seconds ]\r\n * @param headerLength Length of the header in bytes.\r\n * @param contentLength Total length of Audio file in bytes.\r\n * @param bitrate Bitrate provided by YouTube.\r\n * @param video_url YouTube video url.\r\n * @param options Options provided to stream function.\r\n */\r\n constructor(\r\n url: string,\r\n duration: number,\r\n headerLength: number,\r\n contentLength: number,\r\n bitrate: number,\r\n video_url: string,\r\n options: StreamOptions\r\n ) {\r\n this.stream = new WebmSeeker(options.seek!, {\r\n highWaterMark: 5 * 1000 * 1000,\r\n readableObjectMode: true\r\n });\r\n this.url = url;\r\n this.quality = options.quality as number;\r\n this.type = StreamType.Opus;\r\n this.bytes_count = 0;\r\n this.video_url = video_url;\r\n this.per_sec_bytes = bitrate ? Math.ceil(bitrate / 8) : Math.ceil(contentLength / duration);\r\n this.header_length = headerLength;\r\n this.content_length = contentLength;\r\n this.request = null;\r\n this.timer = new Timer(() => {\r\n this.timer.reuse();\r\n this.loop();\r\n }, 265);\r\n this.stream.on('close', () => {\r\n this.timer.destroy();\r\n this.cleanup();\r\n });\r\n this.seek();\r\n }\r\n /**\r\n * **INTERNAL Function**\r\n *\r\n * Uses stream functions to parse Webm Head and gets Offset byte to seek to.\r\n * @returns Nothing\r\n */\r\n private async seek(): Promise {\r\n const parse = await new Promise(async (res, rej) => {\r\n if (!this.stream.headerparsed) {\r\n const stream = await request_stream(this.url, {\r\n headers: {\r\n range: `bytes=0-${this.header_length}`\r\n }\r\n }).catch((err: Error) => err);\r\n\r\n if (stream instanceof Error) {\r\n rej(stream);\r\n return;\r\n }\r\n if (Number(stream.statusCode) >= 400) {\r\n rej(400);\r\n return;\r\n }\r\n this.request = stream;\r\n stream.pipe(this.stream, { end: false });\r\n\r\n // headComplete should always be called, leaving this here just in case\r\n stream.once('end', () => {\r\n this.stream.state = WebmSeekerState.READING_DATA;\r\n res('');\r\n });\r\n\r\n this.stream.once('headComplete', () => {\r\n stream.unpipe(this.stream);\r\n stream.destroy();\r\n this.stream.state = WebmSeekerState.READING_DATA;\r\n res('');\r\n });\r\n } else res('');\r\n }).catch((err) => err);\r\n if (parse instanceof Error) {\r\n this.stream.emit('error', parse);\r\n this.bytes_count = 0;\r\n this.per_sec_bytes = 0;\r\n this.cleanup();\r\n return;\r\n } else if (parse === 400) {\r\n await this.retry();\r\n this.timer.reuse();\r\n return this.seek();\r\n }\r\n const bytes = this.stream.seek(this.content_length);\r\n if (bytes instanceof Error) {\r\n this.stream.emit('error', bytes);\r\n this.bytes_count = 0;\r\n this.per_sec_bytes = 0;\r\n this.cleanup();\r\n return;\r\n }\r\n\r\n this.stream.seekfound = false;\r\n this.bytes_count = bytes;\r\n this.timer.reuse();\r\n this.loop();\r\n }\r\n /**\r\n * Retry if we get 404 or 403 Errors.\r\n */\r\n private async retry() {\r\n const info = await video_stream_info(this.video_url);\r\n const audioFormat = parseAudioFormats(info.format);\r\n this.url = audioFormat[this.quality].url;\r\n }\r\n /**\r\n * This cleans every used variable in class.\r\n *\r\n * This is used to prevent re-use of this class and helping garbage collector to collect it.\r\n */\r\n private cleanup() {\r\n this.request?.destroy();\r\n this.request = null;\r\n this.url = '';\r\n }\r\n /**\r\n * Getting data from audio endpoint url and passing it to stream.\r\n *\r\n * If 404 or 403 occurs, it will retry again.\r\n */\r\n private async loop() {\r\n if (this.stream.destroyed) {\r\n this.timer.destroy();\r\n this.cleanup();\r\n return;\r\n }\r\n const end: number = this.bytes_count + this.per_sec_bytes * 300;\r\n const stream = await request_stream(this.url, {\r\n headers: {\r\n range: `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}`\r\n }\r\n }).catch((err: Error) => err);\r\n if (stream instanceof Error) {\r\n this.stream.emit('error', stream);\r\n this.bytes_count = 0;\r\n this.per_sec_bytes = 0;\r\n this.cleanup();\r\n return;\r\n }\r\n if (Number(stream.statusCode) >= 400) {\r\n this.cleanup();\r\n await this.retry();\r\n this.timer.reuse();\r\n this.loop();\r\n return;\r\n }\r\n this.request = stream;\r\n stream.pipe(this.stream, { end: false });\r\n\r\n stream.once('error', async () => {\r\n this.cleanup();\r\n await this.retry();\r\n this.timer.reuse();\r\n this.loop();\r\n });\r\n\r\n stream.on('data', (chunk: any) => {\r\n this.bytes_count += chunk.length;\r\n });\r\n\r\n stream.on('end', () => {\r\n if (end >= this.content_length) {\r\n this.timer.destroy();\r\n this.stream.end();\r\n this.cleanup();\r\n }\r\n });\r\n }\r\n /**\r\n * Pauses timer.\r\n * Stops running of loop.\r\n *\r\n * Useful if you don't want to get excess data to be stored in stream.\r\n */\r\n pause() {\r\n this.timer.pause();\r\n }\r\n /**\r\n * Resumes timer.\r\n * Starts running of loop.\r\n */\r\n resume() {\r\n this.timer.resume();\r\n }\r\n}\r\n","import { request_content_length, request_stream } from '../Request';\r\nimport { LiveStream, Stream } from './classes/LiveStream';\r\nimport { SeekStream } from './classes/SeekStream';\r\nimport { InfoData, StreamInfoData } from './utils/constants';\r\nimport { video_stream_info } from './utils/extractor';\r\nimport { URL } from 'node:url';\r\n\r\nexport enum StreamType {\r\n Arbitrary = 'arbitrary',\r\n Raw = 'raw',\r\n OggOpus = 'ogg/opus',\r\n WebmOpus = 'webm/opus',\r\n Opus = 'opus'\r\n}\r\n\r\nexport interface StreamOptions {\r\n seek?: number;\r\n quality?: number;\r\n language?: string;\r\n htmldata?: boolean;\r\n precache?: number;\r\n discordPlayerCompatibility?: boolean;\r\n}\r\n\r\n/**\r\n * Command to find audio formats from given format array\r\n * @param formats Formats to search from\r\n * @returns Audio Formats array\r\n */\r\nexport function parseAudioFormats(formats: any[]) {\r\n const result: any[] = [];\r\n formats.forEach((format) => {\r\n const type = format.mimeType as string;\r\n if (type.startsWith('audio')) {\r\n format.codec = type.split('codecs=\"')[1].split('\"')[0];\r\n format.container = type.split('audio/')[1].split(';')[0];\r\n result.push(format);\r\n }\r\n });\r\n return result;\r\n}\r\n/**\r\n * Type for YouTube Stream\r\n */\r\nexport type YouTubeStream = Stream | LiveStream | SeekStream;\r\n/**\r\n * Stream command for YouTube\r\n * @param url YouTube URL\r\n * @param options lets you add quality for stream\r\n * @returns Stream class with type and stream for playing.\r\n */\r\nexport async function stream(url: string, options: StreamOptions = {}): Promise {\r\n const info = await video_stream_info(url, { htmldata: options.htmldata, language: options.language });\r\n return await stream_from_info(info, options);\r\n}\r\n/**\r\n * Stream command for YouTube using info from video_info or decipher_info function.\r\n * @param info video_info data\r\n * @param options lets you add quality for stream\r\n * @returns Stream class with type and stream for playing.\r\n */\r\nexport async function stream_from_info(\r\n info: InfoData | StreamInfoData,\r\n options: StreamOptions = {}\r\n): Promise {\r\n if (info.format.length === 0)\r\n throw new Error('Upcoming and premiere videos that are not currently live cannot be streamed.');\r\n if (options.quality && !Number.isInteger(options.quality))\r\n throw new Error(\"Quality must be set to an integer.\")\r\n\r\n const final: any[] = [];\r\n if (\r\n info.LiveStreamData.isLive === true &&\r\n info.LiveStreamData.dashManifestUrl !== null &&\r\n info.video_details.durationInSec === 0\r\n ) {\r\n return new LiveStream(\r\n info.LiveStreamData.dashManifestUrl,\r\n info.format[info.format.length - 1].targetDurationSec as number,\r\n info.video_details.url,\r\n options.precache\r\n );\r\n }\r\n\r\n const audioFormat = parseAudioFormats(info.format);\r\n if (typeof options.quality !== 'number') options.quality = audioFormat.length - 1;\r\n else if (options.quality <= 0) options.quality = 0;\r\n else if (options.quality >= audioFormat.length) options.quality = audioFormat.length - 1;\r\n if (audioFormat.length !== 0) final.push(audioFormat[options.quality]);\r\n else final.push(info.format[info.format.length - 1]);\r\n let type: StreamType =\r\n final[0].codec === 'opus' && final[0].container === 'webm' ? StreamType.WebmOpus : StreamType.Arbitrary;\r\n await request_stream(`https://${new URL(final[0].url).host}/generate_204`);\r\n if (type === StreamType.WebmOpus) {\r\n if (!options.discordPlayerCompatibility) {\r\n options.seek ??= 0;\r\n if (options.seek >= info.video_details.durationInSec || options.seek < 0)\r\n throw new Error(`Seeking beyond limit. [ 0 - ${info.video_details.durationInSec - 1}]`);\r\n return new SeekStream(\r\n final[0].url,\r\n info.video_details.durationInSec,\r\n final[0].indexRange.end,\r\n Number(final[0].contentLength),\r\n Number(final[0].bitrate),\r\n info.video_details.url,\r\n options\r\n );\r\n } else if (options.seek) throw new Error('Can not seek with discordPlayerCompatibility set to true.');\r\n }\r\n\r\n let contentLength;\r\n if (final[0].contentLength) {\r\n contentLength = Number(final[0].contentLength);\r\n } else {\r\n contentLength = await request_content_length(final[0].url);\r\n }\r\n\r\n return new Stream(\r\n final[0].url,\r\n type,\r\n info.video_details.durationInSec,\r\n contentLength,\r\n info.video_details.url,\r\n options\r\n );\r\n}\r\n","import { YouTubeVideo } from '../classes/Video';\r\nimport { YouTubePlayList } from '../classes/Playlist';\r\nimport { YouTubeChannel } from '../classes/Channel';\r\nimport { YouTube } from '..';\r\nimport { YouTubeThumbnail } from '../classes/Thumbnail';\r\n\r\nconst BLURRED_THUMBNAILS = [\r\n '-oaymwEpCOADEI4CSFryq4qpAxsIARUAAAAAGAElAADIQj0AgKJDeAHtAZmZGUI=',\r\n '-oaymwEiCOADEI4CSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BmZkZQg==',\r\n '-oaymwEiCOgCEMoBSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmbmQQ==',\r\n '-oaymwEiCNAFEJQDSFXyq4qpAxQIARUAAIhCGAFwAcABBu0BZmZmQg==',\r\n '-oaymwEdCNAFEJQDSFryq4qpAw8IARUAAIhCGAHtAWZmZkI=',\r\n '-oaymwEdCNACELwBSFryq4qpAw8IARUAAIhCGAHtAT0K10E='\r\n];\r\n\r\nexport interface ParseSearchInterface {\r\n type?: 'video' | 'playlist' | 'channel';\r\n limit?: number;\r\n language?: string;\r\n unblurNSFWThumbnails?: boolean;\r\n}\r\n\r\nexport interface thumbnail {\r\n width: string;\r\n height: string;\r\n url: string;\r\n}\r\n/**\r\n * Main command which converts html body data and returns the type of data requested.\r\n * @param html body of that request\r\n * @param options limit & type of YouTube search you want.\r\n * @returns Array of one of YouTube type.\r\n */\r\nexport function ParseSearchResult(html: string, options?: ParseSearchInterface): YouTube[] {\r\n if (!html) throw new Error(\"Can't parse Search result without data\");\r\n if (!options) options = { type: 'video', limit: 0 };\r\n else if (!options.type) options.type = 'video';\r\n const hasLimit = typeof options.limit === 'number' && options.limit > 0;\r\n options.unblurNSFWThumbnails ??= false;\r\n\r\n const data = html\r\n .split('var ytInitialData = ')?.[1]\r\n ?.split(';')[0]\r\n .split(/;\\s*(var|const|let)\\s/)[0];\r\n const json_data = JSON.parse(data);\r\n const results = [];\r\n const details =\r\n json_data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.flatMap(\r\n (s: any) => s.itemSectionRenderer?.contents\r\n );\r\n for (const detail of details) {\r\n if (hasLimit && results.length === options.limit) break;\r\n if (!detail || (!detail.videoRenderer && !detail.channelRenderer && !detail.playlistRenderer)) continue;\r\n switch (options.type) {\r\n case 'video': {\r\n const parsed = parseVideo(detail);\r\n if (parsed) {\r\n if (options.unblurNSFWThumbnails) parsed.thumbnails.forEach(unblurThumbnail);\r\n results.push(parsed);\r\n }\r\n break;\r\n }\r\n case 'channel': {\r\n const parsed = parseChannel(detail);\r\n if (parsed) results.push(parsed);\r\n break;\r\n }\r\n case 'playlist': {\r\n const parsed = parsePlaylist(detail);\r\n if (parsed) {\r\n if (options.unblurNSFWThumbnails && parsed.thumbnail) unblurThumbnail(parsed.thumbnail);\r\n results.push(parsed);\r\n }\r\n break;\r\n }\r\n default:\r\n throw new Error(`Unknown search type: ${options.type}`);\r\n }\r\n }\r\n return results;\r\n}\r\n/**\r\n * Function to convert [hour : minutes : seconds] format to seconds\r\n * @param duration hour : minutes : seconds format\r\n * @returns seconds\r\n */\r\nfunction parseDuration(duration: string): number {\r\n if (!duration) return 0;\r\n const args = duration.split(':');\r\n let dur = 0;\r\n\r\n switch (args.length) {\r\n case 3:\r\n dur = parseInt(args[0]) * 60 * 60 + parseInt(args[1]) * 60 + parseInt(args[2]);\r\n break;\r\n case 2:\r\n dur = parseInt(args[0]) * 60 + parseInt(args[1]);\r\n break;\r\n default:\r\n dur = parseInt(args[0]);\r\n }\r\n\r\n return dur;\r\n}\r\n/**\r\n * Function to parse Channel searches\r\n * @param data body of that channel request.\r\n * @returns YouTubeChannel class\r\n */\r\nexport function parseChannel(data?: any): YouTubeChannel {\r\n if (!data || !data.channelRenderer) throw new Error('Failed to Parse YouTube Channel');\r\n const badge = data.channelRenderer.ownerBadges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase();\r\n const url = `https://www.youtube.com${\r\n data.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\r\n data.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url\r\n }`;\r\n const thumbnail = data.channelRenderer.thumbnail.thumbnails[data.channelRenderer.thumbnail.thumbnails.length - 1];\r\n const res = new YouTubeChannel({\r\n id: data.channelRenderer.channelId,\r\n name: data.channelRenderer.title.simpleText,\r\n icon: {\r\n url: thumbnail.url.replace('//', 'https://'),\r\n width: thumbnail.width,\r\n height: thumbnail.height\r\n },\r\n url: url,\r\n verified: Boolean(badge?.includes('verified')),\r\n artist: Boolean(badge?.includes('artist')),\r\n subscribers: data.channelRenderer.subscriberCountText?.simpleText ?? '0 subscribers'\r\n });\r\n\r\n return res;\r\n}\r\n/**\r\n * Function to parse Video searches\r\n * @param data body of that video request.\r\n * @returns YouTubeVideo class\r\n */\r\nexport function parseVideo(data?: any): YouTubeVideo {\r\n if (!data || !data.videoRenderer) throw new Error('Failed to Parse YouTube Video');\r\n\r\n const channel = data.videoRenderer.ownerText.runs[0];\r\n const badge = data.videoRenderer.ownerBadges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase();\r\n const durationText = data.videoRenderer.lengthText;\r\n const res = new YouTubeVideo({\r\n id: data.videoRenderer.videoId,\r\n url: `https://www.youtube.com/watch?v=${data.videoRenderer.videoId}`,\r\n title: data.videoRenderer.title.runs[0].text,\r\n description: data.videoRenderer.detailedMetadataSnippets?.[0].snippetText.runs?.length\r\n ? data.videoRenderer.detailedMetadataSnippets[0].snippetText.runs.map((run: any) => run.text).join('')\r\n : '',\r\n duration: durationText ? parseDuration(durationText.simpleText) : 0,\r\n duration_raw: durationText ? durationText.simpleText : null,\r\n thumbnails: data.videoRenderer.thumbnail.thumbnails,\r\n channel: {\r\n id: channel.navigationEndpoint.browseEndpoint.browseId || null,\r\n name: channel.text || null,\r\n url: `https://www.youtube.com${\r\n channel.navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\r\n channel.navigationEndpoint.commandMetadata.webCommandMetadata.url\r\n }`,\r\n icons: data.videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail\r\n .thumbnails,\r\n verified: Boolean(badge?.includes('verified')),\r\n artist: Boolean(badge?.includes('artist'))\r\n },\r\n uploadedAt: data.videoRenderer.publishedTimeText?.simpleText ?? null,\r\n upcoming: data.videoRenderer.upcomingEventData?.startTime\r\n ? new Date(parseInt(data.videoRenderer.upcomingEventData.startTime) * 1000)\r\n : undefined,\r\n views: data.videoRenderer.viewCountText?.simpleText?.replace(/\\D/g, '') ?? 0,\r\n live: durationText ? false : true\r\n });\r\n\r\n return res;\r\n}\r\n/**\r\n * Function to parse Playlist searches\r\n * @param data body of that playlist request.\r\n * @returns YouTubePlaylist class\r\n */\r\nexport function parsePlaylist(data?: any): YouTubePlayList {\r\n if (!data || !data.playlistRenderer) throw new Error('Failed to Parse YouTube Playlist');\r\n\r\n const thumbnail =\r\n data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1];\r\n const channel = data.playlistRenderer.shortBylineText.runs?.[0];\r\n\r\n const res = new YouTubePlayList(\r\n {\r\n id: data.playlistRenderer.playlistId,\r\n title: data.playlistRenderer.title.simpleText,\r\n thumbnail: {\r\n id: data.playlistRenderer.playlistId,\r\n url: thumbnail.url,\r\n height: thumbnail.height,\r\n width: thumbnail.width\r\n },\r\n channel: {\r\n id: channel?.navigationEndpoint.browseEndpoint.browseId,\r\n name: channel?.text,\r\n url: `https://www.youtube.com${channel?.navigationEndpoint.commandMetadata.webCommandMetadata.url}`\r\n },\r\n videos: parseInt(data.playlistRenderer.videoCount.replace(/\\D/g, ''))\r\n },\r\n true\r\n );\r\n\r\n return res;\r\n}\r\n\r\nfunction unblurThumbnail(thumbnail: YouTubeThumbnail) {\r\n if (BLURRED_THUMBNAILS.find((sqp) => thumbnail.url.includes(sqp))) {\r\n thumbnail.url = thumbnail.url.split('?')[0];\r\n\r\n // we need to update the size parameters as the sqp parameter also included a cropped size\r\n switch (thumbnail.url.split('/').at(-1)!.split('.')[0]) {\r\n case 'hq2':\r\n case 'hqdefault':\r\n thumbnail.width = 480;\r\n thumbnail.height = 360;\r\n break;\r\n case 'hq720':\r\n thumbnail.width = 1280;\r\n thumbnail.height = 720;\r\n break;\r\n case 'sddefault':\r\n thumbnail.width = 640;\r\n thumbnail.height = 480;\r\n break;\r\n case 'mqdefault':\r\n thumbnail.width = 320;\r\n thumbnail.height = 180;\r\n break;\r\n case 'default':\r\n thumbnail.width = 120;\r\n thumbnail.height = 90;\r\n break;\r\n default:\r\n thumbnail.width = thumbnail.height = NaN;\r\n }\r\n }\r\n}\r\n","import { request } from './../Request';\r\nimport { ParseSearchInterface, ParseSearchResult } from './utils/parser';\r\nimport { YouTubeVideo } from './classes/Video';\r\nimport { YouTubeChannel } from './classes/Channel';\r\nimport { YouTubePlayList } from './classes/Playlist';\r\n\r\nenum SearchType {\r\n Video = 'EgIQAQ%253D%253D',\r\n PlayList = 'EgIQAw%253D%253D',\r\n Channel = 'EgIQAg%253D%253D'\r\n}\r\n\r\n/**\r\n * Type for YouTube returns\r\n */\r\nexport type YouTube = YouTubeVideo | YouTubeChannel | YouTubePlayList;\r\n/**\r\n * Command to search from YouTube\r\n * @param search The query to search\r\n * @param options limit & type of YouTube search you want.\r\n * @returns YouTube type.\r\n */\r\nexport async function yt_search(search: string, options: ParseSearchInterface = {}): Promise {\r\n let url = 'https://www.youtube.com/results?search_query=' + search;\r\n options.type ??= 'video';\r\n if (url.indexOf('&sp=') === -1) {\r\n url += '&sp=';\r\n switch (options.type) {\r\n case 'channel':\r\n url += SearchType.Channel;\r\n break;\r\n case 'playlist':\r\n url += SearchType.PlayList;\r\n break;\r\n case 'video':\r\n url += SearchType.Video;\r\n break;\r\n default:\r\n throw new Error(`Unknown search type: ${options.type}`);\r\n }\r\n }\r\n const body = await request(url, {\r\n headers: {\r\n 'accept-language': options.language || 'en-US;q=0.9'\r\n }\r\n });\r\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\r\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\r\n return ParseSearchResult(body, options);\r\n}\r\n","import { request } from '../Request';\r\nimport { SpotifyDataOptions } from '.';\r\nimport { AlbumJSON, PlaylistJSON, TrackJSON } from './constants';\r\n\r\nexport interface SpotifyTrackAlbum {\r\n /**\r\n * Spotify Track Album name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Track Album url\r\n */\r\n url: string;\r\n /**\r\n * Spotify Track Album id\r\n */\r\n id: string;\r\n /**\r\n * Spotify Track Album release date\r\n */\r\n release_date: string;\r\n /**\r\n * Spotify Track Album release date **precise**\r\n */\r\n release_date_precision: string;\r\n /**\r\n * Spotify Track Album total tracks number\r\n */\r\n total_tracks: number;\r\n}\r\n\r\nexport interface SpotifyArtists {\r\n /**\r\n * Spotify Artist Name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Artist Url\r\n */\r\n url: string;\r\n /**\r\n * Spotify Artist ID\r\n */\r\n id: string;\r\n}\r\n\r\nexport interface SpotifyThumbnail {\r\n /**\r\n * Spotify Thumbnail height\r\n */\r\n height: number;\r\n /**\r\n * Spotify Thumbnail width\r\n */\r\n width: number;\r\n /**\r\n * Spotify Thumbnail url\r\n */\r\n url: string;\r\n}\r\n\r\nexport interface SpotifyCopyright {\r\n /**\r\n * Spotify Copyright Text\r\n */\r\n text: string;\r\n /**\r\n * Spotify Copyright Type\r\n */\r\n type: string;\r\n}\r\n/**\r\n * Spotify Track Class\r\n */\r\nexport class SpotifyTrack {\r\n /**\r\n * Spotify Track Name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Class type. == \"track\"\r\n */\r\n type: 'track' | 'playlist' | 'album';\r\n /**\r\n * Spotify Track ID\r\n */\r\n id: string;\r\n /**\r\n * Spotify Track ISRC\r\n */\r\n isrc: string;\r\n /**\r\n * Spotify Track url\r\n */\r\n url: string;\r\n /**\r\n * Spotify Track explicit info.\r\n */\r\n explicit: boolean;\r\n /**\r\n * Spotify Track playability info.\r\n */\r\n playable: boolean;\r\n /**\r\n * Spotify Track Duration in seconds\r\n */\r\n durationInSec: number;\r\n /**\r\n * Spotify Track Duration in milli seconds\r\n */\r\n durationInMs: number;\r\n /**\r\n * Spotify Track Artists data [ array ]\r\n */\r\n artists: SpotifyArtists[];\r\n /**\r\n * Spotify Track Album data\r\n */\r\n album: SpotifyTrackAlbum | undefined;\r\n /**\r\n * Spotify Track Thumbnail Data\r\n */\r\n thumbnail: SpotifyThumbnail | undefined;\r\n /**\r\n * Constructor for Spotify Track\r\n * @param data\r\n */\r\n constructor(data: any) {\r\n this.name = data.name;\r\n this.id = data.id;\r\n this.isrc = data.external_ids?.isrc || '';\r\n this.type = 'track';\r\n this.url = data.external_urls.spotify;\r\n this.explicit = data.explicit;\r\n this.playable = data.is_playable;\r\n this.durationInMs = data.duration_ms;\r\n this.durationInSec = Math.round(this.durationInMs / 1000);\r\n const artists: SpotifyArtists[] = [];\r\n data.artists.forEach((v: any) => {\r\n artists.push({\r\n name: v.name,\r\n id: v.id,\r\n url: v.external_urls.spotify\r\n });\r\n });\r\n this.artists = artists;\r\n if (!data.album?.name) this.album = undefined;\r\n else {\r\n this.album = {\r\n name: data.album.name,\r\n url: data.external_urls.spotify,\r\n id: data.album.id,\r\n release_date: data.album.release_date,\r\n release_date_precision: data.album.release_date_precision,\r\n total_tracks: data.album.total_tracks\r\n };\r\n }\r\n if (!data.album?.images?.[0]) this.thumbnail = undefined;\r\n else this.thumbnail = data.album.images[0];\r\n }\r\n\r\n toJSON(): TrackJSON {\r\n return {\r\n name: this.name,\r\n id: this.id,\r\n url: this.url,\r\n explicit: this.explicit,\r\n durationInMs: this.durationInMs,\r\n durationInSec: this.durationInSec,\r\n artists: this.artists,\r\n album: this.album,\r\n thumbnail: this.thumbnail\r\n };\r\n }\r\n}\r\n/**\r\n * Spotify Playlist Class\r\n */\r\nexport class SpotifyPlaylist {\r\n /**\r\n * Spotify Playlist Name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Class type. == \"playlist\"\r\n */\r\n type: 'track' | 'playlist' | 'album';\r\n /**\r\n * Spotify Playlist collaborative boolean.\r\n */\r\n collaborative: boolean;\r\n /**\r\n * Spotify Playlist Description\r\n */\r\n description: string;\r\n /**\r\n * Spotify Playlist URL\r\n */\r\n url: string;\r\n /**\r\n * Spotify Playlist ID\r\n */\r\n id: string;\r\n /**\r\n * Spotify Playlist Thumbnail Data\r\n */\r\n thumbnail: SpotifyThumbnail;\r\n /**\r\n * Spotify Playlist Owner Artist data\r\n */\r\n owner: SpotifyArtists;\r\n /**\r\n * Spotify Playlist total tracks Count\r\n */\r\n tracksCount: number;\r\n /**\r\n * Spotify Playlist Spotify data\r\n *\r\n * @private\r\n */\r\n private spotifyData: SpotifyDataOptions;\r\n /**\r\n * Spotify Playlist fetched tracks Map\r\n *\r\n * @private\r\n */\r\n private fetched_tracks: Map;\r\n /**\r\n * Boolean to tell whether it is a searched result or not.\r\n */\r\n private readonly search: boolean;\r\n /**\r\n * Constructor for Spotify Playlist Class\r\n * @param data JSON parsed data of playlist\r\n * @param spotifyData Data about sporify token for furhter fetching.\r\n */\r\n constructor(data: any, spotifyData: SpotifyDataOptions, search: boolean) {\r\n this.name = data.name;\r\n this.type = 'playlist';\r\n this.search = search;\r\n this.collaborative = data.collaborative;\r\n this.description = data.description;\r\n this.url = data.external_urls.spotify;\r\n this.id = data.id;\r\n this.thumbnail = data.images[0];\r\n this.owner = {\r\n name: data.owner.display_name,\r\n url: data.owner.external_urls.spotify,\r\n id: data.owner.id\r\n };\r\n this.tracksCount = Number(data.tracks.total);\r\n const videos: SpotifyTrack[] = [];\r\n if (!this.search)\r\n data.tracks.items.forEach((v: any) => {\r\n if (v.track) videos.push(new SpotifyTrack(v.track));\r\n });\r\n this.fetched_tracks = new Map();\r\n this.fetched_tracks.set('1', videos);\r\n this.spotifyData = spotifyData;\r\n }\r\n /**\r\n * Fetches Spotify Playlist tracks more than 100 tracks.\r\n *\r\n * For getting all tracks in playlist, see `total_pages` property.\r\n * @returns Playlist Class.\r\n */\r\n async fetch() {\r\n if (this.search) return this;\r\n let fetching: number;\r\n if (this.tracksCount > 1000) fetching = 1000;\r\n else fetching = this.tracksCount;\r\n if (fetching <= 100) return this;\r\n const work = [];\r\n for (let i = 2; i <= Math.ceil(fetching / 100); i++) {\r\n work.push(\r\n new Promise(async (resolve, reject) => {\r\n const response = await request(\r\n `https://api.spotify.com/v1/playlists/${this.id}/tracks?offset=${\r\n (i - 1) * 100\r\n }&limit=100&market=${this.spotifyData.market}`,\r\n {\r\n headers: {\r\n Authorization: `${this.spotifyData.token_type} ${this.spotifyData.access_token}`\r\n }\r\n }\r\n ).catch((err) => reject(`Response Error : \\n${err}`));\r\n const videos: SpotifyTrack[] = [];\r\n if (typeof response !== 'string') return;\r\n const json_data = JSON.parse(response);\r\n json_data.items.forEach((v: any) => {\r\n if (v.track) videos.push(new SpotifyTrack(v.track));\r\n });\r\n this.fetched_tracks.set(`${i}`, videos);\r\n resolve('Success');\r\n })\r\n );\r\n }\r\n await Promise.allSettled(work);\r\n return this;\r\n }\r\n /**\r\n * Spotify Playlist tracks are divided in pages.\r\n *\r\n * For example getting data of 101 - 200 videos in a playlist,\r\n *\r\n * ```ts\r\n * const playlist = await play.spotify('playlist url')\r\n *\r\n * await playlist.fetch()\r\n *\r\n * const result = playlist.page(2)\r\n * ```\r\n * @param num Page Number\r\n * @returns\r\n */\r\n page(num: number) {\r\n if (!num) throw new Error('Page number is not provided');\r\n if (!this.fetched_tracks.has(`${num}`)) throw new Error('Given Page number is invalid');\r\n return this.fetched_tracks.get(`${num}`) as SpotifyTrack[];\r\n }\r\n /**\r\n * Gets total number of pages in that playlist class.\r\n * @see {@link SpotifyPlaylist.all_tracks}\r\n */\r\n get total_pages() {\r\n return this.fetched_tracks.size;\r\n }\r\n /**\r\n * Spotify Playlist total no of tracks that have been fetched so far.\r\n */\r\n get total_tracks() {\r\n if (this.search) return this.tracksCount;\r\n const page_number: number = this.total_pages;\r\n return (page_number - 1) * 100 + (this.fetched_tracks.get(`${page_number}`) as SpotifyTrack[]).length;\r\n }\r\n /**\r\n * Fetches all the tracks in the playlist and returns them\r\n *\r\n * ```ts\r\n * const playlist = await play.spotify('playlist url')\r\n *\r\n * const tracks = await playlist.all_tracks()\r\n * ```\r\n * @returns An array of {@link SpotifyTrack}\r\n */\r\n async all_tracks(): Promise {\r\n await this.fetch();\r\n\r\n const tracks: SpotifyTrack[] = [];\r\n\r\n for (const page of this.fetched_tracks.values()) tracks.push(...page);\r\n\r\n return tracks;\r\n }\r\n /**\r\n * Converts Class to JSON\r\n * @returns JSON data\r\n */\r\n toJSON(): PlaylistJSON {\r\n return {\r\n name: this.name,\r\n collaborative: this.collaborative,\r\n description: this.description,\r\n url: this.url,\r\n id: this.id,\r\n thumbnail: this.thumbnail,\r\n owner: this.owner,\r\n tracksCount: this.tracksCount\r\n };\r\n }\r\n}\r\n/**\r\n * Spotify Album Class\r\n */\r\nexport class SpotifyAlbum {\r\n /**\r\n * Spotify Album Name\r\n */\r\n name: string;\r\n /**\r\n * Spotify Class type. == \"album\"\r\n */\r\n type: 'track' | 'playlist' | 'album';\r\n /**\r\n * Spotify Album url\r\n */\r\n url: string;\r\n /**\r\n * Spotify Album id\r\n */\r\n id: string;\r\n /**\r\n * Spotify Album Thumbnail data\r\n */\r\n thumbnail: SpotifyThumbnail;\r\n /**\r\n * Spotify Album artists [ array ]\r\n */\r\n artists: SpotifyArtists[];\r\n /**\r\n * Spotify Album copyright data [ array ]\r\n */\r\n copyrights: SpotifyCopyright[];\r\n /**\r\n * Spotify Album Release date\r\n */\r\n release_date: string;\r\n /**\r\n * Spotify Album Release Date **precise**\r\n */\r\n release_date_precision: string;\r\n /**\r\n * Spotify Album total no of tracks\r\n */\r\n tracksCount: number;\r\n /**\r\n * Spotify Album Spotify data\r\n *\r\n * @private\r\n */\r\n private spotifyData: SpotifyDataOptions;\r\n /**\r\n * Spotify Album fetched tracks Map\r\n *\r\n * @private\r\n */\r\n private fetched_tracks: Map;\r\n /**\r\n * Boolean to tell whether it is a searched result or not.\r\n */\r\n private readonly search: boolean;\r\n /**\r\n * Constructor for Spotify Album Class\r\n * @param data Json parsed album data\r\n * @param spotifyData Spotify credentials\r\n */\r\n constructor(data: any, spotifyData: SpotifyDataOptions, search: boolean) {\r\n this.name = data.name;\r\n this.type = 'album';\r\n this.id = data.id;\r\n this.search = search;\r\n this.url = data.external_urls.spotify;\r\n this.thumbnail = data.images[0];\r\n const artists: SpotifyArtists[] = [];\r\n data.artists.forEach((v: any) => {\r\n artists.push({\r\n name: v.name,\r\n id: v.id,\r\n url: v.external_urls.spotify\r\n });\r\n });\r\n this.artists = artists;\r\n this.copyrights = data.copyrights;\r\n this.release_date = data.release_date;\r\n this.release_date_precision = data.release_date_precision;\r\n this.tracksCount = data.total_tracks;\r\n const videos: SpotifyTrack[] = [];\r\n if (!this.search)\r\n data.tracks.items.forEach((v: any) => {\r\n videos.push(new SpotifyTrack(v));\r\n });\r\n this.fetched_tracks = new Map();\r\n this.fetched_tracks.set('1', videos);\r\n this.spotifyData = spotifyData;\r\n }\r\n /**\r\n * Fetches Spotify Album tracks more than 50 tracks.\r\n *\r\n * For getting all tracks in album, see `total_pages` property.\r\n * @returns Album Class.\r\n */\r\n async fetch() {\r\n if (this.search) return this;\r\n let fetching: number;\r\n if (this.tracksCount > 500) fetching = 500;\r\n else fetching = this.tracksCount;\r\n if (fetching <= 50) return this;\r\n const work = [];\r\n for (let i = 2; i <= Math.ceil(fetching / 50); i++) {\r\n work.push(\r\n new Promise(async (resolve, reject) => {\r\n const response = await request(\r\n `https://api.spotify.com/v1/albums/${this.id}/tracks?offset=${(i - 1) * 50}&limit=50&market=${\r\n this.spotifyData.market\r\n }`,\r\n {\r\n headers: {\r\n Authorization: `${this.spotifyData.token_type} ${this.spotifyData.access_token}`\r\n }\r\n }\r\n ).catch((err) => reject(`Response Error : \\n${err}`));\r\n const videos: SpotifyTrack[] = [];\r\n if (typeof response !== 'string') return;\r\n const json_data = JSON.parse(response);\r\n json_data.items.forEach((v: any) => {\r\n if (v) videos.push(new SpotifyTrack(v));\r\n });\r\n this.fetched_tracks.set(`${i}`, videos);\r\n resolve('Success');\r\n })\r\n );\r\n }\r\n await Promise.allSettled(work);\r\n return this;\r\n }\r\n /**\r\n * Spotify Album tracks are divided in pages.\r\n *\r\n * For example getting data of 51 - 100 videos in a album,\r\n *\r\n * ```ts\r\n * const album = await play.spotify('album url')\r\n *\r\n * await album.fetch()\r\n *\r\n * const result = album.page(2)\r\n * ```\r\n * @param num Page Number\r\n * @returns\r\n */\r\n page(num: number) {\r\n if (!num) throw new Error('Page number is not provided');\r\n if (!this.fetched_tracks.has(`${num}`)) throw new Error('Given Page number is invalid');\r\n return this.fetched_tracks.get(`${num}`);\r\n }\r\n /**\r\n * Gets total number of pages in that album class.\r\n * @see {@link SpotifyAlbum.all_tracks}\r\n */\r\n get total_pages() {\r\n return this.fetched_tracks.size;\r\n }\r\n /**\r\n * Spotify Album total no of tracks that have been fetched so far.\r\n */\r\n get total_tracks() {\r\n if (this.search) return this.tracksCount;\r\n const page_number: number = this.total_pages;\r\n return (page_number - 1) * 100 + (this.fetched_tracks.get(`${page_number}`) as SpotifyTrack[]).length;\r\n }\r\n /**\r\n * Fetches all the tracks in the album and returns them\r\n *\r\n * ```ts\r\n * const album = await play.spotify('album url')\r\n *\r\n * const tracks = await album.all_tracks()\r\n * ```\r\n * @returns An array of {@link SpotifyTrack}\r\n */\r\n async all_tracks(): Promise {\r\n await this.fetch();\r\n\r\n const tracks: SpotifyTrack[] = [];\r\n\r\n for (const page of this.fetched_tracks.values()) tracks.push(...page);\r\n\r\n return tracks;\r\n }\r\n /**\r\n * Converts Class to JSON\r\n * @returns JSON data\r\n */\r\n toJSON(): AlbumJSON {\r\n return {\r\n name: this.name,\r\n id: this.id,\r\n type: this.type,\r\n url: this.url,\r\n thumbnail: this.thumbnail,\r\n artists: this.artists,\r\n copyrights: this.copyrights,\r\n release_date: this.release_date,\r\n release_date_precision: this.release_date_precision,\r\n tracksCount: this.tracksCount\r\n };\r\n }\r\n}\r\n","import { request } from '../Request';\r\nimport { SpotifyAlbum, SpotifyPlaylist, SpotifyTrack } from './classes';\r\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs';\r\n\r\nlet spotifyData: SpotifyDataOptions;\r\nif (existsSync('.data/spotify.data')) {\r\n spotifyData = JSON.parse(readFileSync('.data/spotify.data', 'utf-8'));\r\n spotifyData.file = true;\r\n}\r\n/**\r\n * Spotify Data options that are stored in spotify.data file.\r\n */\r\nexport interface SpotifyDataOptions {\r\n client_id: string;\r\n client_secret: string;\r\n redirect_url?: string;\r\n authorization_code?: string;\r\n access_token?: string;\r\n refresh_token?: string;\r\n token_type?: string;\r\n expires_in?: number;\r\n expiry?: number;\r\n market?: string;\r\n file?: boolean;\r\n}\r\n\r\nconst pattern = /^((https:)?\\/\\/)?open\\.spotify\\.com\\/(?:intl\\-.{2}\\/)?(track|album|playlist)\\//;\r\n/**\r\n * Gets Spotify url details.\r\n *\r\n * ```ts\r\n * let spot = await play.spotify('spotify url')\r\n *\r\n * // spot.type === \"track\" | \"playlist\" | \"album\"\r\n *\r\n * if (spot.type === \"track\") {\r\n * spot = spot as play.SpotifyTrack\r\n * // Code with spotify track class.\r\n * }\r\n * ```\r\n * @param url Spotify Url\r\n * @returns A {@link SpotifyTrack} or {@link SpotifyPlaylist} or {@link SpotifyAlbum}\r\n */\r\nexport async function spotify(url: string): Promise {\r\n if (!spotifyData) throw new Error('Spotify Data is missing\\nDid you forgot to do authorization ?');\r\n const url_ = url.trim();\r\n if (!url_.match(pattern)) throw new Error('This is not a Spotify URL');\r\n if (url_.indexOf('track/') !== -1) {\r\n const trackID = url_.split('track/')[1].split('&')[0].split('?')[0];\r\n const response = await request(`https://api.spotify.com/v1/tracks/${trackID}?market=${spotifyData.market}`, {\r\n headers: {\r\n Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`\r\n }\r\n }).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const resObj = JSON.parse(response);\r\n if (resObj.error) throw new Error(`Got ${resObj.error.status} from the spotify request: ${resObj.error.message}`);\r\n return new SpotifyTrack(resObj);\r\n } else if (url_.indexOf('album/') !== -1) {\r\n const albumID = url.split('album/')[1].split('&')[0].split('?')[0];\r\n const response = await request(`https://api.spotify.com/v1/albums/${albumID}?market=${spotifyData.market}`, {\r\n headers: {\r\n Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`\r\n }\r\n }).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const resObj = JSON.parse(response);\r\n if (resObj.error) throw new Error(`Got ${resObj.error.status} from the spotify request: ${resObj.error.message}`);\r\n return new SpotifyAlbum(resObj, spotifyData, false);\r\n } else if (url_.indexOf('playlist/') !== -1) {\r\n const playlistID = url.split('playlist/')[1].split('&')[0].split('?')[0];\r\n const response = await request(\r\n `https://api.spotify.com/v1/playlists/${playlistID}?market=${spotifyData.market}`,\r\n {\r\n headers: {\r\n Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`\r\n }\r\n }\r\n ).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const resObj = JSON.parse(response);\r\n if (resObj.error) throw new Error(`Got ${resObj.error.status} from the spotify request: ${resObj.error.message}`);\r\n return new SpotifyPlaylist(resObj, spotifyData, false);\r\n } else throw new Error('URL is out of scope for play-dl.');\r\n}\r\n/**\r\n * Validate Spotify url\r\n * @param url Spotify URL\r\n * @returns\r\n * ```ts\r\n * 'track' | 'playlist' | 'album' | 'search' | false\r\n * ```\r\n */\r\nexport function sp_validate(url: string): 'track' | 'playlist' | 'album' | 'search' | false {\r\n const url_ = url.trim();\r\n if (!url_.startsWith('https')) return 'search';\r\n if (!url_.match(pattern)) return false;\r\n if (url_.indexOf('track/') !== -1) {\r\n return 'track';\r\n } else if (url_.indexOf('album/') !== -1) {\r\n return 'album';\r\n } else if (url_.indexOf('playlist/') !== -1) {\r\n return 'playlist';\r\n } else return false;\r\n}\r\n/**\r\n * Fuction for authorizing for spotify data.\r\n * @param data Sportify Data options to validate\r\n * @returns boolean.\r\n */\r\nexport async function SpotifyAuthorize(data: SpotifyDataOptions, file: boolean): Promise {\r\n const response = await request(`https://accounts.spotify.com/api/token`, {\r\n headers: {\r\n 'Authorization': `Basic ${Buffer.from(`${data.client_id}:${data.client_secret}`).toString('base64')}`,\r\n 'Content-Type': 'application/x-www-form-urlencoded'\r\n },\r\n body: `grant_type=authorization_code&code=${data.authorization_code}&redirect_uri=${encodeURI(\r\n data.redirect_url as string\r\n )}`,\r\n method: 'POST'\r\n }).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const resp_json = JSON.parse(response);\r\n spotifyData = {\r\n client_id: data.client_id,\r\n client_secret: data.client_secret,\r\n redirect_url: data.redirect_url,\r\n access_token: resp_json.access_token,\r\n refresh_token: resp_json.refresh_token,\r\n expires_in: Number(resp_json.expires_in),\r\n expiry: Date.now() + (resp_json.expires_in - 1) * 1000,\r\n token_type: resp_json.token_type,\r\n market: data.market\r\n };\r\n if (file) writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4));\r\n else {\r\n console.log(`Client ID : ${spotifyData.client_id}`);\r\n console.log(`Client Secret : ${spotifyData.client_secret}`);\r\n console.log(`Refresh Token : ${spotifyData.refresh_token}`);\r\n console.log(`Market : ${spotifyData.market}`);\r\n console.log(`\\nPaste above info in setToken function.`);\r\n }\r\n return true;\r\n}\r\n/**\r\n * Checks if spotify token is expired or not.\r\n *\r\n * Update token if returned false.\r\n * ```ts\r\n * if (play.is_expired()) {\r\n * await play.refreshToken()\r\n * }\r\n * ```\r\n * @returns boolean\r\n */\r\nexport function is_expired(): boolean {\r\n if (Date.now() >= (spotifyData.expiry as number)) return true;\r\n else return false;\r\n}\r\n/**\r\n * type for Spotify Classes\r\n */\r\nexport type Spotify = SpotifyAlbum | SpotifyPlaylist | SpotifyTrack;\r\n/**\r\n * Function for searching songs on Spotify\r\n * @param query searching query\r\n * @param type \"album\" | \"playlist\" | \"track\"\r\n * @param limit max no of results\r\n * @returns Spotify type.\r\n */\r\nexport async function sp_search(\r\n query: string,\r\n type: 'album' | 'playlist' | 'track',\r\n limit: number = 10\r\n): Promise {\r\n const results: Spotify[] = [];\r\n if (!spotifyData) throw new Error('Spotify Data is missing\\nDid you forget to do authorization ?');\r\n if (query.length === 0) throw new Error('Pass some query to search.');\r\n if (limit > 50 || limit < 0) throw new Error(`You crossed limit range of Spotify [ 0 - 50 ]`);\r\n const response = await request(\r\n `https://api.spotify.com/v1/search?type=${type}&q=${query}&limit=${limit}&market=${spotifyData.market}`,\r\n {\r\n headers: {\r\n Authorization: `${spotifyData.token_type} ${spotifyData.access_token}`\r\n }\r\n }\r\n ).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) throw response;\r\n const json_data = JSON.parse(response);\r\n if (type === 'track') {\r\n json_data.tracks.items.forEach((track: any) => {\r\n results.push(new SpotifyTrack(track));\r\n });\r\n } else if (type === 'album') {\r\n json_data.albums.items.forEach((album: any) => {\r\n results.push(new SpotifyAlbum(album, spotifyData, true));\r\n });\r\n } else if (type === 'playlist') {\r\n json_data.playlists.items.forEach((playlist: any) => {\r\n results.push(new SpotifyPlaylist(playlist, spotifyData, true));\r\n });\r\n }\r\n return results;\r\n}\r\n/**\r\n * Refreshes Token\r\n *\r\n * ```ts\r\n * if (play.is_expired()) {\r\n * await play.refreshToken()\r\n * }\r\n * ```\r\n * @returns boolean\r\n */\r\nexport async function refreshToken(): Promise {\r\n const response = await request(`https://accounts.spotify.com/api/token`, {\r\n headers: {\r\n 'Authorization': `Basic ${Buffer.from(`${spotifyData.client_id}:${spotifyData.client_secret}`).toString(\r\n 'base64'\r\n )}`,\r\n 'Content-Type': 'application/x-www-form-urlencoded'\r\n },\r\n body: `grant_type=refresh_token&refresh_token=${spotifyData.refresh_token}`,\r\n method: 'POST'\r\n }).catch((err: Error) => {\r\n return err;\r\n });\r\n if (response instanceof Error) return false;\r\n const resp_json = JSON.parse(response);\r\n spotifyData.access_token = resp_json.access_token;\r\n spotifyData.expires_in = Number(resp_json.expires_in);\r\n spotifyData.expiry = Date.now() + (resp_json.expires_in - 1) * 1000;\r\n spotifyData.token_type = resp_json.token_type;\r\n if (spotifyData.file) writeFileSync('.data/spotify.data', JSON.stringify(spotifyData, undefined, 4));\r\n return true;\r\n}\r\n\r\nexport async function setSpotifyToken(options: SpotifyDataOptions) {\r\n spotifyData = options;\r\n spotifyData.file = false;\r\n await refreshToken();\r\n}\r\n\r\nexport { SpotifyTrack, SpotifyAlbum, SpotifyPlaylist };\r\n","import { existsSync, readFileSync } from 'node:fs';\r\nimport { StreamType } from '../YouTube/stream';\r\nimport { request } from '../Request';\r\nimport { SoundCloudPlaylist, SoundCloudTrack, SoundCloudTrackFormat, SoundCloudStream } from './classes';\r\nlet soundData: SoundDataOptions;\r\nif (existsSync('.data/soundcloud.data')) {\r\n soundData = JSON.parse(readFileSync('.data/soundcloud.data', 'utf-8'));\r\n}\r\n\r\ninterface SoundDataOptions {\r\n client_id: string;\r\n}\r\n\r\nconst pattern = /^(?:(https?):\\/\\/)?(?:(?:www|m)\\.)?(api\\.soundcloud\\.com|soundcloud\\.com|snd\\.sc)\\/(.*)$/;\r\n/**\r\n * Gets info from a soundcloud url.\r\n *\r\n * ```ts\r\n * let sound = await play.soundcloud('soundcloud url')\r\n *\r\n * // sound.type === \"track\" | \"playlist\" | \"user\"\r\n *\r\n * if (sound.type === \"track\") {\r\n * spot = spot as play.SoundCloudTrack\r\n * // Code with SoundCloud track class.\r\n * }\r\n * ```\r\n * @param url soundcloud url\r\n * @returns A {@link SoundCloudTrack} or {@link SoundCloudPlaylist}\r\n */\r\nexport async function soundcloud(url: string): Promise {\r\n if (!soundData) throw new Error('SoundCloud Data is missing\\nDid you forget to do authorization ?');\r\n const url_ = url.trim();\r\n if (!url_.match(pattern)) throw new Error('This is not a SoundCloud URL');\r\n\r\n const data = await request(\r\n `https://api-v2.soundcloud.com/resolve?url=${url_}&client_id=${soundData.client_id}`\r\n ).catch((err: Error) => err);\r\n\r\n if (data instanceof Error) throw data;\r\n\r\n const json_data = JSON.parse(data);\r\n\r\n if (json_data.kind !== 'track' && json_data.kind !== 'playlist')\r\n throw new Error('This url is out of scope for play-dl.');\r\n\r\n if (json_data.kind === 'track') return new SoundCloudTrack(json_data);\r\n else return new SoundCloudPlaylist(json_data, soundData.client_id);\r\n}\r\n/**\r\n * Type of SoundCloud\r\n */\r\nexport type SoundCloud = SoundCloudTrack | SoundCloudPlaylist;\r\n/**\r\n * Function for searching in SoundCloud\r\n * @param query query to search\r\n * @param type 'tracks' | 'playlists' | 'albums'\r\n * @param limit max no. of results\r\n * @returns Array of SoundCloud type.\r\n */\r\nexport async function so_search(\r\n query: string,\r\n type: 'tracks' | 'playlists' | 'albums',\r\n limit: number = 10\r\n): Promise {\r\n const response = await request(\r\n `https://api-v2.soundcloud.com/search/${type}?q=${query}&client_id=${soundData.client_id}&limit=${limit}`\r\n );\r\n const results: (SoundCloudPlaylist | SoundCloudTrack)[] = [];\r\n const json_data = JSON.parse(response);\r\n json_data.collection.forEach((x: any) => {\r\n if (type === 'tracks') results.push(new SoundCloudTrack(x));\r\n else results.push(new SoundCloudPlaylist(x, soundData.client_id));\r\n });\r\n return results;\r\n}\r\n/**\r\n * Main Function for creating a Stream of soundcloud\r\n * @param url soundcloud url\r\n * @param quality Quality to select from\r\n * @returns SoundCloud Stream\r\n */\r\nexport async function stream(url: string, quality?: number): Promise {\r\n const data = await soundcloud(url);\r\n\r\n if (data instanceof SoundCloudPlaylist) throw new Error(\"Streams can't be created from playlist urls\");\r\n\r\n const HLSformats = parseHlsFormats(data.formats);\r\n if (typeof quality !== 'number') quality = HLSformats.length - 1;\r\n else if (quality <= 0) quality = 0;\r\n else if (quality >= HLSformats.length) quality = HLSformats.length - 1;\r\n const req_url = HLSformats[quality].url + '?client_id=' + soundData.client_id;\r\n const s_data = JSON.parse(await request(req_url));\r\n const type = HLSformats[quality].format.mime_type.startsWith('audio/ogg')\r\n ? StreamType.OggOpus\r\n : StreamType.Arbitrary;\r\n return new SoundCloudStream(s_data.url, type);\r\n}\r\n/**\r\n * Gets Free SoundCloud Client ID.\r\n *\r\n * Use this in beginning of your code to add SoundCloud support.\r\n *\r\n * ```ts\r\n * play.getFreeClientID().then((clientID) => play.setToken({\r\n * soundcloud : {\r\n * client_id : clientID\r\n * }\r\n * }))\r\n * ```\r\n * @returns client ID\r\n */\r\nexport async function getFreeClientID(): Promise {\r\n const data: any = await request('https://soundcloud.com/', {headers: {}}).catch(err => err);\r\n\r\n if (data instanceof Error)\r\n throw new Error(\"Failed to get response from soundcloud.com: \" + data.message);\r\n\r\n const splitted = data.split('