UNPKG

youtube-sr

Version:

Simple package to make YouTube search.

1 lines 101 kB
{"version":3,"sources":["../src/formatter.ts","../src/Structures/Channel.ts","../src/Structures/Thumbnail.ts","../src/Structures/Playlist.ts","../src/Structures/Video.ts","../src/Util.ts","../src/mod.ts"],"sourcesContent":["/*\n * MIT License\n *\n * Copyright (c) 2020 twlite\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport { Playlist, Video, Channel } from \"./Structures/exports\";\nimport Util from \"./Util\";\n\nexport class Formatter {\n constructor() {\n return Formatter;\n }\n\n public static formatSearchResult(\n details: any[],\n options: { limit?: number; type?: \"film\" | \"video\" | \"channel\" | \"playlist\" | \"all\" } = {\n limit: 100,\n type: \"all\"\n }\n ) {\n const results: Array<Video | Channel | Playlist> = [];\n\n for (let i = 0; i < details.length; i++) {\n if (typeof options.limit === \"number\" && options.limit > 0 && results.length >= options.limit) break;\n let data = details[i];\n let res: Video | Channel | Playlist;\n if (options.type === \"all\") {\n if (!!data.videoRenderer) options.type = \"video\";\n else if (!!data.channelRenderer) options.type = \"channel\";\n else if (!!data.playlistRenderer) options.type = \"playlist\";\n else continue;\n }\n\n if (options.type === \"video\" || options.type === \"film\") {\n const parsed = Util.parseVideo(data);\n if (!parsed) continue;\n res = parsed;\n } else if (options.type === \"channel\") {\n const parsed = Util.parseChannel(data);\n if (!parsed) continue;\n res = parsed;\n } else if (options.type === \"playlist\") {\n const parsed = Util.parsePlaylist(data);\n if (!parsed) continue;\n res = parsed;\n }\n\n results.push(res);\n }\n\n return results;\n }\n}\n","/*\n * MIT License\n *\n * Copyright (c) 2020 twlite\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nexport interface ChannelIconInterface {\n url?: string;\n width: number;\n height: number;\n}\n\nexport class Channel {\n name?: string;\n verified: boolean;\n id?: string;\n url?: string;\n icon: ChannelIconInterface;\n subscribers?: string;\n\n constructor(data: any) {\n if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);\n\n this._patch(data);\n }\n\n /**\n * Patch raw data\n * @private\n * @ignore\n */\n private _patch(data: any): void {\n if (!data) data = {};\n\n this.name = data.name || null;\n this.verified = !!data.verified || false;\n this.id = data.id || null;\n this.url = data.url || null;\n this.icon = data.icon || { url: null, width: 0, height: 0 };\n this.subscribers = data.subscribers || null;\n\n if (this.icon.url?.startsWith(\"//\")) this.icon.url = `https:${this.icon.url}`;\n }\n\n /**\n * Returns channel icon url\n * @param {object} options Icon options\n * @param {number} [options.size=0] Icon size. **Default is 0**\n */\n iconURL(options = { size: 0 }): string {\n if (typeof options.size !== \"number\" || options.size < 0) throw new Error(\"invalid icon size\");\n if (!this.icon.url) return null;\n const def = this.icon.url.split(\"=s\")[1].split(\"-c\")[0];\n return this.icon.url.replace(`=s${def}-c`, `=s${options.size}-c`);\n }\n\n get type(): \"channel\" {\n return \"channel\";\n }\n\n toString(): string {\n return this.name || \"\";\n }\n\n toJSON() {\n return {\n name: this.name,\n verified: this.verified,\n id: this.id,\n url: this.url,\n iconURL: this.iconURL(),\n type: this.type,\n subscribers: this.subscribers\n };\n }\n}\n","/*\n * MIT License\n *\n * Copyright (c) 2020 twlite\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\ntype ThumbnailType = \"default\" | \"hqdefault\" | \"mqdefault\" | \"sddefault\" | \"maxresdefault\" | \"ultrares\";\nexport class Thumbnail {\n id?: string;\n width: number;\n height: number;\n url?: string;\n\n /**\n * Thumbnail constructor\n * @param data Thumbnail constructor params\n */\n constructor(data: any) {\n if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);\n\n this._patch(data);\n }\n\n /**\n * Patch raw data\n * @param data Raw Data\n * @private\n * @ignore\n */\n private _patch(data: any) {\n if (!data) data = {};\n\n this.id = data.id || null;\n this.width = data.width || 0;\n this.height = data.height || 0;\n this.url = data.url || null;\n }\n\n /**\n * Returns thumbnail url\n * @param {\"default\"|\"hqdefault\"|\"mqdefault\"|\"sddefault\"|\"maxresdefault\"|\"ultrares\"} thumbnailType Thumbnail type\n */\n displayThumbnailURL(thumbnailType: ThumbnailType = \"ultrares\"): string {\n if (![\"default\", \"hqdefault\", \"mqdefault\", \"sddefault\", \"maxresdefault\", \"ultrares\"].includes(thumbnailType)) throw new Error(`Invalid thumbnail type \"${thumbnailType}\"!`);\n if (thumbnailType === \"ultrares\") return this.url;\n return `https://i3.ytimg.com/vi/${this.id}/${thumbnailType}.jpg`;\n }\n\n /**\n * Returns default thumbnail\n * @param {\"0\"|\"1\"|\"2\"|\"3\"|\"4\"} id Thumbnail id. **4 returns thumbnail placeholder.**\n */\n defaultThumbnailURL(id: \"0\" | \"1\" | \"2\" | \"3\" | \"4\"): string {\n if (!id) id = \"0\";\n if (![\"0\", \"1\", \"2\", \"3\", \"4\"].includes(id)) throw new Error(`Invalid thumbnail id \"${id}\"!`);\n return `https://i3.ytimg.com/vi/${this.id}/${id}.jpg`;\n }\n\n toString(): string {\n return this.url ? `${this.url}` : \"\";\n }\n\n toJSON() {\n return {\n id: this.id,\n width: this.width,\n height: this.height,\n url: this.url\n };\n }\n}\n","/*\n * MIT License\n *\n * Copyright (c) 2020 twlite\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport { Thumbnail } from \"./Thumbnail\";\nimport { Video } from \"./Video\";\nimport { Channel } from \"./Channel\";\nimport Util from \"../Util\";\nconst BASE_API = \"https://www.youtube.com/youtubei/v1/browse?key=\";\n\nexport class Playlist {\n id?: string;\n title?: string;\n videoCount: number;\n lastUpdate?: string;\n views?: number;\n url?: string;\n link?: string;\n channel?: Channel;\n thumbnail?: Thumbnail;\n videos: Video[];\n mix?: boolean;\n fake = false;\n private _continuation: { api?: string; token?: string; clientVersion?: string } = {};\n\n constructor(data = {}, searchResult = false) {\n if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);\n Object.defineProperty(this, \"_continuation\", { enumerable: false, configurable: true, writable: true });\n if (!!searchResult) this._patchSearch(data);\n else this._patch(data);\n }\n\n private _patch(data: any) {\n this.id = data.id || null;\n this.title = data.title || null;\n this.videoCount = data.videoCount || 0;\n this.lastUpdate = data.lastUpdate || null;\n this.views = data.views || 0;\n this.url = data.url || data.link || this.id ? `https://www.youtube.com/playlist?list=${this.id}` : null;\n this.link = data.link || data.url || null;\n this.channel = data.author || null;\n this.thumbnail = new Thumbnail(data.thumbnail || {});\n this.videos = data.videos || [];\n this._continuation.api = data.continuation?.api ?? null;\n this._continuation.token = data.continuation?.token ?? null;\n this._continuation.clientVersion = data.continuation?.clientVersion ?? \"<important data>\";\n this.mix = data.mix || false;\n this.fake = Boolean(data.fake);\n }\n\n private _patchSearch(data: any) {\n this.id = data.id || null;\n this.title = data.title || null;\n this.thumbnail = new Thumbnail(data.thumbnail || {});\n this.channel = data.channel || null;\n this.videos = data.videos || [];\n this.videoCount = data.videos?.length || 0;\n this.url = data.url || data.link || this.id ? `https://www.youtube.com/playlist?list=${this.id}` : null;\n this.link = data.link || data.url || null;\n this.lastUpdate = null;\n this.views = 0;\n this.mix = data.mix || false;\n this.fake = Boolean(data.fake);\n }\n\n /**\n * @param limit Max items to parse from current chunk\n */\n async next(limit: number = Infinity): Promise<Video[]> {\n if (!this._continuation || !this._continuation.token) return [];\n\n const nextPage = await Util.getHTML(`${BASE_API}${this._continuation.api}`, {\n method: \"POST\",\n body: JSON.stringify({\n continuation: this._continuation.token,\n context: {\n client: {\n utcOffsetMinutes: 0,\n gl: \"US\",\n hl: \"en\",\n clientName: \"WEB\",\n clientVersion: this._continuation.clientVersion\n },\n user: {},\n request: {}\n }\n })\n });\n\n const contents = Util.json(nextPage)?.onResponseReceivedActions[0]?.appendContinuationItemsAction?.continuationItems;\n if (!contents) return [];\n const partial = Util.getPlaylistVideos(contents, limit);\n this._continuation.token = Util.getContinuationToken(contents);\n this.videos = [...this.videos, ...partial];\n\n return partial;\n }\n\n async fetch(max: number = Infinity) {\n const ctn = this._continuation.token;\n if (!ctn) return this;\n if (max < 1) max = Infinity;\n\n while (typeof this._continuation.token === \"string\" && this._continuation.token.length) {\n if (this.videos.length >= max) break;\n const res = await this.next();\n if (!res.length) break;\n }\n\n return this;\n }\n\n get type(): \"playlist\" {\n return \"playlist\";\n }\n\n *[Symbol.iterator](): IterableIterator<Video> {\n yield* this.videos;\n }\n\n toJSON() {\n return {\n id: this.id,\n title: this.title,\n thumbnail: this.thumbnail?.toJSON() || null,\n channel: {\n name: this.channel.name,\n id: this.channel.id,\n icon: this.channel?.iconURL?.()\n },\n mix: this.mix,\n url: this.url,\n videos: this.videos\n };\n }\n}\n","/*\n * MIT License\n *\n * Copyright (c) 2020 twlite\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport { Channel } from \"./Channel\";\nimport { Thumbnail } from \"./Thumbnail\";\n\nexport interface VideoStreamingData {\n expiresInSeconds: string;\n formats: VideoStreamingFormat[];\n adaptiveFormats: VideoStreamingFormatAdaptive[];\n}\n\nexport interface VideoStreamingFormat {\n itag: number;\n mimeType: string;\n bitrate: number;\n width: number;\n height: number;\n lastModified: string;\n contentLength: string;\n quality: string;\n fps: number;\n qualityLabel: string;\n projectionType: string;\n averageBitrate: number;\n audioQuality: string;\n approxDurationMs: string;\n audioSampleRate: string;\n audioChannels: number;\n signatureCipher: string;\n}\n\nexport interface VideoStreamingFormatAdaptive extends VideoStreamingFormat {\n initRange?: { start: string; end: string };\n indexRange?: { start: string; end: string };\n colorInfo?: {\n primaries: string;\n transferCharacteristics?: string;\n matrixCoefficients?: string;\n };\n highReplication?: boolean;\n loudnessDb?: number;\n}\n\nexport interface MusicInfo {\n title: string;\n cover: string;\n artist: string;\n album: string;\n}\n\nexport class Video {\n id?: string;\n title?: string;\n description?: string;\n durationFormatted: string;\n duration: number;\n uploadedAt?: string;\n views: number;\n thumbnail?: Thumbnail;\n channel?: Channel;\n videos?: Video[];\n likes: number;\n dislikes: number;\n live: boolean;\n private: boolean;\n tags: string[];\n nsfw = false;\n shorts = false;\n unlisted = false;\n streamingData?: VideoStreamingData | null;\n music: MusicInfo[];\n\n constructor(data: any) {\n if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`);\n\n this._patch(data);\n }\n\n /**\n * Patch raw data\n * @private\n * @ignore\n */\n private _patch(data: any): void {\n if (!data) data = {};\n\n this.id = data.id || null;\n this.title = data.title || null;\n this.description = data.description || null;\n this.durationFormatted = data.duration_raw || \"0:00\";\n this.duration = (data.duration < 0 ? 0 : data.duration) || 0;\n this.uploadedAt = data.uploadedAt || null;\n this.views = parseInt(data.views) || 0;\n this.thumbnail = new Thumbnail(data.thumbnail || {});\n this.channel = new Channel(data.channel || {});\n this.likes = data.ratings?.likes || 0;\n this.dislikes = data.ratings?.dislikes || 0;\n this.live = !!data.live;\n this.private = !!data.private;\n this.tags = data.tags || [];\n this.nsfw = Boolean(data.nsfw);\n this.unlisted = Boolean(data.unlisted);\n this.shorts = Boolean(data.shorts);\n this.music = data.music;\n Object.defineProperty(this, \"streamingData\", {\n enumerable: false,\n configurable: true,\n writable: true,\n value: data.streamingData || null\n });\n Object.defineProperty(this, \"videos\", {\n enumerable: false,\n configurable: true,\n writable: true,\n value: data.videos || []\n });\n }\n\n get formats() {\n return this.streamingData?.formats || [];\n }\n\n get adaptiveFormats() {\n return this.streamingData?.adaptiveFormats || [];\n }\n\n get url() {\n if (!this.id) return null;\n return `https://www.youtube.com/watch?v=${this.id}`;\n }\n\n get shortsURL() {\n if (!this.shorts) return this.url;\n return `https://www.youtube.com/shorts/${this.id}`;\n }\n\n /**\n * YouTube video embed html\n * @param {object} options Options\n * @param {string} [options.id] DOM element id\n * @param {number} [options.width] Iframe width\n * @param {number} [options.height] Iframe height\n */\n embedHTML(options = { id: \"ytplayer\", width: 640, height: 360 }): string {\n if (!this.id) return null;\n return `<iframe title=\"__youtube_sr_frame__\" id=\"${options.id || \"ytplayer\"}\" type=\"text/html\" width=\"${options.width || 640}\" height=\"${options.height || 360}\" src=\"${this.embedURL}\" frameborder=\"0\"></iframe>`;\n }\n\n /**\n * Creates mix playlist url from this video\n */\n createMixURL() {\n return `${this.url}&list=RD${this.id}`;\n }\n\n /**\n * YouTube video embed url\n */\n get embedURL(): string {\n if (!this.id) return null;\n return `https://www.youtube.com/embed/${this.id}`;\n }\n\n get type(): \"video\" {\n return \"video\";\n }\n\n toString(): string {\n return this.url || \"\";\n }\n\n toJSON() {\n const res = {\n id: this.id,\n url: this.url,\n shorts_url: this.shortsURL,\n title: this.title,\n description: this.description,\n duration: this.duration,\n duration_formatted: this.durationFormatted,\n uploadedAt: this.uploadedAt,\n unlisted: this.unlisted,\n nsfw: this.nsfw,\n thumbnail: this.thumbnail.toJSON(),\n channel: {\n name: this.channel.name,\n id: this.channel.id,\n icon: this.channel.iconURL()\n },\n views: this.views,\n type: this.type,\n tags: this.tags,\n ratings: {\n likes: this.likes,\n dislikes: this.dislikes\n },\n shorts: this.shorts,\n live: this.live,\n private: this.private,\n music: this.music\n };\n\n return res;\n }\n}\n","/*\n * MIT License\n *\n * Copyright (c) 2020 twlite\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport { Formatter } from \"./formatter\";\nimport { Channel, Video, Playlist, MusicInfo } from \"./Structures/exports\";\n\nconst PLAYLIST_REGEX = /^https?:\\/\\/(www.)?youtube.com\\/playlist\\?list=((PL|FL|UU|LL|RD|OL)[a-zA-Z0-9-_]{16,41})$/;\nconst PLAYLIST_ID = /(PL|FL|UU|LL|RD|OL)[a-zA-Z0-9-_]{11,41}/;\nconst ALBUM_REGEX = /(RDC|O)LAK5uy_[a-zA-Z0-9-_]{33}/;\nconst VIDEO_URL = /^((?:https?:)?\\/\\/)?((?:www|m)\\.)?((?:youtube\\.com|youtu.be))(\\/(?:[\\w\\-]+\\?v=|embed\\/|v\\/)?)([\\w\\-]+)(\\S+)?$/;\nconst VIDEO_ID = /^[a-zA-Z0-9-_]{11}$/;\nconst DEFAULT_INNERTUBE_KEY = \"AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8\";\nlet innertubeCache: string = null;\nlet __fetch: typeof globalThis.fetch;\nconst isNode = typeof process !== \"undefined\" && \"node\" in (process.versions || {});\nconst FETCH_LIBS = [\"node-fetch\", \"cross-fetch\", \"undici\"];\n\nexport interface ParseSearchInterface {\n type?: \"video\" | \"playlist\" | \"channel\" | \"all\" | \"film\";\n limit?: number;\n requestOptions?: RequestInit;\n}\n\nasync function getFetch(): Promise<typeof globalThis.fetch> {\n // return if fetch is already resolved\n if (typeof __fetch === \"function\") return __fetch;\n // try to locate fetch in window\n if (typeof window !== \"undefined\" && \"fetch\" in window) return window.fetch;\n // try to locate fetch in globalThis\n if (\"fetch\" in globalThis) return globalThis.fetch;\n\n // try to resolve fetch by importing fetch libs\n for (const fetchLib of FETCH_LIBS) {\n try {\n const pkg = await import(fetchLib);\n const mod = pkg.fetch || pkg.default || pkg;\n if (mod) return (__fetch = mod);\n } catch {}\n }\n\n if (isNode) throw new Error(`Could not resolve fetch library. Install one of ${FETCH_LIBS.map((m) => `\"${m}\"`).join(\", \")} or define \"fetch\" in global scope!`);\n throw new Error(\"Could not resolve fetch in global scope\");\n}\n\nclass Util {\n constructor() {\n return Util;\n }\n\n static async innertubeKey(): Promise<string> {\n if (innertubeCache) return innertubeCache;\n return await Util.fetchInnerTubeKey();\n }\n\n static get VideoRegex(): RegExp {\n return VIDEO_URL;\n }\n\n static get VideoIDRegex(): RegExp {\n return VIDEO_ID;\n }\n\n static get AlbumRegex(): RegExp {\n return ALBUM_REGEX;\n }\n\n /**\n * YouTube playlist URL Regex\n * @type {RegExp}\n */\n static get PlaylistURLRegex(): RegExp {\n return PLAYLIST_REGEX;\n }\n\n /**\n * YouTube Playlist ID regex\n * @type {RegExp}\n */\n static get PlaylistIDRegex(): RegExp {\n return PLAYLIST_ID;\n }\n\n static async fetchInnerTubeKey() {\n const html = await Util.getHTML(\"https://www.youtube.com?hl=en\");\n const key = html.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ?? html.split('innertubeApiKey\":\"')[1]?.split('\"')[0];\n if (key) innertubeCache = key;\n return key ?? DEFAULT_INNERTUBE_KEY;\n }\n\n /**\n * Parse HTML\n * @param {string} url Website URL\n * @param {RequestInit} [requestOptions] Request Options\n * @returns {Promise<string>}\n */\n static getHTML(url: string, requestOptions: RequestInit = {}): Promise<string> {\n requestOptions = Object.assign(\n {},\n {\n headers: Object.assign(\n {},\n {\n \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; rv:140.0) Gecko/20100101 Firefox/140.0\"\n },\n requestOptions?.headers || {}\n )\n } as RequestInit,\n requestOptions || {}\n );\n\n return new Promise(async (resolve, reject) => {\n // lazy load fetch\n if (!__fetch) __fetch = await getFetch();\n __fetch(url, requestOptions)\n .then((res: Response) => {\n if (!res.ok) throw new Error(`Rejected with status code: ${res.status}`);\n return res.text();\n })\n .then((html: string) => resolve(html))\n .catch((e: Error) => reject(e));\n });\n }\n\n /**\n * Returns duration in ms\n * @param {string} duration Duration to parse\n */\n static parseDuration(duration: string): number {\n duration ??= \"0:00\";\n const args = duration.split(\":\");\n let dur = 0;\n\n switch (args.length) {\n case 3:\n dur = parseInt(args[0]) * 60 * 60 * 1000 + parseInt(args[1]) * 60 * 1000 + parseInt(args[2]) * 1000;\n break;\n case 2:\n dur = parseInt(args[0]) * 60 * 1000 + parseInt(args[1]) * 1000;\n break;\n default:\n dur = parseInt(args[0]) * 1000;\n }\n\n return dur;\n }\n\n /**\n * Parse items from html\n * @param {string} html HTML\n * @param options Options\n */\n static parseSearchResult(html: string, options?: ParseSearchInterface): (Video | Channel | Playlist)[] {\n if (!html) throw new Error(\"Invalid raw data\");\n if (!options) options = { type: \"video\", limit: 0 };\n if (!options.type) options.type = \"video\";\n\n let details = [];\n let fetched = false;\n\n // try to parse html\n try {\n let data = html.split(\"ytInitialData = JSON.parse('\")[1].split(\"');</script>\")[0];\n html = data.replace(/\\\\x([0-9A-F]{2})/gi, (...items) => {\n return String.fromCharCode(parseInt(items[1], 16));\n });\n } catch {\n /* do nothing */\n }\n\n try {\n details = JSON.parse(html.split('{\"itemSectionRenderer\":{\"contents\":')[html.split('{\"itemSectionRenderer\":{\"contents\":').length - 1].split(',\"continuations\":[{')[0]);\n fetched = true;\n } catch {\n /* do nothing */\n }\n\n if (!fetched) {\n try {\n details = JSON.parse(html.split('{\"itemSectionRenderer\":')[html.split('{\"itemSectionRenderer\":').length - 1].split('},{\"continuationItemRenderer\":{')[0]).contents;\n fetched = true;\n } catch {\n /* do nothing */\n }\n }\n\n if (!fetched) return [];\n\n return Formatter.formatSearchResult(details, options);\n }\n\n /**\n * Parse channel from raw data\n * @param {object} data Raw data to parse video from\n */\n static parseChannel(data?: any): Channel {\n if (!data || !data.channelRenderer) return;\n const badges = data.channelRenderer.ownerBadges as any[];\n let url = `https://www.youtube.com${data.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl || data.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url}`;\n let res = new Channel({\n id: data.channelRenderer.channelId,\n name: data.channelRenderer.title.simpleText,\n icon: data.channelRenderer.thumbnail.thumbnails[data.channelRenderer.thumbnail.thumbnails.length - 1],\n url: url,\n verified: !badges?.length ? false : badges.some((badge) => badge[\"verifiedBadge\"] || badge?.metadataBadgeRenderer?.style?.toLowerCase().includes(\"verified\")),\n subscribers: data.channelRenderer.subscriberCountText.simpleText\n });\n\n return res;\n }\n\n /**\n * Parse video from raw data\n * @param {object} data Raw data to parse video from\n */\n static parseVideo(data?: any): Video {\n if (!data || !data.videoRenderer) return;\n\n const badge = data.videoRenderer.ownerBadges && data.videoRenderer.ownerBadges[0];\n let res = new Video({\n id: data.videoRenderer.videoId,\n url: `https://www.youtube.com/watch?v=${data.videoRenderer.videoId}`,\n title: data.videoRenderer.title.runs[0].text,\n description: data.videoRenderer.descriptionSnippet && data.videoRenderer.descriptionSnippet.runs[0] ? data.videoRenderer.descriptionSnippet.runs[0].text : \"\",\n duration: data.videoRenderer.lengthText ? Util.parseDuration(data.videoRenderer.lengthText.simpleText) : 0,\n duration_raw: data.videoRenderer.lengthText ? data.videoRenderer.lengthText.simpleText : null,\n thumbnail: {\n id: data.videoRenderer.videoId,\n url: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].url,\n height: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].height,\n width: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].width\n },\n channel: {\n id: data.videoRenderer.ownerText.runs[0].navigationEndpoint.browseEndpoint.browseId || null,\n name: data.videoRenderer.ownerText.runs[0].text || null,\n url: `https://www.youtube.com${data.videoRenderer.ownerText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl || data.videoRenderer.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,\n icon: {\n url: data.videoRenderer.channelThumbnail?.thumbnails?.[0]?.url || data.videoRenderer.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails?.[0]?.url,\n width: data.videoRenderer.channelThumbnail?.thumbnails?.[0]?.width || data.videoRenderer.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails?.[0]?.width,\n height: data.videoRenderer.channelThumbnail?.thumbnails?.[0]?.height || data.videoRenderer.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails?.[0]?.height\n },\n verified: Boolean(badge?.metadataBadgeRenderer?.style?.toLowerCase().includes(\"verified\"))\n },\n uploadedAt: data.videoRenderer.publishedTimeText?.simpleText ?? null,\n views: data.videoRenderer.viewCountText?.simpleText?.replace(/[^0-9]/g, \"\") ?? 0\n });\n\n return res;\n }\n\n static parsePlaylist(data?: any): Playlist {\n if (!data.playlistRenderer) return;\n\n const res = new Playlist(\n {\n id: data.playlistRenderer.playlistId,\n title: data.playlistRenderer.title.simpleText,\n thumbnail: {\n id: data.playlistRenderer.playlistId,\n url: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].url,\n height: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].height,\n width: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].width\n },\n channel: {\n id: data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId,\n name: data.playlistRenderer.shortBylineText.runs[0].text,\n url: `https://www.youtube.com${data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.browseEndpoint?.canonicalBaseUrl || data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata?.webCommandMetadata?.url}`\n },\n videos: parseInt(data.playlistRenderer.videoCount.replace(/[^0-9]/g, \"\"))\n },\n true\n );\n\n return res;\n }\n\n static getPlaylistVideos(data: any, limit: number = Infinity) {\n const videos = [];\n\n for (let i = 0; i < data.length; i++) {\n if (limit === videos.length) break;\n const info = data[i].playlistVideoRenderer;\n if (!info || !info.shortBylineText) continue; // skip unknown videos\n\n videos.push(\n new Video({\n id: info.videoId,\n index: parseInt(info.index?.simpleText) || 0,\n duration: Util.parseDuration(info.lengthText?.simpleText) || 0,\n duration_raw: info.lengthText?.simpleText ?? \"0:00\",\n thumbnail: {\n id: info.videoId,\n url: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].url,\n height: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].height,\n width: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].width\n },\n title: info.title.runs[0].text,\n channel: {\n id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || null,\n name: info.shortBylineText.runs[0].text || null,\n url: `https://www.youtube.com${info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl || info.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,\n icon: null\n }\n })\n );\n }\n\n return videos;\n }\n\n static getPlaylist(html: string, limit?: number): Playlist {\n if (!limit || typeof limit !== \"number\") limit = 100;\n if (limit <= 0) limit = Infinity;\n let parsed;\n let playlistDetails;\n try {\n const rawJSON = `${html.split('{\"playlistVideoListRenderer\":{\"contents\":')[1].split('}],\"playlistId\"')[0]}}]`;\n parsed = JSON.parse(rawJSON);\n playlistDetails = JSON.parse(html.split('{\"playlistSidebarRenderer\":')[1].split(\"}};</script>\")[0]).items;\n } catch {\n return null;\n }\n const API_KEY = html.split('INNERTUBE_API_KEY\":\"')[1]?.split('\"')[0] ?? html.split('innertubeApiKey\":\"')[1]?.split('\"')[0] ?? DEFAULT_INNERTUBE_KEY;\n const videos = Util.getPlaylistVideos(parsed, limit);\n\n const data = playlistDetails[0].playlistSidebarPrimaryInfoRenderer;\n\n if (!data.title.runs || !data.title.runs.length) return null;\n const author = playlistDetails[1]?.playlistSidebarSecondaryInfoRenderer.videoOwner;\n const views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/[^0-9]/g, \"\") : 0;\n const lastUpdate = data.stats.find((x: any) => \"runs\" in x && x[\"runs\"].find((y: any) => y.text.toLowerCase().includes(\"last update\")))?.runs.pop()?.text ?? null;\n const videosCount = data.stats[0].runs[0].text.replace(/[^0-9]/g, \"\") || 0;\n\n const res = new Playlist({\n continuation: {\n api: API_KEY,\n token: Util.getContinuationToken(parsed),\n clientVersion: html.split('\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"')[1]?.split('\"')[0] ?? html.split('\"innertube_context_client_version\":\"')[1]?.split('\"')[0] ?? \"<some version>\"\n },\n id: data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId,\n title: data.title.runs[0].text,\n videoCount: parseInt(videosCount) || 0,\n lastUpdate: lastUpdate,\n views: parseInt(views) || 0,\n videos: videos,\n url: `https://www.youtube.com/playlist?list=${data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId}`,\n link: `https://www.youtube.com${data.title.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`,\n author: author\n ? {\n name: author.videoOwnerRenderer.title.runs[0].text,\n id: author.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId,\n url: `https://www.youtube.com${author.videoOwnerRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url || author.videoOwnerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl}`,\n icon: author.videoOwnerRenderer.thumbnail.thumbnails.length ? author.videoOwnerRenderer.thumbnail.thumbnails.pop()?.url : null\n }\n : {},\n thumbnail: data.thumbnailRenderer.playlistVideoThumbnailRenderer?.thumbnail.thumbnails.length ? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.pop() : null\n });\n\n return res;\n }\n\n static getContinuationToken(ctx: any): string {\n const continuationToken = ctx.find((x: any) => Object.keys(x)[0] === \"continuationItemRenderer\")?.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token;\n return continuationToken;\n }\n\n static getVideo(html: string) {\n let data,\n nextData = {};\n\n try {\n const parsed = JSON.parse(html.split(\"var ytInitialData = \")[1].split(\";</script>\")[0]);\n data = parsed.contents.twoColumnWatchNextResults.results.results.contents;\n\n try {\n nextData = parsed.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results;\n } catch {}\n } catch {\n throw new Error(\"Could not parse video metadata!\");\n }\n\n const raw = {\n primary: data?.find((section: any) => \"videoPrimaryInfoRenderer\" in section)?.videoPrimaryInfoRenderer || {},\n secondary: data?.find((section: any) => \"videoSecondaryInfoRenderer\" in section)?.videoSecondaryInfoRenderer || {}\n };\n\n let info: any;\n\n try {\n info = JSON.parse(html.split(\"var ytInitialPlayerResponse = \")[1].split(\";</script>\")[0]);\n } catch {\n info = JSON.parse(html.split(\"var ytInitialPlayerResponse = \")[1].split(\";var\")[0]);\n }\n\n if (!info?.videoDetails) return null;\n\n info = {\n ...raw.primary,\n ...raw.secondary,\n info\n };\n\n // Get music info if there are any\n let musicInfo: MusicInfo[] = [];\n\n try {\n const jsonData = html.split('{\"horizontalCardListRenderer\":')[1].split(',{\"reelShelfRenderer\"')[0];\n\n musicInfo = JSON.parse('{\"horizontalCardListRenderer\":' + jsonData).horizontalCardListRenderer.cards.map((val: any) => {\n return {\n title: val.videoAttributeViewModel.title,\n cover: val.videoAttributeViewModel.image.sources[0].url,\n artist: val.videoAttributeViewModel.subtitle,\n album: val.videoAttributeViewModel.secondarySubtitle.content\n };\n });\n } catch {\n musicInfo = [];\n }\n\n const payload = new Video({\n id: info.info.videoDetails.videoId,\n title: info.info.videoDetails.title,\n views: parseInt(info.info.videoDetails.viewCount) || 0,\n tags: info.info.videoDetails.keywords,\n private: info.info.videoDetails.isPrivate,\n unlisted: !!info.info.microformat?.playerMicroformatRenderer?.isUnlisted,\n nsfw: info.info.microformat?.playerMicroformatRenderer?.isFamilySafe === false,\n live: info.info.videoDetails.isLiveContent,\n duration: parseInt(info.info.videoDetails.lengthSeconds) * 1000,\n shorts: [`{\"webCommandMetadata\":{\"url\":\"/shorts/${info.info.videoDetails.videoId}\"`, `{window['ytPageType'] = \"shorts\";`, `\"/hashtag/shorts\"`].some((r) => html.includes(r)),\n duration_raw: Util.durationString(Util.parseMS(parseInt(info.info.videoDetails.lengthSeconds) * 1000 || 0)),\n channel: {\n name: info.info.videoDetails.author,\n id: info.info.videoDetails.channelId,\n url: `https://www.youtube.com${info.owner.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl}`,\n icon: info.owner.videoOwnerRenderer.thumbnail.thumbnails[0],\n subscribers: info.owner.videoOwnerRenderer.subscriberCountText?.simpleText?.replace(\" subscribers\", \"\")\n },\n description: info.info.videoDetails.shortDescription,\n thumbnail: {\n ...info.info.videoDetails.thumbnail.thumbnails[info.info.videoDetails.thumbnail.thumbnails.length - 1],\n id: info.info.videoDetails.videoId\n },\n uploadedAt: info.dateText.simpleText,\n ratings: {\n likes: this.getInfoLikesCount(info) || 0,\n dislikes: 0\n },\n videos: Util.getNext(nextData ?? {}) || [],\n streamingData: info.info.streamingData || null,\n music: musicInfo\n });\n\n return payload;\n }\n\n static getInfoLikesCount(info: Record<string, any>) {\n const buttons = info.videoActions.menuRenderer.topLevelButtons as any[];\n const button = buttons.find((button) => button.toggleButtonRenderer?.defaultIcon.iconType === \"LIKE\");\n if (!button) return 0;\n\n return parseInt(button.toggleButtonRenderer.defaultText.accessibility?.accessibilityData.label.split(\" \")[0].replace(/,/g, \"\"));\n }\n\n static getNext(body: any, home = false): Video[] {\n const results: Video[] = [];\n if (typeof body[Symbol.iterator] !== \"function\") return results;\n\n for (const result of body) {\n const details = home ? result : result.compactVideoRenderer;\n\n if (details) {\n try {\n let viewCount = details.viewCountText.simpleText;\n viewCount = (/^\\d/.test(viewCount) ? viewCount : \"0\").split(\" \")[0];\n\n results.push(\n new Video({\n id: details.videoId,\n title: details.title.simpleText ?? details.title.runs[0]?.text,\n views: parseInt(viewCount.replace(/,/g, \"\")) || 0,\n duration_raw: details.lengthText.simpleText ?? details.lengthText.accessibility.accessibilityData.label,\n duration: Util.parseDuration(details.lengthText.simpleText) / 1000,\n channel: {\n name: details.shortBylineText.runs[0].text,\n id: details.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId,\n url: `https://www.youtube.com${details.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl}`,\n icon: home ? details.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails[0] : details.channelThumbnail.thumbnails[0],\n subscribers: \"0\",\n verified: Boolean(details.ownerBadges[0].metadataBadgeRenderer.tooltip === \"Verified\")\n },\n thumbnail: {\n ...details.thumbnail.thumbnails[details.thumbnail.thumbnails.length - 1],\n id: details.videoId\n },\n uploadedAt: details.publishedTimeText.simpleText,\n ratings: {\n likes: 0,\n dislikes: 0\n },\n description: details.descriptionSnippet?.runs[0]?.text\n })\n );\n } catch {\n continue;\n }\n }\n }\n\n return results;\n }\n\n static getMix(html: string): Playlist {\n let data = null;\n\n try {\n const parsed = JSON.parse(html.split(\"var ytInitialData = \")[1].split(\";</script>\")[0]);\n data = parsed.contents.twoColumnWatchNextResults.playlist.playlist;\n } catch {}\n\n if (!data) return null;\n\n const videos = data.contents.map((m: any) => {\n const t = m.playlistPanelVideoRenderer;\n\n return new Video({\n id: t.videoId,\n title: t.title.simpleText,\n thumbnail: {\n id: t.videoId,\n url: t.thumbnail.thumbnails[t.thumbnail.thumbnails.length - 1].url,\n height: t.thumbnail.thumbnails[t.thumbnail.thumbnails.length - 1].height,\n width: t.thumbnail.thumbnails[t.thumbnail.thumbnails.length - 1].width\n },\n duration: Util.parseDuration(t.lengthText.simpleText),\n duration_raw: t.lengthText.simpleText,\n channel: {\n name: t.shortBylineText.runs[0].text,\n id: t.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId,\n url: `https://www.youtube.com${t.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl}`,\n icon: null\n }\n });\n });\n const list = new Playlist(\n {\n id: data.playlistId,\n title: data.title,\n videoCount: data.contents.length,\n videos,\n link: data.playlistShareUrl,\n url: data.playlistShareUrl,\n thumbnail: videos[0]?.thumbnail?.toJSON() || null,\n channel: {\n name: data.ownerName.simpleText\n },\n mix: true\n },\n true\n );\n\n return l