{"version":3,"sources":["../play-dl/Request/index.ts","../play-dl/YouTube/utils/cookie.ts","../play-dl/Request/useragent.ts","../play-dl/YouTube/classes/LiveStream.ts","../play-dl/YouTube/utils/cipher.ts","../play-dl/YouTube/classes/Channel.ts","../play-dl/YouTube/classes/Thumbnail.ts","../play-dl/YouTube/classes/Video.ts","../play-dl/YouTube/classes/Playlist.ts","../play-dl/YouTube/utils/extractor.ts","../play-dl/YouTube/classes/WebmSeeker.ts","../play-dl/YouTube/classes/SeekStream.ts","../play-dl/YouTube/stream.ts","../play-dl/YouTube/utils/parser.ts","../play-dl/YouTube/search.ts","../play-dl/Spotify/classes.ts","../play-dl/Spotify/index.ts","../play-dl/SoundCloud/index.ts","../play-dl/SoundCloud/classes.ts","../play-dl/Deezer/index.ts","../play-dl/Deezer/classes.ts","../play-dl/token.ts","../play-dl/index.ts"],"sourcesContent":["import { IncomingMessage } from 'node:http';\nimport { RequestOptions, request as httpsRequest } from 'node:https';\nimport { URL } from 'node:url';\nimport { BrotliDecompress, Deflate, Gunzip, createGunzip, createBrotliDecompress, createDeflate } from 'node:zlib';\nimport { cookieHeaders, getCookies } from '../YouTube/utils/cookie';\nimport { getRandomUserAgent } from './useragent';\n\ninterface RequestOpts extends RequestOptions {\n body?: string;\n method?: 'GET' | 'POST' | 'HEAD';\n cookies?: boolean;\n cookieJar?: { [key: string]: string };\n}\n\n/**\n * Main module which play-dl uses to make a request to stream url.\n * @param url URL to make https request to\n * @param options Request options for https request\n * @returns IncomingMessage from the request\n */\nexport function request_stream(req_url: string, options: RequestOpts = { method: 'GET' }): Promise {\n return new Promise(async (resolve, reject) => {\n let res = await https_getter(req_url, options).catch((err: Error) => err);\n if (res instanceof Error) {\n reject(res);\n return;\n }\n if (Number(res.statusCode) >= 300 && Number(res.statusCode) < 400) {\n res = await request_stream(res.headers.location as string, options);\n }\n resolve(res);\n });\n}\n/**\n * Makes a request and follows redirects if necessary\n * @param req_url URL to make https request to\n * @param options Request options for https request\n * @returns A promise with the final response object\n */\nfunction internalRequest(req_url: string, options: RequestOpts = { method: 'GET' }): Promise {\n return new Promise(async (resolve, reject) => {\n let res = await https_getter(req_url, options).catch((err: Error) => err);\n if (res instanceof Error) {\n reject(res);\n return;\n }\n if (Number(res.statusCode) >= 300 && Number(res.statusCode) < 400) {\n res = await internalRequest(res.headers.location as string, options);\n } else if (Number(res.statusCode) > 400) {\n reject(new Error(`Got ${res.statusCode} from the request`));\n return;\n }\n resolve(res);\n });\n}\n/**\n * Main module which play-dl uses to make a request\n * @param url URL to make https request to\n * @param options Request options for https request\n * @returns body of that request\n */\nexport function request(req_url: string, options: RequestOpts = { method: 'GET' }): Promise {\n return new Promise(async (resolve, reject) => {\n let cookies_added = false;\n if (options.cookies) {\n let cook = getCookies();\n if (typeof cook === 'string' && options.headers) {\n Object.assign(options.headers, { cookie: cook });\n cookies_added = true;\n }\n }\n if (options.cookieJar) {\n const cookies = [];\n for (const cookie of Object.entries(options.cookieJar)) {\n cookies.push(cookie.join('='));\n }\n\n if (cookies.length !== 0) {\n if (!options.headers) options.headers = {};\n const existingCookies = cookies_added ? `; ${options.headers.cookie}` : '';\n Object.assign(options.headers, { cookie: `${cookies.join('; ')}${existingCookies}` });\n }\n }\n if (options.headers) {\n options.headers = {\n ...options.headers,\n 'accept-encoding': 'gzip, deflate, br',\n 'user-agent': getRandomUserAgent()\n };\n }\n const res = await internalRequest(req_url, options).catch((err: Error) => err);\n if (res instanceof Error) {\n reject(res);\n return;\n }\n if (res.headers && res.headers['set-cookie']) {\n if (options.cookieJar) {\n for (const cookie of res.headers['set-cookie']) {\n const parts = cookie.split(';')[0].trim().split('=');\n options.cookieJar[parts.shift() as string] = parts.join('=');\n }\n }\n if (cookies_added) {\n cookieHeaders(res.headers['set-cookie']);\n }\n }\n const data: string[] = [];\n let decoder: BrotliDecompress | Gunzip | Deflate | undefined = undefined;\n const encoding = res.headers['content-encoding'];\n if (encoding === 'gzip') decoder = createGunzip();\n else if (encoding === 'br') decoder = createBrotliDecompress();\n else if (encoding === 'deflate') decoder = createDeflate();\n\n if (decoder) {\n res.pipe(decoder);\n decoder.setEncoding('utf-8');\n decoder.on('data', (c) => data.push(c));\n decoder.on('end', () => resolve(data.join('')));\n } else {\n res.setEncoding('utf-8');\n res.on('data', (c) => data.push(c));\n res.on('end', () => resolve(data.join('')));\n }\n });\n}\n\nexport function request_resolve_redirect(url: string): Promise {\n return new Promise(async (resolve, reject) => {\n let res = await https_getter(url, { method: 'HEAD' }).catch((err: Error) => err);\n if (res instanceof Error) {\n reject(res);\n return;\n }\n const statusCode = Number(res.statusCode);\n if (statusCode < 300) {\n resolve(url);\n } else if (statusCode < 400) {\n const resolved = await request_resolve_redirect(res.headers.location as string).catch((err) => err);\n if (resolved instanceof Error) {\n reject(resolved);\n return;\n }\n\n resolve(resolved);\n } else {\n reject(new Error(`${res.statusCode}: ${res.statusMessage}, ${url}`));\n }\n });\n}\n\nexport function request_content_length(url: string): Promise {\n return new Promise(async (resolve, reject) => {\n let res = await https_getter(url, { method: 'HEAD' }).catch((err: Error) => err);\n if (res instanceof Error) {\n reject(res);\n return;\n }\n const statusCode = Number(res.statusCode);\n if (statusCode < 300) {\n resolve(Number(res.headers['content-length']));\n } else if (statusCode < 400) {\n const newURL = await request_resolve_redirect(res.headers.location as string).catch((err) => err);\n if (newURL instanceof Error) {\n reject(newURL);\n return;\n }\n\n const res2 = await request_content_length(newURL).catch((err) => err);\n if (res2 instanceof Error) {\n reject(res2);\n return;\n }\n\n resolve(res2);\n } else {\n reject(\n new Error(`Failed to get content length with error: ${res.statusCode}, ${res.statusMessage}, ${url}`)\n );\n }\n });\n}\n\n/**\n * Main module that play-dl uses for making a https request\n * @param req_url URL to make https request to\n * @param options Request options for https request\n * @returns Incoming Message from the https request\n */\nfunction https_getter(req_url: string, options: RequestOpts = {}): Promise {\n return new Promise((resolve, reject) => {\n const s = new URL(req_url);\n options.method ??= 'GET';\n const req_options: RequestOptions = {\n host: s.hostname,\n path: s.pathname + s.search,\n headers: options.headers ?? {},\n method: options.method\n };\n\n const req = httpsRequest(req_options, resolve);\n req.on('error', (err) => {\n reject(err);\n });\n if (options.method === 'POST') req.write(options.body);\n req.end();\n });\n}\n","import { existsSync, readFileSync, writeFileSync } from 'node:fs';\n\nlet youtubeData: youtubeDataOptions;\nif (existsSync('.data/youtube.data')) {\n youtubeData = JSON.parse(readFileSync('.data/youtube.data', 'utf-8'));\n youtubeData.file = true;\n}\n\ninterface youtubeDataOptions {\n cookie?: Object;\n file?: boolean;\n}\n\nexport function getCookies(): undefined | string {\n let result = '';\n if (!youtubeData?.cookie) return undefined;\n for (const [key, value] of Object.entries(youtubeData.cookie)) {\n result += `${key}=${value};`;\n }\n return result;\n}\n\nexport function setCookie(key: string, value: string): boolean {\n if (!youtubeData?.cookie) return false;\n key = key.trim();\n value = value.trim();\n Object.assign(youtubeData.cookie, { [key]: value });\n return true;\n}\n\nexport function uploadCookie() {\n if (youtubeData.cookie && youtubeData.file)\n writeFileSync('.data/youtube.data', JSON.stringify(youtubeData, undefined, 4));\n}\n\nexport function setCookieToken(options: { cookie: string }) {\n let cook = options.cookie;\n let cookie: Object = {};\n cook.split(';').forEach((x) => {\n const arr = x.split('=');\n if (arr.length <= 1) return;\n const key = arr.shift()?.trim() as string;\n const value = arr.join('=').trim();\n Object.assign(cookie, { [key]: value });\n });\n youtubeData = { cookie };\n youtubeData.file = false;\n}\n\n/**\n * Updates cookies locally either in file or in memory.\n *\n * Example\n * ```ts\n * const response = ... // Any https package get function.\n *\n * play.cookieHeaders(response.headers['set-cookie'])\n * ```\n * @param headCookie response headers['set-cookie'] array\n * @returns Nothing\n */\nexport function cookieHeaders(headCookie: string[]): void {\n if (!youtubeData?.cookie) return;\n headCookie.forEach((x: string) => {\n x.split(';').forEach((z) => {\n const arr = z.split('=');\n if (arr.length <= 1) return;\n const key = arr.shift()?.trim() as string;\n const value = arr.join('=').trim();\n setCookie(key, value);\n });\n });\n uploadCookie();\n}\n","import useragents from './useragents.json';\n\nexport function setUserAgent(array: string[]): void {\n useragents.push(...array);\n}\n\nfunction getRandomInt(min: number, max: number): number {\n min = Math.ceil(min);\n max = Math.floor(max);\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\nexport function getRandomUserAgent() {\n const random = getRandomInt(0, useragents.length - 1);\n return useragents[random];\n}\n","import { Readable } from 'node:stream';\nimport { IncomingMessage } from 'node:http';\nimport { parseAudioFormats, StreamOptions, StreamType } from '../stream';\nimport { request, request_stream } from '../../Request';\nimport { video_stream_info } from '../utils/extractor';\nimport { URL } from 'node:url';\n\n/**\n * YouTube Live Stream class for playing audio from Live Stream videos.\n */\nexport class LiveStream {\n /**\n * Readable Stream through which data passes\n */\n stream: Readable;\n /**\n * Type of audio data that we recieved from live stream youtube url.\n */\n type: StreamType;\n /**\n * Incoming message that we recieve.\n *\n * Storing this is essential.\n * This helps to destroy the TCP connection completely if you stopped player in between the stream\n */\n private request?: IncomingMessage;\n /**\n * Timer that creates loop from interval time provided.\n */\n private normal_timer?: Timer;\n /**\n * Timer used to update dash url so as to avoid 404 errors after long hours of streaming.\n *\n * It updates dash_url every 30 minutes.\n */\n private dash_timer: Timer;\n /**\n * Given Dash URL.\n */\n private dash_url: string;\n /**\n * Base URL in dash manifest file.\n */\n private base_url: string;\n /**\n * Interval to fetch data again to dash url.\n */\n private interval: number;\n /**\n * Timer used to update dash url so as to avoid 404 errors after long hours of streaming.\n *\n * It updates dash_url every 30 minutes.\n */\n private video_url: string;\n /**\n * No of segments of data to add in stream before starting to loop\n */\n private precache: number;\n /**\n * Segment sequence number\n */\n private sequence: number;\n /**\n * Live Stream Class Constructor\n * @param dash_url dash manifest URL\n * @param target_interval interval time for fetching dash data again\n * @param video_url Live Stream video url.\n */\n constructor(dash_url: string, interval: number, video_url: string, precache?: number) {\n this.stream = new Readable({ highWaterMark: 5 * 1000 * 1000, read() {} });\n this.type = StreamType.Arbitrary;\n this.sequence = 0;\n this.dash_url = dash_url;\n this.base_url = '';\n this.interval = interval;\n this.video_url = video_url;\n this.precache = precache || 3;\n this.dash_timer = new Timer(() => {\n this.dash_updater();\n this.dash_timer.reuse();\n }, 1800);\n this.stream.on('close', () => {\n this.cleanup();\n });\n this.initialize_dash();\n }\n /**\n * This cleans every used variable in class.\n *\n * This is used to prevent re-use of this class and helping garbage collector to collect it.\n */\n private cleanup() {\n this.normal_timer?.destroy();\n this.dash_timer.destroy();\n this.request?.destroy();\n this.video_url = '';\n this.request = undefined;\n this.dash_url = '';\n this.base_url = '';\n this.interval = 0;\n }\n /**\n * Updates dash url.\n *\n * Used by dash_timer for updating dash_url every 30 minutes.\n */\n private async dash_updater() {\n const info = await video_stream_info(this.video_url);\n if (info.LiveStreamData.dashManifestUrl) this.dash_url = info.LiveStreamData.dashManifestUrl;\n return this.initialize_dash();\n }\n /**\n * Initializes dash after getting dash url.\n *\n * Start if it is first time of initialishing dash function.\n */\n private async initialize_dash() {\n const response = await request(this.dash_url);\n const audioFormat = response\n .split('')[0]\n .split('');\n if (audioFormat[audioFormat.length - 1] === '') audioFormat.pop();\n this.base_url = audioFormat[audioFormat.length - 1].split('')[1].split('')[0];\n await request_stream(`https://${new URL(this.base_url).host}/generate_204`);\n if (this.sequence === 0) {\n const list = audioFormat[audioFormat.length - 1]\n .split('')[1]\n .split('')[0]\n .replaceAll('');\n if (list[list.length - 1] === '') list.pop();\n if (list.length > this.precache) list.splice(0, list.length - this.precache);\n this.sequence = Number(list[0].split('sq/')[1].split('/')[0]);\n this.first_data(list.length);\n }\n }\n /**\n * Used only after initializing dash function first time.\n * @param len Length of data that you want to\n */\n private async first_data(len: number) {\n for (let i = 1; i <= len; i++) {\n await new Promise(async (resolve) => {\n const stream = await request_stream(this.base_url + 'sq/' + this.sequence).catch((err: Error) => err);\n if (stream instanceof Error) {\n this.stream.emit('error', stream);\n return;\n }\n this.request = stream;\n stream.on('data', (c) => {\n this.stream.push(c);\n });\n stream.on('end', () => {\n this.sequence++;\n resolve('');\n });\n stream.once('error', (err) => {\n this.stream.emit('error', err);\n });\n });\n }\n this.normal_timer = new Timer(() => {\n this.loop();\n this.normal_timer?.reuse();\n }, this.interval);\n }\n /**\n * This loops function in Live Stream Class.\n *\n * Gets next segment and push it.\n */\n private loop() {\n return new Promise(async (resolve) => {\n const stream = await request_stream(this.base_url + 'sq/' + this.sequence).catch((err: Error) => err);\n if (stream instanceof Error) {\n this.stream.emit('error', stream);\n return;\n }\n this.request = stream;\n stream.on('data', (c) => {\n this.stream.push(c);\n });\n stream.on('end', () => {\n this.sequence++;\n resolve('');\n });\n stream.once('error', (err) => {\n this.stream.emit('error', err);\n });\n });\n }\n /**\n * Deprecated Functions\n */\n pause() {}\n /**\n * Deprecated Functions\n */\n resume() {}\n}\n/**\n * YouTube Stream Class for playing audio from normal videos.\n */\nexport class Stream {\n /**\n * Readable Stream through which data passes\n */\n stream: Readable;\n /**\n * Type of audio data that we recieved from normal youtube url.\n */\n type: StreamType;\n /**\n * Audio Endpoint Format Url to get data from.\n */\n private url: string;\n /**\n * Used to calculate no of bytes data that we have recieved\n */\n private bytes_count: number;\n /**\n * Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds)\n */\n private per_sec_bytes: number;\n /**\n * Total length of audio file in bytes\n */\n private content_length: number;\n /**\n * YouTube video url. [ Used only for retrying purposes only. ]\n */\n private video_url: string;\n /**\n * Timer for looping data every 265 seconds.\n */\n private timer: Timer;\n /**\n * Quality given by user. [ Used only for retrying purposes only. ]\n */\n private quality: number;\n /**\n * Incoming message that we recieve.\n *\n * Storing this is essential.\n * This helps to destroy the TCP connection completely if you stopped player in between the stream\n */\n private request: IncomingMessage | null;\n /**\n * YouTube Stream Class constructor\n * @param url Audio Endpoint url.\n * @param type Type of Stream\n * @param duration Duration of audio playback [ in seconds ]\n * @param contentLength Total length of Audio file in bytes.\n * @param video_url YouTube video url.\n * @param options Options provided to stream function.\n */\n constructor(\n url: string,\n type: StreamType,\n duration: number,\n contentLength: number,\n video_url: string,\n options: StreamOptions\n ) {\n this.stream = new Readable({ highWaterMark: 5 * 1000 * 1000, read() {} });\n this.url = url;\n this.quality = options.quality as number;\n this.type = type;\n this.bytes_count = 0;\n this.video_url = video_url;\n this.per_sec_bytes = Math.ceil(contentLength / duration);\n this.content_length = contentLength;\n this.request = null;\n this.timer = new Timer(() => {\n this.timer.reuse();\n this.loop();\n }, 265);\n this.stream.on('close', () => {\n this.timer.destroy();\n this.cleanup();\n });\n this.loop();\n }\n /**\n * Retry if we get 404 or 403 Errors.\n */\n private async retry() {\n const info = await video_stream_info(this.video_url);\n const audioFormat = parseAudioFormats(info.format);\n this.url = audioFormat[this.quality].url;\n }\n /**\n * This cleans every used variable in class.\n *\n * This is used to prevent re-use of this class and helping garbage collector to collect it.\n */\n private cleanup() {\n this.request?.destroy();\n this.request = null;\n this.url = '';\n }\n /**\n * Getting data from audio endpoint url and passing it to stream.\n *\n * If 404 or 403 occurs, it will retry again.\n */\n private async loop() {\n if (this.stream.destroyed) {\n this.timer.destroy();\n this.cleanup();\n return;\n }\n const end: number = this.bytes_count + this.per_sec_bytes * 300;\n const stream = await request_stream(this.url, {\n headers: {\n range: `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}`\n }\n }).catch((err: Error) => err);\n if (stream instanceof Error) {\n this.stream.emit('error', stream);\n this.bytes_count = 0;\n this.per_sec_bytes = 0;\n this.cleanup();\n return;\n }\n if (Number(stream.statusCode) >= 400) {\n this.cleanup();\n await this.retry();\n this.timer.reuse();\n this.loop();\n return;\n }\n this.request = stream;\n stream.on('data', (c) => {\n this.stream.push(c);\n });\n\n stream.once('error', async () => {\n this.cleanup();\n await this.retry();\n this.timer.reuse();\n this.loop();\n });\n\n stream.on('data', (chunk: any) => {\n this.bytes_count += chunk.length;\n });\n\n stream.on('end', () => {\n if (end >= this.content_length) {\n this.timer.destroy();\n this.stream.push(null);\n this.cleanup();\n }\n });\n }\n /**\n * Pauses timer.\n * Stops running of loop.\n *\n * Useful if you don't want to get excess data to be stored in stream.\n */\n pause() {\n this.timer.pause();\n }\n /**\n * Resumes timer.\n * Starts running of loop.\n */\n resume() {\n this.timer.resume();\n }\n}\n/**\n * Timer Class.\n *\n * setTimeout + extra features ( re-starting, pausing, resuming ).\n */\nexport class Timer {\n /**\n * Boolean for checking if Timer is destroyed or not.\n */\n private destroyed: boolean;\n /**\n * Boolean for checking if Timer is paused or not.\n */\n private paused: boolean;\n /**\n * setTimeout function\n */\n private timer: NodeJS.Timer;\n /**\n * Callback to be executed once timer finishes.\n */\n private callback: () => void;\n /**\n * Seconds time when it is started.\n */\n private time_start: number;\n /**\n * Total time left.\n */\n private time_left: number;\n /**\n * Total time given by user [ Used only for re-using timer. ]\n */\n private time_total: number;\n /**\n * Constructor for Timer Class\n * @param callback Function to execute when timer is up.\n * @param time Total time to wait before execution.\n */\n constructor(callback: () => void, time: number) {\n this.callback = callback;\n this.time_total = time;\n this.time_left = time;\n this.paused = false;\n this.destroyed = false;\n this.time_start = process.hrtime()[0];\n this.timer = setTimeout(this.callback, this.time_total * 1000);\n }\n /**\n * Pauses Timer\n * @returns Boolean to tell that if it is paused or not.\n */\n pause() {\n if (!this.paused && !this.destroyed) {\n this.paused = true;\n clearTimeout(this.timer);\n this.time_left = this.time_left - (process.hrtime()[0] - this.time_start);\n return true;\n } else return false;\n }\n /**\n * Resumes Timer\n * @returns Boolean to tell that if it is resumed or not.\n */\n resume() {\n if (this.paused && !this.destroyed) {\n this.paused = false;\n this.time_start = process.hrtime()[0];\n this.timer = setTimeout(this.callback, this.time_left * 1000);\n return true;\n } else return false;\n }\n /**\n * Reusing of timer\n * @returns Boolean to tell if it is re-used or not.\n */\n reuse() {\n if (!this.destroyed) {\n clearTimeout(this.timer);\n this.time_left = this.time_total;\n this.paused = false;\n this.time_start = process.hrtime()[0];\n this.timer = setTimeout(this.callback, this.time_total * 1000);\n return true;\n } else return false;\n }\n /**\n * Destroy timer.\n *\n * It can't be used again.\n */\n destroy() {\n clearTimeout(this.timer);\n this.destroyed = true;\n this.callback = () => {};\n this.time_total = 0;\n this.time_left = 0;\n this.paused = false;\n this.time_start = 0;\n }\n}\n","import { URL, URLSearchParams } from 'node:url';\nimport { request } from './../../Request';\n\ninterface formatOptions {\n url?: string;\n sp?: string;\n signatureCipher?: string;\n cipher?: string;\n s?: string;\n}\n// RegExp for various js functions\nconst var_js = '[a-zA-Z_\\\\$]\\\\w*';\nconst singlequote_js = `'[^'\\\\\\\\]*(:?\\\\\\\\[\\\\s\\\\S][^'\\\\\\\\]*)*'`;\nconst duoblequote_js = `\"[^\"\\\\\\\\]*(:?\\\\\\\\[\\\\s\\\\S][^\"\\\\\\\\]*)*\"`;\nconst quote_js = `(?:${singlequote_js}|${duoblequote_js})`;\nconst key_js = `(?:${var_js}|${quote_js})`;\nconst prop_js = `(?:\\\\.${var_js}|\\\\[${quote_js}\\\\])`;\nconst empty_js = `(?:''|\"\")`;\nconst reverse_function = ':function\\\\(a\\\\)\\\\{' + '(?:return )?a\\\\.reverse\\\\(\\\\)' + '\\\\}';\nconst slice_function = ':function\\\\(a,b\\\\)\\\\{' + 'return a\\\\.slice\\\\(b\\\\)' + '\\\\}';\nconst splice_function = ':function\\\\(a,b\\\\)\\\\{' + 'a\\\\.splice\\\\(0,b\\\\)' + '\\\\}';\nconst swap_function =\n ':function\\\\(a,b\\\\)\\\\{' +\n 'var c=a\\\\[0\\\\];a\\\\[0\\\\]=a\\\\[b(?:%a\\\\.length)?\\\\];a\\\\[b(?:%a\\\\.length)?\\\\]=c(?:;return a)?' +\n '\\\\}';\nconst obj_regexp = new RegExp(\n `var (${var_js})=\\\\{((?:(?:${key_js}${reverse_function}|${key_js}${slice_function}|${key_js}${splice_function}|${key_js}${swap_function}),?\\\\r?\\\\n?)+)\\\\};`\n);\nconst function_regexp = new RegExp(\n `${\n `function(?: ${var_js})?\\\\(a\\\\)\\\\{` + `a=a\\\\.split\\\\(${empty_js}\\\\);\\\\s*` + `((?:(?:a=)?${var_js}`\n }${prop_js}\\\\(a,\\\\d+\\\\);)+)` +\n `return a\\\\.join\\\\(${empty_js}\\\\)` +\n `\\\\}`\n);\nconst reverse_regexp = new RegExp(`(?:^|,)(${key_js})${reverse_function}`, 'm');\nconst slice_regexp = new RegExp(`(?:^|,)(${key_js})${slice_function}`, 'm');\nconst splice_regexp = new RegExp(`(?:^|,)(${key_js})${splice_function}`, 'm');\nconst swap_regexp = new RegExp(`(?:^|,)(${key_js})${swap_function}`, 'm');\n/**\n * Function to get tokens from html5player body data.\n * @param body body data of html5player.\n * @returns Array of tokens.\n */\nfunction js_tokens(body: string) {\n const function_action = function_regexp.exec(body);\n const object_action = obj_regexp.exec(body);\n if (!function_action || !object_action) return null;\n\n const object = object_action[1].replace(/\\$/g, '\\\\$');\n const object_body = object_action[2].replace(/\\$/g, '\\\\$');\n const function_body = function_action[1].replace(/\\$/g, '\\\\$');\n\n let result = reverse_regexp.exec(object_body);\n const reverseKey = result && result[1].replace(/\\$/g, '\\\\$').replace(/\\$|^'|^\"|'$|\"$/g, '');\n\n result = slice_regexp.exec(object_body);\n const sliceKey = result && result[1].replace(/\\$/g, '\\\\$').replace(/\\$|^'|^\"|'$|\"$/g, '');\n\n result = splice_regexp.exec(object_body);\n const spliceKey = result && result[1].replace(/\\$/g, '\\\\$').replace(/\\$|^'|^\"|'$|\"$/g, '');\n\n result = swap_regexp.exec(object_body);\n const swapKey = result && result[1].replace(/\\$/g, '\\\\$').replace(/\\$|^'|^\"|'$|\"$/g, '');\n\n const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`;\n const myreg = `(?:a=)?${object}(?:\\\\.${keys}|\\\\['${keys}'\\\\]|\\\\[\"${keys}\"\\\\])` + `\\\\(a,(\\\\d+)\\\\)`;\n const tokenizeRegexp = new RegExp(myreg, 'g');\n const tokens = [];\n while ((result = tokenizeRegexp.exec(function_body)) !== null) {\n const key = result[1] || result[2] || result[3];\n switch (key) {\n case swapKey:\n tokens.push(`sw${result[4]}`);\n break;\n case reverseKey:\n tokens.push('rv');\n break;\n case sliceKey:\n tokens.push(`sl${result[4]}`);\n break;\n case spliceKey:\n tokens.push(`sp${result[4]}`);\n break;\n }\n }\n return tokens;\n}\n/**\n * Function to decipher signature\n * @param tokens Tokens from js_tokens function\n * @param signature Signatured format url\n * @returns deciphered signature\n */\nfunction deciper_signature(tokens: string[], signature: string) {\n let sig = signature.split('');\n const len = tokens.length;\n for (let i = 0; i < len; i++) {\n let token = tokens[i],\n pos;\n switch (token.slice(0, 2)) {\n case 'sw':\n pos = parseInt(token.slice(2));\n swappositions(sig, pos);\n break;\n case 'rv':\n sig.reverse();\n break;\n case 'sl':\n pos = parseInt(token.slice(2));\n sig = sig.slice(pos);\n break;\n case 'sp':\n pos = parseInt(token.slice(2));\n sig.splice(0, pos);\n break;\n }\n }\n return sig.join('');\n}\n/**\n * Function to swap positions in a array\n * @param array array\n * @param position position to switch with first element\n */\nfunction swappositions(array: string[], position: number) {\n const first = array[0];\n array[0] = array[position];\n array[position] = first;\n}\n/**\n * Sets Download url with some extra parameter\n * @param format video fomat\n * @param sig deciphered signature\n * @returns void\n */\nfunction download_url(format: formatOptions, sig: string) {\n if (!format.url) return;\n\n const decoded_url = decodeURIComponent(format.url);\n\n const parsed_url = new URL(decoded_url);\n parsed_url.searchParams.set('ratebypass', 'yes');\n\n if (sig) {\n parsed_url.searchParams.set(format.sp || 'signature', sig);\n }\n format.url = parsed_url.toString();\n}\n/**\n * Main function which handles all queries related to video format deciphering\n * @param formats video formats\n * @param html5player url of html5player\n * @returns array of format.\n */\nexport async function format_decipher(formats: formatOptions[], html5player: string): Promise {\n const body = await request(html5player);\n const tokens = js_tokens(body);\n formats.forEach((format) => {\n const cipher = format.signatureCipher || format.cipher;\n if (cipher) {\n const params = Object.fromEntries(new URLSearchParams(cipher));\n Object.assign(format, params);\n delete format.signatureCipher;\n delete format.cipher;\n }\n if (tokens && format.s) {\n const sig = deciper_signature(tokens, format.s);\n download_url(format, sig);\n delete format.s;\n delete format.sp;\n }\n });\n return formats;\n}\n","export interface ChannelIconInterface {\n /**\n * YouTube Channel Icon URL\n */\n url: string;\n /**\n * YouTube Channel Icon Width\n */\n width: number;\n /**\n * YouTube Channel Icon Height\n */\n height: number;\n}\n/**\n * YouTube Channel Class\n */\nexport class YouTubeChannel {\n /**\n * YouTube Channel Title\n */\n name?: string;\n /**\n * YouTube Channel Verified status.\n */\n verified?: boolean;\n /**\n * YouTube Channel artist if any.\n */\n artist?: boolean;\n /**\n * YouTube Channel ID.\n */\n id?: string;\n /**\n * YouTube Class type. == \"channel\"\n */\n type: 'video' | 'playlist' | 'channel';\n /**\n * YouTube Channel Url\n */\n url?: string;\n /**\n * YouTube Channel Icons data.\n */\n icons?: ChannelIconInterface[];\n /**\n * YouTube Channel subscribers count.\n */\n subscribers?: string;\n /**\n * YouTube Channel Constructor\n * @param data YouTube Channel data that we recieve from basic info or from search\n */\n constructor(data: any = {}) {\n if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);\n this.type = 'channel';\n this.name = data.name || null;\n this.verified = !!data.verified || false;\n this.artist = !!data.artist || false;\n this.id = data.id || null;\n this.url = data.url || null;\n this.icons = data.icons || [{ url: null, width: 0, height: 0 }];\n this.subscribers = data.subscribers || null;\n }\n\n /**\n * Returns channel icon url\n * @param {object} options Icon options\n * @param {number} [options.size=0] Icon size. **Default is 0**\n */\n iconURL(options = { size: 0 }): string | undefined {\n if (typeof options.size !== 'number' || options.size < 0) throw new Error('invalid icon size');\n if (!this.icons?.[0]?.url) return undefined;\n const def = this.icons?.[0]?.url.split('=s')[1].split('-c')[0];\n return this.icons?.[0]?.url.replace(`=s${def}-c`, `=s${options.size}-c`);\n }\n /**\n * Converts Channel Class to channel name.\n * @returns name of channel\n */\n toString(): string {\n return this.name || '';\n }\n /**\n * Converts Channel Class to JSON format\n * @returns json data of the channel\n */\n toJSON(): ChannelJSON {\n return {\n name: this.name,\n verified: this.verified,\n artist: this.artist,\n id: this.id,\n url: this.url,\n icons: this.icons,\n type: this.type,\n subscribers: this.subscribers\n };\n }\n}\n\ninterface ChannelJSON {\n /**\n * YouTube Channel Title\n */\n name?: string;\n /**\n * YouTube Channel Verified status.\n */\n verified?: boolean;\n /**\n * YouTube Channel artist if any.\n */\n artist?: boolean;\n /**\n * YouTube Channel ID.\n */\n id?: string;\n /**\n * Type of Class [ Channel ]\n */\n type: 'video' | 'playlist' | 'channel';\n /**\n * YouTube Channel Url\n */\n url?: string;\n /**\n * YouTube Channel Icon data.\n */\n icons?: ChannelIconInterface[];\n /**\n * YouTube Channel subscribers count.\n */\n subscribers?: string;\n}\n","export class YouTubeThumbnail {\n url: string;\n width: number;\n height: number;\n\n constructor(data: any) {\n this.url = data.url;\n this.width = data.width;\n this.height = data.height;\n }\n\n toJSON() {\n return {\n url: this.url,\n width: this.width,\n height: this.height\n };\n }\n}\n","import { YouTubeChannel } from './Channel';\nimport { YouTubeThumbnail } from './Thumbnail';\n\n/**\n * Licensed music in the video\n * \n * The property names change depending on your region's language.\n */\ninterface VideoMusic {\n song?: string;\n url?: string | null;\n artist?: string;\n album?: string;\n writers?: string;\n licenses?: string;\n}\n\ninterface VideoOptions {\n /**\n * YouTube Video ID\n */\n id?: string;\n /**\n * YouTube video url\n */\n url: string;\n /**\n * YouTube Video title\n */\n title?: string;\n /**\n * YouTube Video description.\n */\n description?: string;\n /**\n * YouTube Video Duration Formatted\n */\n durationRaw: string;\n /**\n * YouTube Video Duration in seconds\n */\n durationInSec: number;\n /**\n * YouTube Video Uploaded Date\n */\n uploadedAt?: string;\n /**\n * 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\n */\n upcoming?: Date | true;\n /**\n * YouTube Views\n */\n views: number;\n /**\n * YouTube Thumbnail Data\n */\n thumbnail?: {\n width: number | undefined;\n height: number | undefined;\n url: string | undefined;\n };\n /**\n * YouTube Video's uploader Channel Data\n */\n channel?: YouTubeChannel;\n /**\n * YouTube Video's likes\n */\n likes: number;\n /**\n * YouTube Video live status\n */\n live: boolean;\n /**\n * YouTube Video private status\n */\n private: boolean;\n /**\n * YouTube Video tags\n */\n tags: string[];\n /**\n * `true` if the video has been identified by the YouTube community as inappropriate or offensive to some audiences and viewer discretion is advised\n */\n discretionAdvised?: boolean;\n /**\n * Gives info about music content in that video.\n * \n * The property names of VideoMusic change depending on your region's language.\n */\n music?: VideoMusic[];\n /**\n * The chapters for this video\n *\n * 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.\n */\n chapters: VideoChapter[];\n}\n\nexport interface VideoChapter {\n /**\n * The title of the chapter\n */\n title: string;\n /**\n * The timestamp of the start of the chapter\n */\n timestamp: string;\n /**\n * The start of the chapter in seconds\n */\n seconds: number;\n /**\n * Thumbnails of the frame at the start of this chapter\n */\n thumbnails: YouTubeThumbnail[];\n}\n\n/**\n * Class for YouTube Video url\n */\nexport class YouTubeVideo {\n /**\n * YouTube Video ID\n */\n id?: string;\n /**\n * YouTube video url\n */\n url: string;\n /**\n * YouTube Class type. == \"video\"\n */\n type: 'video' | 'playlist' | 'channel';\n /**\n * YouTube Video title\n */\n title?: string;\n /**\n * YouTube Video description.\n */\n description?: string;\n /**\n * YouTube Video Duration Formatted\n */\n durationRaw: string;\n /**\n * YouTube Video Duration in seconds\n */\n durationInSec: number;\n /**\n * YouTube Video Uploaded Date\n */\n uploadedAt?: string;\n /**\n * YouTube Live Date\n */\n liveAt?: string;\n /**\n * 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\n */\n upcoming?: Date | true;\n /**\n * YouTube Views\n */\n views: number;\n /**\n * YouTube Thumbnail Data\n */\n thumbnails: YouTubeThumbnail[];\n /**\n * YouTube Video's uploader Channel Data\n */\n channel?: YouTubeChannel;\n /**\n * YouTube Video's likes\n */\n likes: number;\n /**\n * YouTube Video live status\n */\n live: boolean;\n /**\n * YouTube Video private status\n */\n private: boolean;\n /**\n * YouTube Video tags\n */\n tags: string[];\n /**\n * `true` if the video has been identified by the YouTube community as inappropriate or offensive to some audiences and viewer discretion is advised\n */\n discretionAdvised?: boolean;\n /**\n * Gives info about music content in that video.\n */\n music?: VideoMusic[];\n /**\n * The chapters for this video\n *\n * 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.\n */\n chapters: VideoChapter[];\n /**\n * Constructor for YouTube Video Class\n * @param data JSON parsed data.\n */\n constructor(data: any) {\n if (!data) throw new Error(`Can not initiate ${this.constructor.name} without data`);\n\n this.id = data.id || undefined;\n this.url = `https://www.youtube.com/watch?v=${this.id}`;\n this.type = 'video';\n this.title = data.title || undefined;\n this.description = data.description || undefined;\n this.durationRaw = data.duration_raw || '0:00';\n this.durationInSec = (data.duration < 0 ? 0 : data.duration) || 0;\n this.uploadedAt = data.uploadedAt || undefined;\n this.liveAt = data.liveAt || undefined;\n this.upcoming = data.upcoming;\n this.views = parseInt(data.views) || 0;\n const thumbnails = [];\n for (const thumb of data.thumbnails) {\n thumbnails.push(new YouTubeThumbnail(thumb));\n }\n this.thumbnails = thumbnails || [];\n this.channel = new YouTubeChannel(data.channel) || {};\n this.likes = data.likes || 0;\n this.live = !!data.live;\n this.private = !!data.private;\n this.tags = data.tags || [];\n this.discretionAdvised = data.discretionAdvised ?? undefined;\n this.music = data.music || [];\n this.chapters = data.chapters || [];\n }\n /**\n * Converts class to title name of video.\n * @returns Title name\n */\n toString(): string {\n return this.url || '';\n }\n /**\n * Converts class to JSON data\n * @returns JSON data.\n */\n toJSON(): VideoOptions {\n return {\n id: this.id,\n url: this.url,\n title: this.title,\n description: this.description,\n durationInSec: this.durationInSec,\n durationRaw: this.durationRaw,\n uploadedAt: this.uploadedAt,\n thumbnail: this.thumbnails[this.thumbnails.length - 1].toJSON() || this.thumbnails,\n channel: this.channel,\n views: this.views,\n tags: this.tags,\n likes: this.likes,\n live: this.live,\n private: this.private,\n discretionAdvised: this.discretionAdvised,\n music: this.music,\n chapters: this.chapters\n };\n }\n}\n","import { getPlaylistVideos, getContinuationToken } from '../utils/extractor';\nimport { request } from '../../Request';\nimport { YouTubeChannel } from './Channel';\nimport { YouTubeVideo } from './Video';\nimport { YouTubeThumbnail } from './Thumbnail';\nconst BASE_API = 'https://www.youtube.com/youtubei/v1/browse?key=';\n/**\n * YouTube Playlist Class containing vital informations about playlist.\n */\nexport class YouTubePlayList {\n /**\n * YouTube Playlist ID\n */\n id?: string;\n /**\n * YouTube Playlist Name\n */\n title?: string;\n /**\n * YouTube Class type. == \"playlist\"\n */\n type: 'video' | 'playlist' | 'channel';\n /**\n * Total no of videos in that playlist\n */\n videoCount?: number;\n /**\n * Time when playlist was last updated\n */\n lastUpdate?: string;\n /**\n * Total views of that playlist\n */\n views?: number;\n /**\n * YouTube Playlist url\n */\n url?: string;\n /**\n * YouTube Playlist url with starting video url.\n */\n link?: string;\n /**\n * YouTube Playlist channel data\n */\n channel?: YouTubeChannel;\n /**\n * YouTube Playlist thumbnail Data\n */\n thumbnail?: YouTubeThumbnail;\n /**\n * Videos array containing data of first 100 videos\n */\n private videos?: YouTubeVideo[];\n /**\n * Map contaning data of all fetched videos\n */\n private fetched_videos: Map;\n /**\n * Token containing API key, Token, ClientVersion.\n */\n private _continuation: {\n api?: string;\n token?: string;\n clientVersion?: string;\n } = {};\n /**\n * Total no of pages count.\n */\n private __count: number;\n /**\n * Constructor for YouTube Playlist Class\n * @param data Json Parsed YouTube Playlist data\n * @param searchResult If the data is from search or not\n */\n constructor(data: any, searchResult = false) {\n if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);\n this.__count = 0;\n this.fetched_videos = new Map();\n this.type = 'playlist';\n if (searchResult) this.__patchSearch(data);\n else this.__patch(data);\n }\n /**\n * Updates variable according to a normal data.\n * @param data Json Parsed YouTube Playlist data\n */\n private __patch(data: any) {\n this.id = data.id || undefined;\n this.url = data.url || undefined;\n this.title = data.title || undefined;\n this.videoCount = data.videoCount || 0;\n this.lastUpdate = data.lastUpdate || undefined;\n this.views = data.views || 0;\n this.link = data.link || undefined;\n this.channel = new YouTubeChannel(data.channel) || undefined;\n this.thumbnail = data.thumbnail ? new YouTubeThumbnail(data.thumbnail) : undefined;\n this.videos = data.videos || [];\n this.__count++;\n this.fetched_videos.set(`${this.__count}`, this.videos as YouTubeVideo[]);\n this._continuation.api = data.continuation?.api ?? undefined;\n this._continuation.token = data.continuation?.token ?? undefined;\n this._continuation.clientVersion = data.continuation?.clientVersion ?? '';\n }\n /**\n * Updates variable according to a searched data.\n * @param data Json Parsed YouTube Playlist data\n */\n private __patchSearch(data: any) {\n this.id = data.id || undefined;\n this.url = this.id ? `https://www.youtube.com/playlist?list=${this.id}` : undefined;\n this.title = data.title || undefined;\n this.thumbnail = new YouTubeThumbnail(data.thumbnail) || undefined;\n this.channel = data.channel || undefined;\n this.videos = [];\n this.videoCount = data.videos || 0;\n this.link = undefined;\n this.lastUpdate = undefined;\n this.views = 0;\n }\n /**\n * Parses next segment of videos from playlist and returns parsed data.\n * @param limit Total no of videos to parse.\n *\n * Default = Infinity\n * @returns Array of YouTube Video Class\n */\n async next(limit = Infinity): Promise {\n if (!this._continuation || !this._continuation.token) return [];\n\n const nextPage = await request(`${BASE_API}${this._continuation.api}&prettyPrint=false`, {\n method: 'POST',\n body: JSON.stringify({\n continuation: this._continuation.token,\n context: {\n client: {\n utcOffsetMinutes: 0,\n gl: 'US',\n hl: 'en',\n clientName: 'WEB',\n clientVersion: this._continuation.clientVersion\n },\n user: {},\n request: {}\n }\n })\n });\n\n const contents =\n JSON.parse(nextPage)?.onResponseReceivedActions[0]?.appendContinuationItemsAction?.continuationItems;\n if (!contents) return [];\n\n const playlist_videos = getPlaylistVideos(contents, limit);\n this.fetched_videos.set(`${this.__count}`, playlist_videos);\n this._continuation.token = getContinuationToken(contents);\n return playlist_videos;\n }\n /**\n * Fetches remaining data from playlist\n *\n * For fetching and getting all songs data, see `total_pages` property.\n * @param max Max no of videos to fetch\n *\n * Default = Infinity\n * @returns\n */\n async fetch(max = Infinity): Promise {\n const continuation = this._continuation.token;\n if (!continuation) return this;\n if (max < 1) max = Infinity;\n\n while (typeof this._continuation.token === 'string' && this._continuation.token.length) {\n this.__count++;\n const res = await this.next();\n max -= res.length;\n if (max <= 0) break;\n if (!res.length) break;\n }\n\n return this;\n }\n /**\n * YouTube Playlists are divided into pages.\n *\n * For example, if you want to get 101 - 200 songs\n *\n * ```ts\n * const playlist = await play.playlist_info('playlist url')\n *\n * await playlist.fetch()\n *\n * const result = playlist.page(2)\n * ```\n * @param number Page number\n * @returns Array of YouTube Video Class\n * @see {@link YouTubePlayList.all_videos}\n */\n page(number: number): YouTubeVideo[] {\n if (!number) throw new Error('Page number is not provided');\n if (!this.fetched_videos.has(`${number}`)) throw new Error('Given Page number is invalid');\n return this.fetched_videos.get(`${number}`) as YouTubeVideo[];\n }\n /**\n * Gets total number of pages in that playlist class.\n * @see {@link YouTubePlayList.all_videos}\n */\n get total_pages() {\n return this.fetched_videos.size;\n }\n /**\n * This tells total number of videos that have been fetched so far.\n *\n * This can be equal to videosCount if all videos in playlist have been fetched and they are not hidden.\n */\n get total_videos() {\n const page_number: number = this.total_pages;\n return (page_number - 1) * 100 + (this.fetched_videos.get(`${page_number}`) as YouTubeVideo[]).length;\n }\n /**\n * Fetches all the videos in the playlist and returns them\n *\n * ```ts\n * const playlist = await play.playlist_info('playlist url')\n *\n * const videos = await playlist.all_videos()\n * ```\n * @returns An array of {@link YouTubeVideo} objects\n * @see {@link YouTubePlayList.fetch}\n */\n async all_videos(): Promise {\n await this.fetch();\n\n const videos = [];\n\n for (const page of this.fetched_videos.values()) videos.push(...page);\n\n return videos;\n }\n /**\n * Converts Playlist Class to a json parsed data.\n * @returns\n */\n toJSON(): PlaylistJSON {\n return {\n id: this.id,\n title: this.title,\n thumbnail: this.thumbnail?.toJSON() || this.thumbnail,\n channel: this.channel,\n url: this.url,\n videos: this.videos\n };\n }\n}\n\ninterface PlaylistJSON {\n /**\n * YouTube Playlist ID\n */\n id?: string;\n /**\n * YouTube Playlist Name\n */\n title?: string;\n /**\n * Total no of videos in that playlist\n */\n videoCount?: number;\n /**\n * Time when playlist was last updated\n */\n lastUpdate?: string;\n /**\n * Total views of that playlist\n */\n views?: number;\n /**\n * YouTube Playlist url\n */\n url?: string;\n /**\n * YouTube Playlist url with starting video url.\n */\n link?: string;\n /**\n * YouTube Playlist channel data\n */\n channel?: YouTubeChannel;\n /**\n * YouTube Playlist thumbnail Data\n */\n thumbnail?: {\n width: number | undefined;\n height: number | undefined;\n url: string | undefined;\n };\n /**\n * first 100 videos in that playlist\n */\n videos?: YouTubeVideo[];\n}\n","import { request } from './../../Request/index';\nimport { format_decipher } from './cipher';\nimport { VideoChapter, YouTubeVideo } from '../classes/Video';\nimport { YouTubePlayList } from '../classes/Playlist';\nimport { InfoData, StreamInfoData } from './constants';\nimport { URL, URLSearchParams } from 'node:url';\nimport { parseAudioFormats } from '../stream';\n\ninterface InfoOptions {\n htmldata?: boolean;\n language?: string;\n}\n\ninterface PlaylistOptions {\n incomplete?: boolean;\n language?: string;\n}\n\nconst video_id_pattern = /^[a-zA-Z\\d_-]{11,12}$/;\nconst playlist_id_pattern = /^(PL|UU|LL|RD|OL)[a-zA-Z\\d_-]{10,}$/;\nconst DEFAULT_API_KEY = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';\nconst video_pattern =\n /^((?:https?:)?\\/\\/)?(?:(?:www|m|music)\\.)?((?:youtube\\.com|youtu.be))(\\/(?:[\\w\\-]+\\?v=|shorts\\/|embed\\/|live\\/|v\\/)?)([\\w\\-]+)(\\S+)?$/;\nconst playlist_pattern =\n /^((?:https?:)?\\/\\/)?(?:(?:www|m|music)\\.)?((?:youtube\\.com|youtu.be))\\/(?:(playlist|watch))?(.*)?((\\?|\\&)list=)(PL|UU|LL|RD|OL)[a-zA-Z\\d_-]{10,}(&.*)?$/;\n/**\n * Validate YouTube URL or ID.\n *\n * **CAUTION :** If your search word is 11 or 12 characters long, you might get it validated as video ID.\n *\n * To avoid above, add one more condition to yt_validate\n * ```ts\n * if (url.startsWith('https') && yt_validate(url) === 'video') {\n * // YouTube Video Url.\n * }\n * ```\n * @param url YouTube URL OR ID\n * @returns\n * ```\n * 'playlist' | 'video' | 'search' | false\n * ```\n */\nexport function yt_validate(url: string): 'playlist' | 'video' | 'search' | false {\n const url_ = url.trim();\n if (url_.indexOf('list=') === -1) {\n if (url_.startsWith('https')) {\n if (url_.match(video_pattern)) {\n let id: string;\n if (url_.includes('youtu.be/')) id = url_.split('youtu.be/')[1].split(/(\\?|\\/|&)/)[0];\n else if (url_.includes('youtube.com/embed/'))\n id = url_.split('youtube.com/embed/')[1].split(/(\\?|\\/|&)/)[0];\n else if (url_.includes('youtube.com/shorts/'))\n id = url_.split('youtube.com/shorts/')[1].split(/(\\?|\\/|&)/)[0];\n else id = url_.split('watch?v=')[1]?.split(/(\\?|\\/|&)/)[0];\n if (id?.match(video_id_pattern)) return 'video';\n else return false;\n } else return false;\n } else {\n if (url_.match(video_id_pattern)) return 'video';\n else if (url_.match(playlist_id_pattern)) return 'playlist';\n else return 'search';\n }\n } else {\n if (!url_.match(playlist_pattern)) return yt_validate(url_.replace(/(\\?|\\&)list=[^&]*/, ''));\n else return 'playlist';\n }\n}\n/**\n * Extracts the video ID from a YouTube URL.\n *\n * Will return the value of `urlOrId` if it looks like a video ID.\n * @param urlOrId A YouTube URL or video ID\n * @returns the video ID or `false` if it can't find a video ID.\n */\nfunction extractVideoId(urlOrId: string): string | false {\n if (urlOrId.startsWith('https://') && urlOrId.match(video_pattern)) {\n let id: string;\n if (urlOrId.includes('youtu.be/')) {\n id = urlOrId.split('youtu.be/')[1].split(/(\\?|\\/|&)/)[0];\n } else if (urlOrId.includes('youtube.com/embed/')) {\n id = urlOrId.split('youtube.com/embed/')[1].split(/(\\?|\\/|&)/)[0];\n } else if (urlOrId.includes('youtube.com/shorts/')) {\n id = urlOrId.split('youtube.com/shorts/')[1].split(/(\\?|\\/|&)/)[0];\n } else if (urlOrId.includes('youtube.com/live/')) {\n id = urlOrId.split('youtube.com/live/')[1].split(/(\\?|\\/|&)/)[0];\n } else {\n id = (urlOrId.split('watch?v=')[1] ?? urlOrId.split('&v=')[1]).split(/(\\?|\\/|&)/)[0];\n }\n\n if (id.match(video_id_pattern)) return id;\n } else if (urlOrId.match(video_id_pattern)) {\n return urlOrId;\n }\n\n return false;\n}\n/**\n * Extract ID of YouTube url.\n * @param url ID or url of YouTube\n * @returns ID of video or playlist.\n */\nexport function extractID(url: string): string {\n const check = yt_validate(url);\n if (!check || check === 'search') throw new Error('This is not a YouTube url or videoId or PlaylistID');\n const url_ = url.trim();\n if (url_.startsWith('https')) {\n if (url_.indexOf('list=') === -1) {\n const video_id = extractVideoId(url_);\n if (!video_id) throw new Error('This is not a YouTube url or videoId or PlaylistID');\n return video_id;\n } else {\n return url_.split('list=')[1].split('&')[0];\n }\n } else return url_;\n}\n/**\n * Basic function to get data from a YouTube url or ID.\n *\n * Example\n * ```ts\n * const video = await play.video_basic_info('youtube video url')\n *\n * const res = ... // Any https package get function.\n *\n * const video = await play.video_basic_info(res.body, { htmldata : true })\n * ```\n * @param url YouTube url or ID or html body data\n * @param options Video Info Options\n * - `boolean` htmldata : given data is html data or not\n * @returns Video Basic Info {@link InfoData}.\n */\nexport async function video_basic_info(url: string, options: InfoOptions = {}): Promise {\n if (typeof url !== 'string') throw new Error('url parameter is not a URL string or a string of HTML');\n const url_ = url.trim();\n let body: string;\n const cookieJar = {};\n if (options.htmldata) {\n body = url_;\n } else {\n const video_id = extractVideoId(url_);\n if (!video_id) throw new Error('This is not a YouTube Watch URL');\n const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`;\n body = await request(new_url, {\n headers: {\n 'accept-language': options.language || 'en-US;q=0.9'\n },\n cookies: true,\n cookieJar\n });\n }\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\n const player_data = body\n .split('var ytInitialPlayerResponse = ')?.[1]\n ?.split(';')[0]\n .split(/(?<=}}});\\s*(var|const|let)\\s/)[0];\n if (!player_data) throw new Error('Initial Player Response Data is undefined.');\n const initial_data = body\n .split('var ytInitialData = ')?.[1]\n ?.split(';')[0]\n .split(/;\\s*(var|const|let)\\s/)[0];\n if (!initial_data) throw new Error('Initial Response Data is undefined.');\n const player_response = JSON.parse(player_data);\n const initial_response = JSON.parse(initial_data);\n const vid = player_response.videoDetails;\n\n let discretionAdvised = false;\n let upcoming = false;\n if (player_response.playabilityStatus.status !== 'OK') {\n if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') {\n if (options.htmldata)\n throw new Error(\n `Accepting the viewer discretion is not supported when using htmldata, video: ${vid.videoId}`\n );\n discretionAdvised = true;\n const cookies =\n initial_response.topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton\n .buttonRenderer.command.saveConsentAction;\n if (cookies) {\n Object.assign(cookieJar, {\n VISITOR_INFO1_LIVE: cookies.visitorCookie,\n CONSENT: cookies.consentCookie\n });\n }\n\n const updatedValues = await acceptViewerDiscretion(vid.videoId, cookieJar, body, true);\n player_response.streamingData = updatedValues.streamingData;\n initial_response.contents.twoColumnWatchNextResults.secondaryResults = updatedValues.relatedVideos;\n } else if (player_response.playabilityStatus.status === 'LIVE_STREAM_OFFLINE') upcoming = true;\n else\n throw new Error(\n `While getting info from url\\n${\n player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ??\n player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText ??\n player_response.playabilityStatus.reason\n }`\n );\n }\n const ownerInfo =\n initial_response.contents.twoColumnWatchNextResults.results?.results?.contents[1]?.videoSecondaryInfoRenderer\n ?.owner?.videoOwnerRenderer;\n const badge = ownerInfo?.badges?.[0]?.metadataBadgeRenderer?.style?.toLowerCase();\n const html5player = `https://www.youtube.com${body.split('\"jsUrl\":\"')[1].split('\"')[0]}`;\n const related: string[] = [];\n initial_response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results.forEach(\n (res: any) => {\n if (res.compactVideoRenderer)\n related.push(`https://www.youtube.com/watch?v=${res.compactVideoRenderer.videoId}`);\n if (res.itemSectionRenderer?.contents)\n res.itemSectionRenderer.contents.forEach((x: any) => {\n if (x.compactVideoRenderer)\n related.push(`https://www.youtube.com/watch?v=${x.compactVideoRenderer.videoId}`);\n });\n }\n );\n const microformat = player_response.microformat.playerMicroformatRenderer;\n const musicInfo = initial_response.engagementPanels.find((item: any) => item?.engagementPanelSectionListRenderer?.panelIdentifier == 'engagement-panel-structured-description')?.engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items\n .find((el: any) => el.videoDescriptionMusicSectionRenderer)?.videoDescriptionMusicSectionRenderer.carouselLockups;\n\n const music: any[] = [];\n if (musicInfo) {\n musicInfo.forEach((x: any) => {\n if (!x.carouselLockupRenderer) return;\n const row = x.carouselLockupRenderer;\n\n const song = row.videoLockup?.compactVideoRenderer.title.simpleText ?? row.videoLockup?.compactVideoRenderer.title.runs?.find((x:any) => x.text)?.text;\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 ?? \"\"]);\n const contents = Object.fromEntries(metadata ?? {});\n const id = row.videoLockup?.compactVideoRenderer.navigationEndpoint?.watchEndpoint.videoId\n ?? row.infoRows?.find((x: any) => x.infoRowRenderer.title.simpleText.toLowerCase() == \"song\")?.infoRowRenderer.defaultMetadata.runs?.find((x: any) => x.navigationEndpoint)?.navigationEndpoint.watchEndpoint?.videoId;\n\n music.push({song, url: id ? `https://www.youtube.com/watch?v=${id}` : null, ...contents})\n });\n }\n const rawChapters =\n initial_response.playerOverlays.playerOverlayRenderer.decoratedPlayerBarRenderer?.decoratedPlayerBarRenderer.playerBar?.multiMarkersPlayerBarRenderer.markersMap?.find(\n (m: any) => m.key === 'DESCRIPTION_CHAPTERS'\n )?.value?.chapters;\n const chapters: VideoChapter[] = [];\n if (rawChapters) {\n for (const { chapterRenderer } of rawChapters) {\n chapters.push({\n title: chapterRenderer.title.simpleText,\n timestamp: parseSeconds(chapterRenderer.timeRangeStartMillis / 1000),\n seconds: chapterRenderer.timeRangeStartMillis / 1000,\n thumbnails: chapterRenderer.thumbnail.thumbnails\n });\n }\n }\n let upcomingDate;\n if (upcoming) {\n if (microformat.liveBroadcastDetails.startTimestamp)\n upcomingDate = new Date(microformat.liveBroadcastDetails.startTimestamp);\n else {\n const timestamp =\n player_response.playabilityStatus.liveStreamability.liveStreamabilityRenderer.offlineSlate\n .liveStreamOfflineSlateRenderer.scheduledStartTime;\n upcomingDate = new Date(parseInt(timestamp) * 1000);\n }\n }\n\n const likeRenderer = initial_response.contents.twoColumnWatchNextResults.results.results.contents\n .find((content: any) => content.videoPrimaryInfoRenderer)\n ?.videoPrimaryInfoRenderer.videoActions.menuRenderer.topLevelButtons?.find(\n (button: any) => button.toggleButtonRenderer?.defaultIcon.iconType === 'LIKE' || button.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultIcon.iconType === 'LIKE'\n )\n\n const video_details = new YouTubeVideo({\n id: vid.videoId,\n title: vid.title,\n description: vid.shortDescription,\n duration: Number(vid.lengthSeconds),\n duration_raw: parseSeconds(vid.lengthSeconds),\n uploadedAt: microformat.publishDate,\n liveAt: microformat.liveBroadcastDetails?.startTimestamp,\n upcoming: upcomingDate,\n thumbnails: vid.thumbnail.thumbnails,\n channel: {\n name: vid.author,\n id: vid.channelId,\n url: `https://www.youtube.com/channel/${vid.channelId}`,\n verified: Boolean(badge?.includes('verified')),\n artist: Boolean(badge?.includes('artist')),\n icons: ownerInfo?.thumbnail?.thumbnails || undefined\n },\n views: vid.viewCount,\n tags: vid.keywords,\n likes: parseInt(\n likeRenderer?.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\\D+/g, '') ?? \n likeRenderer?.segmentedLikeDislikeButtonRenderer?.likeButton.toggleButtonRenderer?.defaultText.accessibility?.accessibilityData.label.replace(/\\D+/g, '') ?? 0\n ),\n live: vid.isLiveContent,\n private: vid.isPrivate,\n discretionAdvised,\n music,\n chapters\n });\n let format = [];\n if (!upcoming) {\n format.push(...(player_response.streamingData.formats ?? []));\n format.push(...(player_response.streamingData.adaptiveFormats ?? []));\n\n // get the formats for the android player for legacy videos\n // fixes the stream being closed because not enough data\n // arrived in time for ffmpeg to be able to extract audio data\n if (parseAudioFormats(format).length === 0 && !options.htmldata) {\n format = await getAndroidFormats(vid.videoId, cookieJar, body);\n }\n }\n const LiveStreamData = {\n isLive: video_details.live,\n dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null,\n hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null\n };\n return {\n LiveStreamData,\n html5player,\n format,\n video_details,\n related_videos: related\n };\n}\n/**\n * Gets the data required for streaming from YouTube url, ID or html body data and deciphers it.\n *\n * Internal function used by {@link stream} instead of {@link video_info}\n * because it only extracts the information required for streaming.\n *\n * @param url YouTube url or ID or html body data\n * @param options Video Info Options\n * - `boolean` htmldata : given data is html data or not\n * @returns Deciphered Video Info {@link StreamInfoData}.\n */\nexport async function video_stream_info(url: string, options: InfoOptions = {}): Promise {\n if (typeof url !== 'string') throw new Error('url parameter is not a URL string or a string of HTML');\n let body: string;\n const cookieJar = {};\n if (options.htmldata) {\n body = url;\n } else {\n const video_id = extractVideoId(url);\n if (!video_id) throw new Error('This is not a YouTube Watch URL');\n const new_url = `https://www.youtube.com/watch?v=${video_id}&has_verified=1`;\n body = await request(new_url, {\n headers: { 'accept-language': 'en-US,en;q=0.9' },\n cookies: true,\n cookieJar\n });\n }\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\n const player_data = body\n .split('var ytInitialPlayerResponse = ')?.[1]\n ?.split(';')[0]\n .split(/(?<=}}});\\s*(var|const|let)\\s/)[0];\n if (!player_data) throw new Error('Initial Player Response Data is undefined.');\n const player_response = JSON.parse(player_data);\n let upcoming = false;\n if (player_response.playabilityStatus.status !== 'OK') {\n if (player_response.playabilityStatus.status === 'CONTENT_CHECK_REQUIRED') {\n if (options.htmldata)\n throw new Error(\n `Accepting the viewer discretion is not supported when using htmldata, video: ${player_response.videoDetails.videoId}`\n );\n\n const initial_data = body\n .split('var ytInitialData = ')?.[1]\n ?.split(';')[0]\n .split(/;\\s*(var|const|let)\\s/)[0];\n if (!initial_data) throw new Error('Initial Response Data is undefined.');\n\n const cookies =\n JSON.parse(initial_data).topbar.desktopTopbarRenderer.interstitial?.consentBumpV2Renderer.agreeButton\n .buttonRenderer.command.saveConsentAction;\n if (cookies) {\n Object.assign(cookieJar, {\n VISITOR_INFO1_LIVE: cookies.visitorCookie,\n CONSENT: cookies.consentCookie\n });\n }\n\n const updatedValues = await acceptViewerDiscretion(\n player_response.videoDetails.videoId,\n cookieJar,\n body,\n false\n );\n player_response.streamingData = updatedValues.streamingData;\n } else if (player_response.playabilityStatus.status === 'LIVE_STREAM_OFFLINE') upcoming = true;\n else\n throw new Error(\n `While getting info from url\\n${\n player_response.playabilityStatus.errorScreen.playerErrorMessageRenderer?.reason.simpleText ??\n player_response.playabilityStatus.errorScreen.playerKavRenderer?.reason.simpleText ??\n player_response.playabilityStatus.reason\n }`\n );\n }\n const html5player = `https://www.youtube.com${body.split('\"jsUrl\":\"')[1].split('\"')[0]}`;\n const duration = Number(player_response.videoDetails.lengthSeconds);\n const video_details = {\n url: `https://www.youtube.com/watch?v=${player_response.videoDetails.videoId}`,\n durationInSec: (duration < 0 ? 0 : duration) || 0\n };\n let format = [];\n if (!upcoming) {\n format.push(...(player_response.streamingData.formats ?? []));\n format.push(...(player_response.streamingData.adaptiveFormats ?? []));\n\n // get the formats for the android player for legacy videos\n // fixes the stream being closed because not enough data\n // arrived in time for ffmpeg to be able to extract audio data\n if (parseAudioFormats(format).length === 0 && !options.htmldata) {\n format = await getAndroidFormats(player_response.videoDetails.videoId, cookieJar, body);\n }\n }\n\n const LiveStreamData = {\n isLive: player_response.videoDetails.isLiveContent,\n dashManifestUrl: player_response.streamingData?.dashManifestUrl ?? null,\n hlsManifestUrl: player_response.streamingData?.hlsManifestUrl ?? null\n };\n return await decipher_info(\n {\n LiveStreamData,\n html5player,\n format,\n video_details\n },\n true\n );\n}\n/**\n * Function to convert seconds to [hour : minutes : seconds] format\n * @param seconds seconds to convert\n * @returns [hour : minutes : seconds] format\n */\nfunction parseSeconds(seconds: number): string {\n const d = Number(seconds);\n const h = Math.floor(d / 3600);\n const m = Math.floor((d % 3600) / 60);\n const s = Math.floor((d % 3600) % 60);\n\n const hDisplay = h > 0 ? (h < 10 ? `0${h}` : h) + ':' : '';\n const mDisplay = m > 0 ? (m < 10 ? `0${m}` : m) + ':' : '00:';\n const sDisplay = s > 0 ? (s < 10 ? `0${s}` : s) : '00';\n return hDisplay + mDisplay + sDisplay;\n}\n/**\n * Gets data from YouTube url or ID or html body data and deciphers it.\n * ```\n * video_basic_info + decipher_info = video_info\n * ```\n *\n * Example\n * ```ts\n * const video = await play.video_info('youtube video url')\n *\n * const res = ... // Any https package get function.\n *\n * const video = await play.video_info(res.body, { htmldata : true })\n * ```\n * @param url YouTube url or ID or html body data\n * @param options Video Info Options\n * - `boolean` htmldata : given data is html data or not\n * @returns Deciphered Video Info {@link InfoData}.\n */\nexport async function video_info(url: string, options: InfoOptions = {}): Promise {\n const data = await video_basic_info(url.trim(), options);\n return await decipher_info(data);\n}\n/**\n * Function uses data from video_basic_info and deciphers it if it contains signatures.\n * @param data Data - {@link InfoData}\n * @param audio_only `boolean` - To decipher only audio formats only.\n * @returns Deciphered Video Info {@link InfoData}\n */\nexport async function decipher_info(\n data: T,\n audio_only: boolean = false\n): Promise {\n if (\n data.LiveStreamData.isLive === true &&\n data.LiveStreamData.dashManifestUrl !== null &&\n data.video_details.durationInSec === 0\n ) {\n return data;\n } else if (data.format.length > 0 && (data.format[0].signatureCipher || data.format[0].cipher)) {\n if (audio_only) data.format = parseAudioFormats(data.format);\n data.format = await format_decipher(data.format, data.html5player);\n return data;\n } else return data;\n}\n/**\n * Gets YouTube playlist info from a playlist url.\n *\n * Example\n * ```ts\n * const playlist = await play.playlist_info('youtube playlist url')\n *\n * const playlist = await play.playlist_info('youtube playlist url', { incomplete : true })\n * ```\n * @param url Playlist URL\n * @param options Playlist Info Options\n * - `boolean` incomplete : When this is set to `false` (default) this function will throw an error\n * if the playlist contains hidden videos.\n * If it is set to `true`, it parses the playlist skipping the hidden videos,\n * only visible videos are included in the resulting {@link YouTubePlaylist}.\n *\n * @returns YouTube Playlist\n */\nexport async function playlist_info(url: string, options: PlaylistOptions = {}): Promise {\n if (!url || typeof url !== 'string') throw new Error(`Expected playlist url, received ${typeof url}!`);\n let url_ = url.trim();\n if (!url_.startsWith('https')) url_ = `https://www.youtube.com/playlist?list=${url_}`;\n if (url_.indexOf('list=') === -1) throw new Error('This is not a Playlist URL');\n\n if (url_.includes('music.youtube.com')) {\n const urlObj = new URL(url_);\n urlObj.hostname = 'www.youtube.com';\n url_ = urlObj.toString();\n }\n\n const body = await request(url_, {\n headers: {\n 'accept-language': options.language || 'en-US;q=0.9'\n }\n });\n if (body.indexOf('Our systems have detected unusual traffic from your computer network.') !== -1)\n throw new Error('Captcha page: YouTube has detected that you are a bot!');\n const response = JSON.parse(\n body\n .split('var ytInitialData = ')[1]\n .split(';')[0]\n .split(/;\\s*(var|const|let)\\s/)[0]\n );\n if (response.alerts) {\n if (response.alerts[0].alertWithButtonRenderer?.type === 'INFO') {\n if (!options.incomplete)\n throw new Error(\n `While parsing playlist url\\n${response.alerts[0].alertWithButtonRenderer.text.simpleText}`\n );\n } else if (response.alerts[0].alertRenderer?.type === 'ERROR')\n throw new Error(`While parsing playlist url\\n${response.alerts[0].alertRenderer.text.runs[0].text}`);\n else throw new Error('While parsing playlist url\\nUnknown Playlist Error');\n }\n if (response.currentVideoEndpoint) {\n return getWatchPlaylist(response, body, url_);\n } else return getNormalPlaylist(response, body);\n}\n/**\n * Function to parse Playlist from YouTube search\n * @param data html data of that request\n * @param limit No. of videos to parse\n * @returns Array of YouTubeVideo.\n */\nexport function getPlaylistVideos(data: any, limit = Infinity): YouTubeVideo[] {\n const videos = [];\n\n for (let i = 0; i < data.length; i++) {\n if (limit === videos.length) break;\n const info = data[i].playlistVideoRenderer;\n if (!info || !info.shortBylineText) continue;\n\n videos.push(\n new YouTubeVideo({\n id: info.videoId,\n duration: parseInt(info.lengthSeconds) || 0,\n duration_raw: info.lengthText?.simpleText ?? '0:00',\n thumbnails: info.thumbnail.thumbnails,\n title: info.title.runs[0].text,\n upcoming: info.upcomingEventData?.startTime\n ? new Date(parseInt(info.upcomingEventData.startTime) * 1000)\n : undefined,\n channel: {\n id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || undefined,\n name: info.shortBylineText.runs[0].text || undefined,\n url: `https://www.youtube.com${\n info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl ||\n info.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url\n }`,\n icon: undefined\n }\n })\n );\n }\n return videos;\n}\n/**\n * Function to get Continuation Token\n * @param data html data of playlist url\n * @returns token\n */\nexport function getContinuationToken(data: any): string {\n return data.find((x: any) => Object.keys(x)[0] === 'continuationItemRenderer')?.continuationItemRenderer\n .continuationEndpoint?.continuationCommand?.token;\n}\n\nasync function acceptViewerDiscretion(\n videoId: string,\n cookieJar: { [key: string]: string },\n body: string,\n extractRelated: boolean\n): Promise<{ streamingData: any; relatedVideos?: any }> {\n const apiKey =\n body.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ??\n body.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ??\n DEFAULT_API_KEY;\n const sessionToken =\n body.split('\"XSRF_TOKEN\":\"')[1]?.split('\"')[0].replaceAll('\\\\u003d', '=') ??\n body.split('\"xsrf_token\":\"')[1]?.split('\"')[0].replaceAll('\\\\u003d', '=');\n if (!sessionToken)\n throw new Error(`Unable to extract XSRF_TOKEN to accept the viewer discretion popup for video: ${videoId}.`);\n\n const verificationResponse = await request(`https://www.youtube.com/youtubei/v1/verify_age?key=${apiKey}&prettyPrint=false`, {\n method: 'POST',\n body: JSON.stringify({\n context: {\n client: {\n utcOffsetMinutes: 0,\n gl: 'US',\n hl: 'en',\n clientName: 'WEB',\n clientVersion:\n body.split('\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"')[1]?.split('\"')[0] ??\n body.split('\"innertube_context_client_version\":\"')[1]?.split('\"')[0] ??\n ''\n },\n user: {},\n request: {}\n },\n nextEndpoint: {\n urlEndpoint: {\n url: `/watch?v=${videoId}&has_verified=1`\n }\n },\n setControvercy: true\n }),\n cookies: true,\n cookieJar\n });\n\n const endpoint = JSON.parse(verificationResponse).actions[0].navigateAction.endpoint;\n\n const videoPage = await request(`https://www.youtube.com/${endpoint.urlEndpoint.url}&pbj=1`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: new URLSearchParams([\n ['command', JSON.stringify(endpoint)],\n ['session_token', sessionToken]\n ]).toString(),\n cookies: true,\n cookieJar\n });\n\n if (videoPage.includes('

Something went wrong

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