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

Something went wrong

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