UNPKG

@vookav2/play-dl

Version:

YouTube, SoundCloud, Spotify, Deezer searching and streaming for discord-js bots

1 lines 309 kB
{"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<IncomingMessage> {\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<IncomingMessage> {\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<string> {\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<string> {\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<number> {\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<IncomingMessage> {\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('<AdaptationSet id=\"0\"')[1]\n .split('</AdaptationSet>')[0]\n .split('</Representation>');\n if (audioFormat[audioFormat.length - 1] === '') audioFormat.pop();\n this.base_url = audioFormat[audioFormat.length - 1].split('<BaseURL>')[1].split('</BaseURL>')[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('<SegmentList>')[1]\n .split('</SegmentList>')[0]\n .replaceAll('<SegmentURL media=\"', '')\n .split('\"/>');\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<formatOptions[]> {\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<string, YouTubeVideo[]>;\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 ?? '<important data>';\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<YouTubeVideo[]> {\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