├── .prettierignore ├── .prettierrc ├── .yarnrc.yml ├── src ├── adapters │ ├── index.ts │ ├── raw.ts │ ├── irc.ts │ ├── truffle │ │ ├── hash.ts │ │ ├── users.ts │ │ ├── aliases.ts │ │ └── emotes.ts │ ├── subathon.ts │ ├── json.ts │ └── truffle.ts ├── routes │ ├── stream.ts │ └── channel.ts ├── util │ ├── util.ts │ ├── youtube.ts │ └── types.ts ├── index.ts └── YoutubeChat.ts ├── .eslintrc ├── wrangler.toml ├── test └── websocket.ts ├── tsconfig.json ├── package.json ├── README.md ├── .gitignore └── .yarn └── patches └── youtubei.js-npm-2.3.2-3338aebb89.patch /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "useTabs": true 6 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.2.3.cjs 8 | -------------------------------------------------------------------------------- /src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | import { LiveChatAction } from '@util/types'; 2 | 3 | export abstract class MessageAdapter { 4 | public static readonly hasState: boolean; 5 | 6 | public abstract readonly sockets: Set; 7 | 8 | abstract transform(action: LiveChatAction): string | undefined; 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint", 6 | "neverthrow" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "ignorePatterns": "dist" 14 | } 15 | -------------------------------------------------------------------------------- /src/adapters/raw.ts: -------------------------------------------------------------------------------- 1 | import { LiveChatAction } from '@util/types'; 2 | import { MessageAdapter } from '.'; 3 | 4 | export class RawMessageAdapter extends MessageAdapter { 5 | public sockets = new Set(); 6 | 7 | transform(action: LiveChatAction): string | undefined { 8 | return JSON.stringify(action); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/adapters/irc.ts: -------------------------------------------------------------------------------- 1 | import { LiveChatAction } from '@util/types'; 2 | import { MessageAdapter } from '.'; 3 | 4 | export class IRCMessageAdapter extends MessageAdapter { 5 | public sockets = new Set(); 6 | 7 | transform(action: LiveChatAction): string | undefined { 8 | throw new Error('Method not implemented.'); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "youtube-websocket" 2 | main = "src/index.ts" 3 | compatibility_date = "2022-09-24" 4 | node_compat = true 5 | 6 | [vars] 7 | TRUFFLE_API_BASE = 'https://v2.truffle.vip' 8 | 9 | [durable_objects] 10 | bindings = [{ name = "YOUTUBE_CHAT", class_name = "YoutubeChat" }] 11 | 12 | [[migrations]] 13 | tag = "v1" 14 | new_classes = ["YoutubeChat"] 15 | -------------------------------------------------------------------------------- /test/websocket.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket } from 'ws'; 2 | import { appendFile } from 'fs/promises'; 3 | 4 | const ws = new WebSocket( 5 | 'wss://youtube-websocket.mogul-moves.workers.dev/v/tJC0YAOKZxc?adapter=raw' 6 | ); 7 | 8 | ws.addEventListener('error', (ev) => { 9 | console.log(ev.message); 10 | }); 11 | 12 | ws.addEventListener('open', () => { 13 | console.log('Connected'); 14 | }); 15 | 16 | ws.on('message', async (msg) => { 17 | const json = JSON.parse(msg.toString()); 18 | console.log(json); 19 | await appendFile('./debug.out', msg.toString() + ',\n'); 20 | }); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "lib": [ 5 | "es2021" 6 | ], 7 | "baseUrl": ".", 8 | "module": "es2022", 9 | "moduleResolution": "node", 10 | "types": [ 11 | "@cloudflare/workers-types" 12 | ], 13 | "resolveJsonModule": true, 14 | "noEmit": true, 15 | "isolatedModules": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "strict": true, 19 | "alwaysStrict": true, 20 | "noUncheckedIndexedAccess": true, 21 | "skipLibCheck": true, 22 | "paths": { 23 | "@util/*": [ 24 | "src/util/*" 25 | ] 26 | } 27 | }, 28 | "ts-node": { 29 | "compilerOptions": { 30 | "module": "commonjs" 31 | } 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/stream.ts: -------------------------------------------------------------------------------- 1 | import { ok } from 'neverthrow'; 2 | import { Handler } from '@util/types'; 3 | import { notFound } from '@util/util'; 4 | import { getVideoData } from '@util/youtube'; 5 | import { createChatObject } from '../YoutubeChat'; 6 | 7 | export const getStream: Handler<{ id: string }> = async (request, env) => { 8 | if (!request.params.id) return notFound; 9 | 10 | if (!/^[A-Za-z0-9_-]{11}$/.test(request.params.id)) return notFound; 11 | const url = `https://www.youtube.com/watch?v=${request.params.id}`; 12 | 13 | const videoData = await getVideoData([url]); 14 | if (videoData.isErr()) return videoData; 15 | 16 | const res = await createChatObject( 17 | request.params.id, 18 | videoData.value, 19 | request, 20 | env 21 | ); 22 | return ok(res); 23 | }; 24 | -------------------------------------------------------------------------------- /src/adapters/truffle/hash.ts: -------------------------------------------------------------------------------- 1 | export function getStringHash(string: string): number { 2 | let hash = 0; 3 | if (string.length === 0) return 0; 4 | for (let i = 0; i < string.length; i++) { 5 | const chr = string.charCodeAt(i); 6 | hash = (hash << 5) - hash + chr; 7 | hash |= 0; // Convert to 32bit integer 8 | } 9 | return hash; 10 | } 11 | 12 | export function hashColor(string: string): string { 13 | const hash = getStringHash(string); 14 | return `hsl(${(((hash % 60) + 60) % 60) * 6}deg, 100%, 70%)`; 15 | } 16 | 17 | const colors = [ 18 | '#ff0000', 19 | '#009000', 20 | '#b22222', 21 | '#ff7f50', 22 | '#9acd32', 23 | '#ff4500', 24 | '#2e8b57', 25 | '#daa520', 26 | '#d2691e', 27 | '#5f9ea0', 28 | '#1e90ff', 29 | '#ff69b4', 30 | '#00ff7f', 31 | '#a244f9', 32 | ]; 33 | export function getUsernameColor(string: string): string { 34 | const hash = getStringHash(string); 35 | return colors[ 36 | ((hash % colors.length) + colors.length) % colors.length 37 | ] as string; 38 | } 39 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | import { err } from 'neverthrow'; 2 | import { Json, JsonObject } from './types'; 3 | 4 | export const notFound = err(['Not Found', 404]); 5 | 6 | export const youtubeHeaders = { 7 | 'User-Agent': 8 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36', 9 | }; 10 | 11 | export function traverseJSON( 12 | obj: Json, 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | callback: (value: any, key: string | number) => T | undefined 15 | ): T | undefined { 16 | if (!obj) return; 17 | if (typeof obj === 'object') { 18 | const entries = Object.entries(obj); 19 | for (const [key, value] of entries) { 20 | const itemResult = callback(value, key); 21 | if (itemResult) return itemResult; 22 | const subResult = traverseJSON(value, callback); 23 | if (subResult) return subResult; 24 | } 25 | } 26 | } 27 | 28 | export function isObject(obj: unknown): obj is JsonObject { 29 | return typeof obj === 'object' && !Array.isArray(obj); 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youtube-websocket", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "MINIFLARE_SUBREQUEST_LIMIT=99999 wrangler dev --local", 6 | "deploy": "wrangler publish", 7 | "lint": "eslint src --fix", 8 | "test": "ts-node ./test/websocket" 9 | }, 10 | "dependencies": { 11 | "itty-router": "^2.6.6", 12 | "neverthrow": "^5.0.1", 13 | "youtubei.js": "^2.3.2" 14 | }, 15 | "devDependencies": { 16 | "@cloudflare/workers-types": "^3.17.0", 17 | "@types/ws": "^8.5.3", 18 | "@typescript-eslint/eslint-plugin": "^5.40.1", 19 | "@typescript-eslint/parser": "^5.40.1", 20 | "eslint": "^8.25.0", 21 | "eslint-config-prettier": "^8.5.0", 22 | "eslint-plugin-neverthrow": "^1.1.4", 23 | "eslint-plugin-prettier": "^4.2.1", 24 | "prettier": "^2.7.1", 25 | "ts-node": "^10.9.1", 26 | "typescript": "^4.8.4", 27 | "wrangler": "2.1.12", 28 | "ws": "^8.9.0" 29 | }, 30 | "private": true, 31 | "packageManager": "yarn@3.2.3", 32 | "resolutions": { 33 | "youtubei.js@^2.3.2": "patch:youtubei.js@npm%3A2.3.2#./.yarn/patches/youtubei.js-npm-2.3.2-3338aebb89.patch" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # youtube-websocket 2 | 3 | A Cloudflare Worker for proxying YouTube Live Chat over WebSockets. 4 | 5 | ## Usage 6 | 7 | If you have a channel ID, (*i.e. `"LudwigAhgren"` or `"UCRAEUAmW9kletIzOxhpLRFw"`*): 8 | 9 | - `wss://your-worker-endpoint.workers.dev/c/:channelId` 10 | 11 | If you have a video ID, (*i.e. `"dAiqTo3N8MU"`*): 12 | 13 | - `wss://your-worker-endpoint.workers.dev/s/:videoId` 14 | 15 | Once connected, you will receive JSON encoded messages for each chat event, with the following type definition: 16 | 17 | ```typescript 18 | type ChatEvent = { 19 | type: 'message'; 20 | id: string; // Chat message ID 21 | message: string; // Chat message text 22 | author: { 23 | name: string; // Author YouTube channel name 24 | id: string; // Author YouTube channel ID 25 | badges: { 26 | tooltip: string; // The tooltip shown when hovering over the badge 27 | type: 'icon' | 'custom'; 28 | // When type === 'icon', this is the name of the badge icon. 29 | // When type === 'custom', this is the URL of the badge image. 30 | badge: string; 31 | }[]; 32 | }; 33 | unix: number; // The unix timestamp of the message 34 | }; 35 | ``` 36 | -------------------------------------------------------------------------------- /src/adapters/truffle/users.ts: -------------------------------------------------------------------------------- 1 | import { Emote } from './emotes'; 2 | export interface SerializedMetadata { 3 | /** 4 | * Youtube ID 5 | */ 6 | _: string; 7 | /** 8 | * Display name 9 | */ 10 | a?: string; 11 | /** 12 | * Subbed months 13 | */ 14 | b?: number; 15 | /** 16 | * Name color 17 | */ 18 | c?: string; 19 | 20 | /** 21 | * Encoded Emote indices 22 | */ 23 | d?: string; 24 | 25 | /** 26 | * Spore badge slugs 27 | */ 28 | e?: string[]; 29 | 30 | /** 31 | * Has Spore Collectible 32 | */ 33 | f?: boolean; 34 | 35 | /** 36 | * Spore Chat highlight message powerup color 37 | */ 38 | g?: string; 39 | } 40 | 41 | export interface SporeUserInfo { 42 | /** 43 | * Youtube ID 44 | */ 45 | id?: string; 46 | 47 | /** 48 | * Owned Emote slugs 49 | */ 50 | emotes?: string[]; 51 | 52 | /** 53 | * badges 54 | */ 55 | badges?: string[]; 56 | 57 | /** 58 | * Serialized Emotes for the Youtube emojiManager 59 | */ 60 | serializedEmotes?: Emote[]; 61 | 62 | /** 63 | * Spore Display name 64 | */ 65 | name?: string; 66 | 67 | /** 68 | * Spore user months subbed 69 | */ 70 | subbedMonths?: number; 71 | 72 | /** 73 | * hex color for name color 74 | */ 75 | nameColor?: string; 76 | } 77 | 78 | export type UserInfo = Omit; 79 | 80 | export type InfoMap = [string, UserInfo][]; 81 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { IHTTPMethods, Router } from 'itty-router'; 2 | import { getChannel } from './routes/channel'; 3 | import { getStream } from './routes/stream'; 4 | import { HandlerResult } from '@util/types'; 5 | import { notFound } from '@util/util'; 6 | export { YoutubeChat } from './YoutubeChat'; 7 | 8 | export interface Env { 9 | YOUTUBE_CHAT: DurableObjectNamespace; 10 | 11 | // Variables 12 | TRUFFLE_API_BASE: string; 13 | } 14 | 15 | function route(request: Request, env: Env): Promise { 16 | const router = Router(); 17 | 18 | router.get('/c/:id', getChannel); 19 | router.get('/s/:id', getStream); 20 | router.get('/v/:id', getStream); 21 | router.all('*', () => notFound); 22 | 23 | return router.handle(request, env); 24 | } 25 | 26 | const handler: ExportedHandler = { 27 | async fetch(request, env) { 28 | try { 29 | const result = await route(request, env); 30 | if (result.isOk()) { 31 | return result.value; 32 | } else { 33 | const [message, status] = result.error; 34 | return new Response(message, { status }); 35 | } 36 | } catch (error) { 37 | console.error(error); 38 | if (error instanceof Response) { 39 | return error; 40 | } else { 41 | return new Response(String(error) || 'Internal Server Error', { 42 | status: 500, 43 | }); 44 | } 45 | } 46 | }, 47 | }; 48 | 49 | export default handler; 50 | -------------------------------------------------------------------------------- /src/routes/channel.ts: -------------------------------------------------------------------------------- 1 | import { err, ok } from 'neverthrow'; 2 | import { Handler } from '@util/types'; 3 | import { notFound, traverseJSON } from '@util/util'; 4 | import { getVideoData } from '@util/youtube'; 5 | import { createChatObject } from '../YoutubeChat'; 6 | // import Innertube from 'youtubei.js/dist'; 7 | 8 | export const getChannel: Handler<{ id: string }> = async (request, env) => { 9 | if (!request.params.id) return notFound; 10 | 11 | // const innertube = await Innertube.create(); 12 | // innertube.get 13 | // const chan = await innertube.getChannel(request.params.id); 14 | // console.log(chan); 15 | 16 | const urls = getChannelLiveUrl(request.params.id); 17 | 18 | const videoData = await getVideoData(urls); 19 | if (videoData.isErr()) return videoData; 20 | 21 | const { initialData } = videoData.value; 22 | 23 | const videoId = traverseJSON(initialData, (value, key) => { 24 | if (key === 'currentVideoEndpoint') { 25 | return value?.watchEndpoint?.videoId; 26 | } 27 | }); 28 | 29 | if (!videoId) return err(['Stream not found', 404]); 30 | 31 | const res = await createChatObject(videoId, videoData.value, request, env); 32 | return ok(res); 33 | }; 34 | 35 | function getChannelLiveUrl(channelId: string) { 36 | if (channelId.startsWith('@')) { 37 | return [`https://www.youtube.com/${channelId}/live`]; 38 | } 39 | const isId = /^UC.{22}$/.test(channelId); 40 | let urlParts: string[]; 41 | if (isId) urlParts = ['channel', 'c', 'user']; 42 | else urlParts = ['c', 'user', 'channel']; 43 | return urlParts.map( 44 | (part) => `https://www.youtube.com/${part}/${channelId}/live` 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/adapters/truffle/aliases.ts: -------------------------------------------------------------------------------- 1 | import { LiveChatAction } from '@util/types'; 2 | import { parseYTString } from '@util/youtube'; 3 | import { getUsernameColor } from './hash'; 4 | import { UserInfo } from './users'; 5 | 6 | export type CustomBadge = { 7 | url: string; 8 | months?: number; 9 | slug?: string; 10 | }; 11 | 12 | export type MonthBadge = Omit, 'slug'>; 13 | export type SporeBadge = Omit, 'months'>; 14 | 15 | export const isMonthBadge = ( 16 | customBadge: CustomBadge | undefined 17 | ): customBadge is MonthBadge => { 18 | return customBadge?.months !== undefined; 19 | }; 20 | 21 | export const isSporeBadge = ( 22 | customBadge: CustomBadge | undefined 23 | ): customBadge is SporeBadge => { 24 | return customBadge?.slug !== undefined; 25 | }; 26 | 27 | function getSporeUserBadges( 28 | userInfo: UserInfo | undefined, 29 | sporeYoutubeBadges: Map 30 | ) { 31 | const youtubeBadges: string[] = []; 32 | 33 | if (userInfo) { 34 | const badges = new Set(userInfo?.e ?? []); 35 | 36 | for (const badgeSlug of badges) { 37 | const image = sporeYoutubeBadges.get(badgeSlug); 38 | 39 | if (image) { 40 | youtubeBadges.push(image); 41 | } 42 | } 43 | } 44 | 45 | return youtubeBadges; 46 | } 47 | type LiveChatTextMessageRenderer = 48 | LiveChatAction[string]['item']['liveChatTextMessageRenderer']; 49 | function getUsername( 50 | message: LiveChatTextMessageRenderer, 51 | userInfo?: UserInfo 52 | ) { 53 | // Username 54 | if (userInfo?.a) { 55 | return decodeURIComponent(userInfo.a); 56 | } 57 | return parseYTString(message.authorName); 58 | } 59 | function getYoutubeBadges( 60 | message: LiveChatTextMessageRenderer, 61 | modBadgeImage: string 62 | ) { 63 | const badges: string[] = []; 64 | 65 | for (const badgeParent of message.authorBadges ?? []) { 66 | const badge = badgeParent.liveChatAuthorBadgeRenderer; 67 | 68 | if (badge.icon?.iconType === 'moderator') { 69 | badges.push(modBadgeImage); 70 | } else if (badge.customThumbnail?.thumbnails[0]) { 71 | badges.push(badge.customThumbnail?.thumbnails[0].url); 72 | } 73 | } 74 | return badges; 75 | } 76 | 77 | export function addAliasesToMessage( 78 | message: LiveChatTextMessageRenderer, 79 | users: Map, 80 | sporeYoutubeBadges: Map, 81 | modBadgeImage: string 82 | ) { 83 | const userInfo = users.get(message.authorExternalChannelId); 84 | const username = getUsername(message, userInfo); 85 | return { 86 | username, 87 | color: userInfo?.c ?? getUsernameColor(username), 88 | badges: [ 89 | ...getYoutubeBadges(message, modBadgeImage), 90 | ...getSporeUserBadges(userInfo, sporeYoutubeBadges), 91 | ], 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/util/youtube.ts: -------------------------------------------------------------------------------- 1 | import { Err, err, Ok, ok } from 'neverthrow'; 2 | import { 3 | Continuation, 4 | isTextRun, 5 | Json, 6 | JsonObject, 7 | Result, 8 | YTString, 9 | } from './types'; 10 | import { youtubeHeaders } from './util'; 11 | 12 | export type VideoData = { 13 | initialData: Json; 14 | config: YTConfig; 15 | }; 16 | 17 | export type YTConfig = { 18 | INNERTUBE_API_KEY: string; 19 | INNERTUBE_CONTEXT: Json; 20 | } & JsonObject; 21 | export async function getVideoData( 22 | urls: string[] 23 | ): Promise | Err> { 24 | let response: Response | undefined; 25 | for (const url of urls) { 26 | response = await fetch(url, { 27 | headers: youtubeHeaders, 28 | }); 29 | if (response.ok) break; 30 | } 31 | if (!response || response.status === 404) 32 | return err(['Stream not found', 404]); 33 | if (!response.ok) 34 | return err([ 35 | 'Failed to fetch stream: ' + response.statusText, 36 | response.status, 37 | ]); 38 | 39 | const text = await response.text(); 40 | 41 | const initialData = getMatch( 42 | text, 43 | /(?:window\s*\[\s*["']ytInitialData["']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;/ 44 | ); 45 | if (initialData.isErr()) return initialData; 46 | const config = getMatch(text, /(?:ytcfg.set)\(({[\s\S]+?})\)\s*;/); 47 | if (config.isErr()) return config; 48 | 49 | if (!config.value.INNERTUBE_API_KEY || !config.value.INNERTUBE_CONTEXT) 50 | return err(['Failed to load YouTube context', 500]); 51 | 52 | return ok({ initialData: initialData.value, config: config.value }); 53 | } 54 | 55 | function getMatch( 56 | html: string, 57 | pattern: RegExp 58 | ): Result { 59 | const match = pattern.exec(html); 60 | if (!match?.[1]) return err(['Failed to find video data', 404]); 61 | try { 62 | return ok(JSON.parse(match[1])); 63 | } catch { 64 | return err(['Failed to parse video data', 404]); 65 | } 66 | } 67 | 68 | export function getContinuationToken(continuation: Continuation) { 69 | const key = Object.keys(continuation)[0] as keyof Continuation; 70 | return continuation[key]?.continuation; 71 | } 72 | 73 | export function parseYTString(string?: YTString): string { 74 | if (!string) return ''; 75 | if (string.simpleText) return string.simpleText; 76 | if (string.runs) 77 | return string.runs 78 | .map((run) => { 79 | if (isTextRun(run)) { 80 | return run.text; 81 | } else { 82 | if (run.emoji.isCustomEmoji) { 83 | return ` ${ 84 | run.emoji.image.accessibility?.accessibilityData?.label ?? 85 | run.emoji.searchTerms[1] ?? 86 | run.emoji.searchTerms[0] 87 | } `; 88 | } else { 89 | return run.emoji.emojiId; 90 | } 91 | } 92 | }) 93 | .join('') 94 | .trim(); 95 | return ''; 96 | } 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | debug.out 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | 14 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 15 | 16 | # Runtime data 17 | 18 | pids 19 | _.pid 20 | _.seed 21 | \*.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | 29 | coverage 30 | \*.lcov 31 | 32 | # nyc test coverage 33 | 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | 42 | bower_components 43 | 44 | # node-waf configuration 45 | 46 | .lock-wscript 47 | 48 | # Compiled binary addons (https://nodejs.org/api/addons.html) 49 | 50 | build/Release 51 | 52 | # Dependency directories 53 | 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | 59 | web_modules/ 60 | 61 | # TypeScript cache 62 | 63 | \*.tsbuildinfo 64 | 65 | # Optional npm cache directory 66 | 67 | .npm 68 | 69 | # Optional eslint cache 70 | 71 | .eslintcache 72 | 73 | # Optional stylelint cache 74 | 75 | .stylelintcache 76 | 77 | # Microbundle cache 78 | 79 | .rpt2_cache/ 80 | .rts2_cache_cjs/ 81 | .rts2_cache_es/ 82 | .rts2_cache_umd/ 83 | 84 | # Optional REPL history 85 | 86 | .node_repl_history 87 | 88 | # Output of 'npm pack' 89 | 90 | \*.tgz 91 | 92 | # Yarn Integrity file 93 | 94 | .yarn-integrity 95 | 96 | # dotenv environment variable files 97 | 98 | .env 99 | .env.development.local 100 | .env.test.local 101 | .env.production.local 102 | .env.local 103 | 104 | # parcel-bundler cache (https://parceljs.org/) 105 | 106 | .cache 107 | .parcel-cache 108 | 109 | # Next.js build output 110 | 111 | .next 112 | out 113 | 114 | # Nuxt.js build / generate output 115 | 116 | .nuxt 117 | dist 118 | 119 | # Gatsby files 120 | 121 | .cache/ 122 | 123 | # Comment in the public line in if your project uses Gatsby and not Next.js 124 | 125 | # https://nextjs.org/blog/next-9-1#public-directory-support 126 | 127 | # public 128 | 129 | # vuepress build output 130 | 131 | .vuepress/dist 132 | 133 | # vuepress v2.x temp and cache directory 134 | 135 | .temp 136 | .cache 137 | 138 | # Docusaurus cache and generated files 139 | 140 | .docusaurus 141 | 142 | # Serverless directories 143 | 144 | .serverless/ 145 | 146 | # FuseBox cache 147 | 148 | .fusebox/ 149 | 150 | # DynamoDB Local files 151 | 152 | .dynamodb/ 153 | 154 | # TernJS port file 155 | 156 | .tern-port 157 | 158 | # Stores VSCode versions used for testing VSCode extensions 159 | 160 | .vscode-test 161 | 162 | # yarn v2 163 | 164 | .yarn/cache 165 | .yarn/unplugged 166 | .yarn/build-state.yml 167 | .yarn/install-state.gz 168 | .pnp.\* 169 | 170 | # wrangler project 171 | 172 | .dev.vars 173 | -------------------------------------------------------------------------------- /src/adapters/subathon.ts: -------------------------------------------------------------------------------- 1 | import { ChatItemRenderer, LiveChatAction } from '@util/types'; 2 | import { parseYTString } from '@util/youtube'; 3 | import { MessageAdapter } from '.'; 4 | 5 | export type ChatEvent = 6 | | { 7 | type: 'member'; 8 | id: string; 9 | author: { 10 | id: string; 11 | name: string; 12 | }; 13 | unix: number; 14 | } 15 | | { 16 | type: 'superchat'; 17 | id: string; 18 | message: string; 19 | amount: { 20 | // cents: number; 21 | text: string; 22 | }; 23 | author: { 24 | name: string; 25 | id: string; 26 | }; 27 | unix: number; 28 | } 29 | | { 30 | type: 'membergift'; 31 | id: string; 32 | recipient: { 33 | id: string; 34 | name: string; 35 | }; 36 | gifter: string; 37 | unix: number; 38 | }; 39 | 40 | function parseUnix(unix: string): number { 41 | return Math.round(Number(unix) / 1000); 42 | } 43 | 44 | export class SubathonMessageAdapter extends MessageAdapter { 45 | public sockets = new Set(); 46 | 47 | transform(action: LiveChatAction): string | undefined { 48 | const parsed = this.parseAction(action); 49 | if (parsed) return JSON.stringify(parsed); 50 | } 51 | 52 | protected parseAction(data: LiveChatAction): ChatEvent | undefined { 53 | delete data.clickTrackingParams; 54 | const actionType = Object.keys(data)[0] as keyof LiveChatAction; 55 | const action = data[actionType]?.item; 56 | if (!action) return; 57 | const rendererType = Object.keys(action)[0] as keyof ChatItemRenderer; 58 | switch (rendererType) { 59 | case 'liveChatMembershipItemRenderer': { 60 | const renderer = action[rendererType]; 61 | return { 62 | type: 'member', 63 | id: renderer.id, 64 | author: { 65 | id: renderer.authorExternalChannelId, 66 | name: parseYTString(renderer.authorName), 67 | }, 68 | unix: parseUnix(renderer.timestampUsec), 69 | }; 70 | } 71 | case 'liveChatPaidMessageRenderer': { 72 | const renderer = action[rendererType]; 73 | return { 74 | type: 'superchat', 75 | id: renderer.id, 76 | message: parseYTString(renderer.message), 77 | author: { 78 | id: renderer.authorExternalChannelId, 79 | name: parseYTString(renderer.authorName), 80 | }, 81 | amount: { 82 | // cents: renderer.purchaseAmountMicros / 1000000, 83 | text: parseYTString(renderer.purchaseAmountText), 84 | }, 85 | unix: parseUnix(renderer.timestampUsec), 86 | }; 87 | } 88 | case 'liveChatSponsorshipsGiftRedemptionAnnouncementRenderer': { 89 | const renderer = action[rendererType]; 90 | const messageRuns = renderer.message.runs; 91 | let gifter = 'Unknown'; 92 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 93 | if (messageRuns?.length === 2 && 'text' in messageRuns[1]!) { 94 | gifter = messageRuns[1].text; 95 | } 96 | return { 97 | type: 'membergift', 98 | id: renderer.id, 99 | recipient: { 100 | id: renderer.authorExternalChannelId, 101 | name: parseYTString(renderer.authorName), 102 | }, 103 | gifter, 104 | unix: parseUnix(renderer.timestampUsec), 105 | }; 106 | } 107 | // default: { 108 | // console.log(rendererType, action[rendererType]); 109 | // return; 110 | // } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/adapters/json.ts: -------------------------------------------------------------------------------- 1 | import { ChatItemRenderer, LiveChatAction } from '@util/types'; 2 | import { parseYTString } from '@util/youtube'; 3 | import { MessageAdapter } from '.'; 4 | 5 | export type ChatEvent = 6 | | { 7 | type: 'message'; 8 | id: string; 9 | message: string; 10 | author: { 11 | name: string; 12 | id: string; 13 | badges: { 14 | tooltip: string; 15 | type: 'icon' | 'custom'; 16 | badge: string; 17 | }[]; 18 | }; 19 | unix: number; 20 | } 21 | | { 22 | type: 'member'; 23 | id: string; 24 | author: { 25 | id: string; 26 | name: string; 27 | }; 28 | unix: number; 29 | } 30 | | { 31 | type: 'superchat'; 32 | id: string; 33 | message: string; 34 | amount: { 35 | // cents: number; 36 | text: string; 37 | }; 38 | author: { 39 | name: string; 40 | id: string; 41 | }; 42 | unix: number; 43 | }; 44 | 45 | function parseUnix(unix: string): number { 46 | return Math.round(Number(unix) / 1000); 47 | } 48 | 49 | export class JSONMessageAdapter extends MessageAdapter { 50 | public sockets = new Set(); 51 | 52 | transform(action: LiveChatAction): string | undefined { 53 | const parsed = this.parseAction(action); 54 | if (parsed) return JSON.stringify(parsed); 55 | } 56 | 57 | protected parseAction(data: LiveChatAction): ChatEvent | undefined { 58 | delete data.clickTrackingParams; 59 | const actionType = Object.keys(data)[0] as keyof LiveChatAction; 60 | const action = data[actionType]?.item; 61 | if (!action) return; 62 | const rendererType = Object.keys(action)[0] as keyof ChatItemRenderer; 63 | switch (rendererType) { 64 | case 'liveChatTextMessageRenderer': { 65 | const renderer = action[rendererType]; 66 | return { 67 | type: 'message', 68 | message: parseYTString(renderer.message), 69 | id: renderer.id, 70 | author: { 71 | id: renderer.authorExternalChannelId, 72 | name: parseYTString(renderer.authorName), 73 | badges: 74 | renderer.authorBadges?.map( 75 | ({ liveChatAuthorBadgeRenderer: badge }) => ({ 76 | tooltip: badge.tooltip, 77 | type: badge.icon ? 'icon' : 'custom', 78 | badge: badge.icon 79 | ? badge.icon.iconType 80 | : badge.customThumbnail?.thumbnails?.[0]?.url ?? '', 81 | }) 82 | ) ?? [], 83 | }, 84 | unix: parseUnix(renderer.timestampUsec), 85 | }; 86 | } 87 | case 'liveChatMembershipItemRenderer': { 88 | const renderer = action[rendererType]; 89 | return { 90 | type: 'member', 91 | id: renderer.id, 92 | author: { 93 | id: renderer.authorExternalChannelId, 94 | name: parseYTString(renderer.authorName), 95 | }, 96 | unix: parseUnix(renderer.timestampUsec), 97 | }; 98 | } 99 | case 'liveChatPaidMessageRenderer': { 100 | const renderer = action[rendererType]; 101 | return { 102 | type: 'superchat', 103 | id: renderer.id, 104 | message: parseYTString(renderer.message), 105 | author: { 106 | id: renderer.authorExternalChannelId, 107 | name: parseYTString(renderer.authorName), 108 | }, 109 | amount: { 110 | // cents: renderer.purchaseAmountMicros / 1000000, 111 | text: parseYTString(renderer.purchaseAmountText), 112 | }, 113 | unix: parseUnix(renderer.timestampUsec), 114 | }; 115 | } 116 | // default: { 117 | // console.log(rendererType, action[rendererType]); 118 | // return; 119 | // } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/util/types.ts: -------------------------------------------------------------------------------- 1 | import type { Err, Ok } from 'neverthrow'; 2 | import { Env } from '..'; 3 | 4 | export type Continuation = { 5 | [key in T]: { 6 | continuation: string; 7 | clickTrackingParams: string; 8 | }; 9 | }; 10 | 11 | export type LiveChatResponse = { 12 | continuationContents?: { 13 | liveChatContinuation: { 14 | continuations?: Continuation[]; 15 | actions?: LiveChatAction[]; 16 | }; 17 | }; 18 | }; 19 | 20 | export type TextRun = { 21 | text: string; 22 | bold?: boolean; 23 | italics?: boolean; 24 | }; 25 | export type EmojiRun = { 26 | emoji: { 27 | emojiId: string; 28 | shortcuts: string[]; 29 | searchTerms: string[]; 30 | image: YTImage; 31 | isCustomEmoji?: boolean; 32 | }; 33 | }; 34 | 35 | export function isTextRun(run: unknown): run is TextRun { 36 | return (run as TextRun)?.text !== undefined; 37 | } 38 | 39 | export type YTString = { 40 | simpleText?: string; 41 | runs?: (TextRun | EmojiRun)[]; 42 | }; 43 | export type YTImage = { 44 | thumbnails: { 45 | height?: number; 46 | width?: number; 47 | url: string; 48 | }[]; 49 | accessibility?: { 50 | accessibilityData: { 51 | label: string; 52 | }; 53 | }; 54 | webThumbnailDetailsExtensionData?: { 55 | isPreloaded: boolean; 56 | }; 57 | }; 58 | export type ChatItemRenderer = { 59 | liveChatPaidMessageRenderer: { 60 | id: string; 61 | timestampUsec: string; 62 | authorName: YTString; 63 | authorPhoto: YTImage; 64 | purchaseAmountText: YTString; 65 | message: YTString; 66 | headerBackgroundColor: number; 67 | headerTextColor: number; 68 | bodyBackgroundColor: number; 69 | bodyTextColor: number; 70 | authorExternalChannelId: string; 71 | authorNameTextColor: number; 72 | timestampColor: number; 73 | textInputBackgroundColor: number; 74 | }; 75 | liveChatTickerPaidMessageItemRenderer: { 76 | id: string; 77 | amount: YTString; 78 | amountTextColor: number; 79 | startBackgroundColor: number; 80 | endBackgroundColor: number; 81 | authorPhoto: YTImage; 82 | durationSec: number; 83 | authorExternalChannelId: string; 84 | fullDurationSec: number; 85 | }; 86 | liveChatMembershipItemRenderer: { 87 | id: string; 88 | timestampUsec: string; 89 | authorExternalChannelId: string; 90 | headerSubtext: YTString; 91 | authorName: YTString; 92 | authorPhoto: YTImage; 93 | authorBadges: YTBadge[]; 94 | }; 95 | liveChatTickerSponsorItemRenderer: { 96 | id: string; 97 | detailText: YTString; 98 | detailTextColor: number; 99 | startBackgroundColor: number; 100 | endBackgroundColor: number; 101 | sponsorPhoto: YTImage; 102 | durationSec: number; 103 | authorExternalChannelId: string; 104 | fullDurationSec: number; 105 | }; 106 | liveChatTextMessageRenderer: { 107 | id: string; 108 | message: YTString; 109 | authorName: YTString; 110 | authorPhoto: YTImage; 111 | timestampUsec: string; 112 | authorExternalChannelId: string; 113 | authorBadges?: YTBadge[]; 114 | }; 115 | liveChatAutoModMessageRenderer: unknown; 116 | liveChatLegacyPaidMessageRenderer: unknown; 117 | liveChatPaidStickerRenderer: unknown; 118 | liveChatDonationAnnouncementRenderer: unknown; 119 | liveChatModeChangeMessageRenderer: unknown; 120 | liveChatModerationMessageRenderer: unknown; 121 | liveChatPlaceholderItemRenderer: unknown; 122 | liveChatPurchasedProductMessageRenderer: unknown; 123 | liveChatSponsorshipsGiftPurchaseAnnouncementRenderer: unknown; 124 | liveChatSponsorshipsGiftRedemptionAnnouncementRenderer: { 125 | id: string; 126 | timestampUsec: string; 127 | authorExternalChannelId: string; 128 | authorName: YTString; 129 | authorPhoto: YTImage; 130 | message: YTString; 131 | }; 132 | liveChatViewerEngagementMessageRenderer: unknown; 133 | liveChatTickerPaidStickerItemRenderer: unknown; 134 | }; 135 | 136 | export type YTBadge = { 137 | liveChatAuthorBadgeRenderer: { 138 | customThumbnail?: YTImage; 139 | icon?: { 140 | iconType: 'VERIFIED' | 'MODERATOR' | string; 141 | }; 142 | tooltip: string; 143 | accessibility: { 144 | accessibilityData: { 145 | label: string; 146 | }; 147 | }; 148 | }; 149 | }; 150 | 151 | export type LiveChatAction = { 152 | [action in Action]: { 153 | item: ChatItemRenderer; 154 | }; 155 | } & { 156 | clickTrackingParams?: string; 157 | }; 158 | 159 | export type Handler = Record> = 160 | (request: Request & { params: T }, env: Env) => Promise; 161 | export type HandlerResult = Result; 162 | 163 | export type Result = Ok | Err; 164 | 165 | export type Json = JsonPrimitive | JsonArray | JsonObject; 166 | type JsonPrimitive = null | boolean | number | string; 167 | type JsonArray = Json[]; 168 | export type JsonObject = { 169 | [key: string]: Json; 170 | }; 171 | -------------------------------------------------------------------------------- /src/adapters/truffle/emotes.ts: -------------------------------------------------------------------------------- 1 | import { isTextRun, LiveChatAction } from '@util/types'; 2 | import { TruffleChatEvent } from '../truffle'; 3 | 4 | export enum EmoteProvider { 5 | Twitch, 6 | FFZ, 7 | BTTV, 8 | Custom, 9 | Spore, 10 | } 11 | 12 | type SporeFileExtensions = 13 | | 'png' 14 | | 'webp' 15 | | 'jpg' 16 | | 'jpeg' 17 | | 'gif' 18 | | 'svg' 19 | | 'mp4' 20 | | 'webm' 21 | | 'h264' 22 | | 'ogv' 23 | | 'mov' 24 | | 'obj' 25 | | 'mtl' 26 | | 'glb' 27 | | 'gltf' 28 | | 'json'; 29 | 30 | export type SporeEmote = { 31 | id: string; 32 | slug: string; 33 | bitIndex: number; 34 | channelId: string; 35 | ext: SporeFileExtensions; 36 | }; 37 | 38 | export type Emote = { 39 | provider: EmoteProvider; 40 | id: string; 41 | name: string; 42 | ext?: string; 43 | bitIndex?: number; 44 | channelId?: string; 45 | }; 46 | 47 | export function getEmoteImage( 48 | emote: Emote, 49 | apiBase: string 50 | ): string | undefined { 51 | if (emote.provider === EmoteProvider.Twitch) { 52 | return `https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/static/dark/1.0`; 53 | } else if (emote.provider === EmoteProvider.FFZ) { 54 | return `https://cdn.frankerfacez.com/emote/${emote.id}/1`; 55 | } else if (emote.provider === EmoteProvider.BTTV) { 56 | return `https://cdn.betterttv.net/emote/${emote.id}/1x`; 57 | } else if (emote.provider === EmoteProvider.Spore && emote?.ext) { 58 | return `https://cdn.bio/ugc/collectible/${emote.id}.tiny.${emote.ext}`; 59 | } else if (emote.provider === EmoteProvider.Custom) { 60 | return `${apiBase}/emotes/${emote.id}`; 61 | } else { 62 | return undefined; 63 | } 64 | } 65 | 66 | const splitPattern = /[\s.,?!]/; 67 | function splitWords(string: string): string[] { 68 | const result: string[] = []; 69 | let startOfMatch = 0; 70 | for (let i = 0; i < string.length - 1; i++) { 71 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 72 | if (splitPattern.test(string[i]!) !== splitPattern.test(string[i + 1]!)) { 73 | result.push(string.substring(startOfMatch, i + 1)); 74 | startOfMatch = i + 1; 75 | } 76 | } 77 | result.push(string.substring(startOfMatch)); 78 | return result; 79 | } 80 | 81 | export function addEmotesToMessage( 82 | message: LiveChatAction[string]['item']['liveChatTextMessageRenderer'], 83 | getEmote: (name: string, userId: string) => string | undefined 84 | ): TruffleChatEvent['message'] { 85 | const userId = message.authorExternalChannelId; 86 | 87 | const runs: TruffleChatEvent['message'] = []; 88 | for (const run of message.message.runs ?? []) { 89 | if (isTextRun(run)) { 90 | const { text } = run; 91 | let index = 0; 92 | let startOfText = 0; 93 | const words = splitWords(text); 94 | let hasEmote = false; 95 | for (const word of words) { 96 | const emote = 97 | word === '🌝' ? getEmote('Kappa', userId) : getEmote(word, userId); 98 | if (emote) { 99 | hasEmote = true; 100 | if (index > 0) runs.push(text.substring(startOfText, index)); 101 | runs.push({ 102 | emoji: emote, 103 | }); 104 | startOfText = index + word.length; 105 | } 106 | index += word.length; 107 | } 108 | if (hasEmote) { 109 | runs.push(text.substring(startOfText, index)); 110 | } else { 111 | runs.push(run.text); 112 | } 113 | } else { 114 | // Handle Kappa 115 | if (run.emoji.emojiId === '🌝') { 116 | const emote = getEmote('Kappa', userId); 117 | if (emote) runs.push({ emoji: emote }); 118 | } else if (isTextRun(run)) { 119 | runs.push(run.text); 120 | } else { 121 | runs.push({ 122 | emoji: run.emoji.image.thumbnails[0]?.url ?? '', 123 | }); 124 | } 125 | } 126 | } 127 | return runs; 128 | } 129 | 130 | export function decodeEmotes( 131 | encodedEmotes: string, 132 | emoteIndicesMap: Map, 133 | preferCache = true 134 | ): number[] { 135 | const get = () => { 136 | const characters: number[] = []; 137 | for (let i = 0; i < encodedEmotes.length; i++) { 138 | characters[encodedEmotes.length - 1 - i] = 139 | encodedEmotes.charCodeAt(i) - 35; 140 | } 141 | const finalArray: number[] = []; 142 | for (let i = 0; i < characters.length; i++) { 143 | if (characters[i] === 0) continue; 144 | 145 | for (let bit = 0; bit < 6; bit++) { 146 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 147 | const bitMatches = (characters[i]! & (1 << bit)) !== 0; 148 | if (bitMatches) { 149 | finalArray.push(i * 6 + bit); 150 | } 151 | } 152 | } 153 | return finalArray; 154 | }; 155 | 156 | if (preferCache) { 157 | const cachedValue = emoteIndicesMap.get(encodedEmotes); 158 | 159 | if (cachedValue) return cachedValue; 160 | 161 | const decodedValue = get(); 162 | emoteIndicesMap.set(encodedEmotes, decodedValue); 163 | return decodedValue; 164 | } else { 165 | return get(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/adapters/truffle.ts: -------------------------------------------------------------------------------- 1 | import { ChatItemRenderer, LiveChatAction } from '@util/types'; 2 | import { Env } from 'src'; 3 | import { MessageAdapter } from '.'; 4 | import { 5 | addAliasesToMessage, 6 | CustomBadge, 7 | isSporeBadge, 8 | } from './truffle/aliases'; 9 | import { 10 | addEmotesToMessage, 11 | decodeEmotes, 12 | Emote, 13 | EmoteProvider, 14 | getEmoteImage, 15 | } from './truffle/emotes'; 16 | import { InfoMap, UserInfo } from './truffle/users'; 17 | 18 | export type TruffleChatEvent = { 19 | type: 'message'; 20 | id: string; 21 | message: (string | { emoji: string })[]; 22 | author: { 23 | name: string; 24 | color: string; 25 | id: string; 26 | badges: string[]; 27 | }; 28 | unix: number; 29 | }; 30 | 31 | export class TruffleMessageAdapter extends MessageAdapter { 32 | public sockets = new Set(); 33 | 34 | constructor(private env: Env, private youtubeChannelId: string) { 35 | super(); 36 | this.fetchAll(); 37 | } 38 | 39 | private emoteMap = new Map< 40 | string, 41 | { image: string; isSporeEmote: boolean } 42 | >(); 43 | private encodedEmoteMap = new Map(); 44 | private emoteIndicesMap = new Map(); 45 | private users = new Map(); 46 | private sporeYoutubeBadges = new Map(); 47 | 48 | private lastFetched = Date.now(); 49 | async fetchAll() { 50 | this.lastFetched = Date.now(); 51 | fetch(`${this.env.TRUFFLE_API_BASE}/gateway/emotes`) 52 | .then((res) => res.json()) 53 | .then((body) => { 54 | for (const emote of body) { 55 | const image = getEmoteImage(emote, this.env.TRUFFLE_API_BASE); 56 | if (!image) continue; 57 | if (image) { 58 | this.emoteMap.set(emote.name, { 59 | image, 60 | isSporeEmote: emote.provider === EmoteProvider.Spore, 61 | }); 62 | 63 | if (emote.bitIndex !== undefined) { 64 | this.encodedEmoteMap.set(emote.name, emote.bitIndex); 65 | } 66 | } 67 | } 68 | }); 69 | fetch( 70 | `${this.env.TRUFFLE_API_BASE}/gateway/users/c/${this.youtubeChannelId}` 71 | ) 72 | .then((res) => res.json()) 73 | .then((body) => { 74 | this.users = new Map(body); 75 | }); 76 | fetch(`${this.env.TRUFFLE_API_BASE}/gateway/badges`) 77 | .then((res) => res.json()) 78 | .then((body) => { 79 | const sporeBadges = body.filter(isSporeBadge); 80 | 81 | for (const badge of sporeBadges) { 82 | this.sporeYoutubeBadges.set(badge.slug, badge.url); 83 | } 84 | }); 85 | } 86 | 87 | transform(action: LiveChatAction): string | undefined { 88 | if (this.lastFetched < Date.now() - 1000 * 60) { 89 | this.fetchAll(); 90 | } 91 | const parsed = this.parseAction(action); 92 | if (parsed) return JSON.stringify(parsed); 93 | } 94 | 95 | protected parseAction(data: LiveChatAction): TruffleChatEvent | undefined { 96 | const actionType = Object.keys(data)[0] as keyof LiveChatAction; 97 | const action = data[actionType]?.item; 98 | if (!action) return; 99 | const rendererType = Object.keys(action)[0] as keyof ChatItemRenderer; 100 | switch (rendererType) { 101 | case 'liveChatTextMessageRenderer': { 102 | const renderer = action[rendererType]; 103 | const { badges, username, color } = addAliasesToMessage( 104 | renderer, 105 | this.users, 106 | this.sporeYoutubeBadges, 107 | 'https://overlay.truffle.vip/mod.png' 108 | ); 109 | return { 110 | type: 'message', 111 | message: addEmotesToMessage(renderer, (name, userId) => { 112 | const emote = this.emoteMap.get(name); 113 | const user = this.users.get(userId); 114 | /** 115 | * For spore emotes, we are defaulting to rendering the emote. It will only not render 116 | * the emote if the user spore emote map is cached on the client and the user doesn't own the emote. 117 | * If the message is from a non-extension user, it will not render a spore emote 118 | */ 119 | if (emote?.isSporeEmote) { 120 | if (user) { 121 | const encodedUserEmotes = user?.d; 122 | 123 | if (encodedUserEmotes) { 124 | const emoteIndex = this.encodedEmoteMap.get(name); 125 | const decodedUserEmoteIndices = decodeEmotes( 126 | encodedUserEmotes, 127 | this.emoteIndicesMap, 128 | true 129 | ); 130 | 131 | // if the user has an emote cache but doesn't own the emote, don't render the emote. 132 | if ( 133 | emoteIndex !== undefined && 134 | !decodedUserEmoteIndices.includes(emoteIndex) 135 | ) { 136 | return undefined; 137 | } 138 | } 139 | } 140 | } 141 | return emote?.image; 142 | }), 143 | id: renderer.id, 144 | author: { 145 | id: renderer.authorExternalChannelId, 146 | color, 147 | name: username, 148 | badges, 149 | }, 150 | unix: Math.round(Number(renderer.timestampUsec) / 1000), 151 | }; 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/YoutubeChat.ts: -------------------------------------------------------------------------------- 1 | import { IHTTPMethods, Router } from 'itty-router'; 2 | import { Env } from '.'; 3 | import { 4 | ChatItemRenderer, 5 | Continuation, 6 | LiveChatAction, 7 | LiveChatResponse, 8 | } from '@util/types'; 9 | import { traverseJSON } from '@util/util'; 10 | import { getContinuationToken, VideoData } from '@util/youtube'; 11 | import { MessageAdapter } from './adapters'; 12 | import { JSONMessageAdapter } from './adapters/json'; 13 | import { IRCMessageAdapter } from './adapters/irc'; 14 | import { RawMessageAdapter } from './adapters/raw'; 15 | import { TruffleMessageAdapter } from './adapters/truffle'; 16 | import { SubathonMessageAdapter } from './adapters/subathon'; 17 | 18 | const adapterMap: Record< 19 | string, 20 | (env: Env, channelId: string) => MessageAdapter 21 | > = { 22 | json: () => new JSONMessageAdapter(), 23 | irc: () => new IRCMessageAdapter(), 24 | truffle: (env, channelId) => new TruffleMessageAdapter(env, channelId), 25 | subathon: () => new SubathonMessageAdapter(), 26 | raw: () => new RawMessageAdapter(), 27 | }; 28 | 29 | type Handler = (request: Request) => Promise; 30 | 31 | export async function createChatObject( 32 | videoId: string, 33 | videoData: VideoData, 34 | req: Request, 35 | env: Env 36 | ): Promise { 37 | const id = env.YOUTUBE_CHAT.idFromName(videoId); 38 | const object = env.YOUTUBE_CHAT.get(id); 39 | 40 | const init = await object.fetch('http://youtube.chat/init', { 41 | method: 'POST', 42 | body: JSON.stringify(videoData), 43 | }); 44 | if (!init.ok) return init; 45 | 46 | const url = new URL(req.url); 47 | 48 | return object.fetch('http://youtube.chat/ws' + url.search, req); 49 | } 50 | 51 | const chatInterval = 250; 52 | 53 | export class YoutubeChat implements DurableObject { 54 | private router: Router; 55 | private channelId!: string; 56 | private initialData!: VideoData['initialData']; 57 | private config!: VideoData['config']; 58 | private seenMessages = new Map(); 59 | 60 | constructor(private state: DurableObjectState, private env: Env) { 61 | const r = Router(); 62 | this.router = r; 63 | r.post('/init', this.init); 64 | r.get('/ws', this.handleWebsocket); 65 | r.all('*', () => new Response('Not found', { status: 404 })); 66 | } 67 | 68 | private broadcast(data: LiveChatAction) { 69 | for (const adapter of this.adapters.values()) { 70 | const transformed = adapter.transform(data); 71 | if (!transformed) continue; 72 | for (const socket of adapter.sockets) { 73 | socket.send(transformed); 74 | } 75 | } 76 | } 77 | 78 | private initialized = false; 79 | private init: Handler = (req) => { 80 | return this.state.blockConcurrencyWhile(async () => { 81 | if (this.initialized) return new Response(); 82 | this.initialized = true; 83 | const data = await req.json(); 84 | this.config = data.config; 85 | this.initialData = data.initialData; 86 | this.channelId = traverseJSON(this.initialData, (value, key) => { 87 | if (key === 'channelNavigationEndpoint') { 88 | return value.browseEndpoint?.browseId; 89 | } 90 | }); 91 | const continuation = traverseJSON(data.initialData, (value) => { 92 | if (value.title === 'Live chat') { 93 | return value.continuation as Continuation; 94 | } 95 | }); 96 | 97 | if (!continuation) { 98 | this.initialized = false; 99 | return new Response('Failed to load chat', { 100 | status: 404, 101 | }); 102 | } 103 | 104 | const token = getContinuationToken(continuation); 105 | if (!token) { 106 | this.initialized = false; 107 | return new Response('Failed to load chat', { 108 | status: 404, 109 | }); 110 | } 111 | 112 | this.fetchChat(token); 113 | setInterval(() => this.clearSeenMessages(), 60 * 1000); 114 | 115 | return new Response(); 116 | }); 117 | }; 118 | 119 | private nextContinuationToken?: string; 120 | 121 | private async clearSeenMessages() { 122 | const cutoff = Date.now() - 1000 * 60; 123 | for (const message of this.seenMessages.entries()) { 124 | const [id, timestamp] = message; 125 | if (timestamp < cutoff) { 126 | this.seenMessages.delete(id); 127 | } 128 | } 129 | } 130 | 131 | private async fetchChat(continuationToken: string) { 132 | let nextToken = continuationToken; 133 | try { 134 | const res = await fetch( 135 | `https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=${this.config.INNERTUBE_API_KEY}`, 136 | { 137 | method: 'POST', 138 | body: JSON.stringify({ 139 | context: this.config.INNERTUBE_CONTEXT, 140 | continuation: continuationToken, 141 | webClientInfo: { isDocumentHidden: false }, 142 | }), 143 | } 144 | ); 145 | if (!res.ok) { 146 | throw new Error(res.statusText); 147 | } 148 | const data = await res.json(); 149 | const nextContinuation = 150 | data.continuationContents?.liveChatContinuation.continuations?.[0]; 151 | nextToken = 152 | (nextContinuation 153 | ? getContinuationToken(nextContinuation) 154 | : undefined) ?? continuationToken; 155 | 156 | // if data.continuationContents is undefined the stream is probably over 157 | const actions = 158 | data.continuationContents?.liveChatContinuation.actions ?? []; 159 | 160 | for (const action of actions) { 161 | const id = this.getId(action); 162 | if (id) { 163 | if (this.seenMessages.has(id)) continue; 164 | this.seenMessages.set(id, Date.now()); 165 | } 166 | this.broadcast(action); 167 | 168 | // const parsed = parseChatAction(action); 169 | // if (parsed) { 170 | // if (this.seenMessages.has(parsed.id)) continue; 171 | // this.seenMessages.set(parsed.id, parsed.unix); 172 | // this.broadcast(parsed); 173 | // } 174 | } 175 | } catch (e) { 176 | console.error(e); 177 | } finally { 178 | this.nextContinuationToken = nextToken; 179 | if (this.adapters.size > 0) 180 | setTimeout(() => this.fetchChat(nextToken), chatInterval); 181 | } 182 | } 183 | 184 | private getId(data: LiveChatAction) { 185 | delete data.clickTrackingParams; 186 | const actionType = Object.keys(data)[0] as keyof LiveChatAction; 187 | const action = data[actionType]?.item; 188 | if (!action) return; 189 | const rendererType = Object.keys(action)[0] as keyof ChatItemRenderer; 190 | const renderer = action[rendererType] as { id: string }; 191 | return renderer.id; 192 | } 193 | 194 | private adapters = new Map(); 195 | 196 | private makeAdapter(adapterType: string): MessageAdapter { 197 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 198 | const adapterFactory = adapterMap[adapterType] ?? adapterMap.json!; 199 | 200 | const cached = this.adapters.get(adapterType); 201 | if (cached) return cached; 202 | 203 | const adapter = adapterFactory(this.env, this.channelId); 204 | this.adapters.set(adapterType, adapter); 205 | return adapter; 206 | } 207 | 208 | private handleWebsocket: Handler = async (req) => { 209 | if (req.headers.get('Upgrade') !== 'websocket') 210 | return new Response('Expected a websocket', { status: 400 }); 211 | 212 | const url = new URL(req.url); 213 | const adapterType = url.searchParams.get('adapter') ?? 'json'; 214 | 215 | const pair = new WebSocketPair(); 216 | const ws = pair[1]; 217 | ws.accept(); 218 | 219 | const wsResponse = new Response(null, { 220 | status: 101, 221 | webSocket: pair[0], 222 | }); 223 | 224 | const adapter = this.makeAdapter(adapterType); 225 | adapter.sockets.add(ws); 226 | if (this.nextContinuationToken) this.fetchChat(this.nextContinuationToken); 227 | 228 | ws.addEventListener('close', () => { 229 | adapter.sockets.delete(ws); 230 | if (adapter.sockets.size === 0) this.adapters.delete(adapterType); 231 | }); 232 | 233 | return wsResponse; 234 | }; 235 | 236 | async fetch(req: Request): Promise { 237 | return this.router.handle(req); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /.yarn/patches/youtubei.js-npm-2.3.2-3338aebb89.patch: -------------------------------------------------------------------------------- 1 | diff --git a/bundle/browser.js b/bundle/browser.js 2 | index 8c47320a97cbd6b0689666fd1b6906156ed371a0..72950e9ef73d9494eabd9ff8a4f595a8920bbf51 100644 3 | --- a/bundle/browser.js 4 | +++ b/bundle/browser.js 5 | @@ -7409,8 +7409,7 @@ var package_default = { 6 | dependencies: { 7 | "@protobuf-ts/runtime": "^2.7.0", 8 | jintr: "^0.3.1", 9 | - linkedom: "^0.14.12", 10 | - undici: "^5.7.0" 11 | + linkedom: "^0.14.12" 12 | }, 13 | devDependencies: { 14 | "@protobuf-ts/plugin": "^2.7.0", 15 | diff --git a/bundle/browser.min.js b/bundle/browser.min.js 16 | index bb00361c9f42e68a3ef97e9ba1a0be735157bbf4..896e2d13fa3be400ebba513185b2d7497a8444e7 100644 17 | --- a/bundle/browser.min.js 18 | +++ b/bundle/browser.min.js 19 | @@ -15,7 +15,7 @@ Defaulting to 2020, but this will stop working in the future.`)),c.ecmaVersion=1 20 | `:t==="*"&&(u="*/"),o=r.indexOf(u,n+1+1),o!==-1)return o=o+u.length-1,i=r.substring(e,o+1),{idx:o,text:i};var l="css expression error: unfinished comment in expression!";return{error:l}}else return!1};Wi.CSSValueExpression.prototype._parseJSString=function(r,e,t){var i=this._findMatchedIdx(r,e,t),n;return i===-1?!1:(n=r.substring(e,i+t.length),{idx:i,text:n})};Wi.CSSValueExpression.prototype._parseJSRexExp=function(r,e){var t=r.substring(0,e).replace(/\s+$/,""),i=[/^$/,/\($/,/\[$/,/\!$/,/\+$/,/\-$/,/\*$/,/\/\s+/,/\%$/,/\=$/,/\>$/,/<$/,/\&$/,/\|$/,/\^$/,/\~$/,/\?$/,/\,$/,/delete$/,/in$/,/instanceof$/,/new$/,/typeof$/,/void$/],n=i.some(function(u){return u.test(t)});if(n){var o="/";return this._parseJSString(r,e,o)}else return!1};Wi.CSSValueExpression.prototype._findMatchedIdx=function(r,e,t){for(var i=e,n,o=-1;;)if(n=r.indexOf(t,i+1),n===-1){n=o;break}else{var u=r.substring(e+1,n),l=u.match(/\\+$/);if(!l||l[0]%2===0)break;i=n}var h=r.indexOf(` 21 | `,e+1);return h{var V1={};V1.MatcherList=s(function(){this.length=0},"MatcherList");V1.MatcherList.prototype={constructor:V1.MatcherList,get matcherText(){return Array.prototype.join.call(this,", ")},set matcherText(r){for(var e=r.split(","),t=this.length=e.length,i=0;i{var yr={CSSRule:Xt().CSSRule,MatcherList:Vx().MatcherList};yr.CSSDocumentRule=s(function(){yr.CSSRule.call(this),this.matcher=new yr.MatcherList,this.cssRules=[]},"CSSDocumentRule");yr.CSSDocumentRule.prototype=new yr.CSSRule;yr.CSSDocumentRule.prototype.constructor=yr.CSSDocumentRule;yr.CSSDocumentRule.prototype.type=10;Object.defineProperty(yr.CSSDocumentRule.prototype,"cssText",{get:function(){for(var r=[],e=0,t=this.cssRules.length;e{var Be={};Be.parse=s(function(e){for(var t=0,i="before-selector",n,o="",u=0,l={selector:!0,value:!0,"value-parenthesis":!0,atRule:!0,"importRule-begin":!0,importRule:!0,atBlock:!0,conditionBlock:!0,"documentRule-begin":!0},h=new Be.CSSStyleSheet,d=h,f,v=[],x=!1,E,N,F="",H,G,X,V,_,W,K,le,_e=/@(-(?:\w+-)+)?keyframes/g,Ue=s(function(Ei){var _t=e.substring(0,t).split(` 22 | `),Qt=_t.length,pt=_t.pop().length+1,Qe=new Error(Ei+" (line "+Qt+", char "+pt+")");throw Qe.line=Qt,Qe.char=pt,Qe.styleSheet=h,Qe},"parseError"),ce;ce=e.charAt(t);t++)switch(ce){case" ":case" ":case"\r":case` 23 | -`:case"\f":l[i]&&(o+=ce);break;case'"':n=t+1;do n=e.indexOf('"',n)+1,n||Ue('Unmatched "');while(e[n-2]==="\\");switch(o+=e.slice(t,n),t=n-1,i){case"before-value":i="value";break;case"importRule-begin":i="importRule";break}break;case"'":n=t+1;do n=e.indexOf("'",n)+1,n||Ue("Unmatched '");while(e[n-2]==="\\");switch(o+=e.slice(t,n),t=n-1,i){case"before-value":i="value";break;case"importRule-begin":i="importRule";break}break;case"/":e.charAt(t+1)==="*"?(t+=2,n=e.indexOf("*/",t),n===-1?Ue("Missing */"):t=n+1):o+=ce,i==="importRule-begin"&&(o+=" ",i="importRule");break;case"@":if(e.indexOf("@-moz-document",t)===t){i="documentRule-begin",K=new Be.CSSDocumentRule,K.__starts=t,t+=13,o="";break}else if(e.indexOf("@media",t)===t){i="atBlock",G=new Be.CSSMediaRule,G.__starts=t,t+=5,o="";break}else if(e.indexOf("@supports",t)===t){i="conditionBlock",X=new Be.CSSSupportsRule,X.__starts=t,t+=8,o="";break}else if(e.indexOf("@host",t)===t){i="hostRule-begin",t+=4,le=new Be.CSSHostRule,le.__starts=t,o="";break}else if(e.indexOf("@import",t)===t){i="importRule-begin",t+=6,o+="@import";break}else if(e.indexOf("@font-face",t)===t){i="fontFaceRule-begin",t+=9,_=new Be.CSSFontFaceRule,_.__starts=t,o="";break}else{_e.lastIndex=t;var Ae=_e.exec(e);if(Ae&&Ae.index===t){i="keyframesRule-begin",W=new Be.CSSKeyframesRule,W.__starts=t,W._vendorPrefix=Ae[1],t+=Ae[0].length-1,o="";break}else i==="selector"&&(i="atRule")}o+=ce;break;case"{":i==="selector"||i==="atRule"?(H.selectorText=o.trim(),H.style.__starts=t,o="",i="before-name"):i==="atBlock"?(G.media.mediaText=o.trim(),f&&v.push(f),d=f=G,G.parentStyleSheet=h,o="",i="before-selector"):i==="conditionBlock"?(X.conditionText=o.trim(),f&&v.push(f),d=f=X,X.parentStyleSheet=h,o="",i="before-selector"):i==="hostRule-begin"?(f&&v.push(f),d=f=le,le.parentStyleSheet=h,o="",i="before-selector"):i==="fontFaceRule-begin"?(f&&(_.parentRule=f),_.parentStyleSheet=h,H=_,o="",i="before-name"):i==="keyframesRule-begin"?(W.name=o.trim(),f&&(v.push(f),W.parentRule=f),W.parentStyleSheet=h,d=f=W,o="",i="keyframeRule-begin"):i==="keyframeRule-begin"?(H=new Be.CSSKeyframeRule,H.keyText=o.trim(),H.__starts=t,o="",i="before-name"):i==="documentRule-begin"&&(K.matcher.matcherText=o.trim(),f&&(v.push(f),K.parentRule=f),d=f=K,K.parentStyleSheet=h,o="",i="before-selector");break;case":":i==="name"?(N=o.trim(),o="",i="before-value"):o+=ce;break;case"(":if(i==="value")if(o.trim()==="expression"){var Ft=new Be.CSSValueExpression(e,t).parse();Ft.error?Ue(Ft.error):(o+=Ft.expression,t=Ft.idx)}else i="value-parenthesis",u=1,o+=ce;else i==="value-parenthesis"&&u++,o+=ce;break;case")":i==="value-parenthesis"&&(u--,u===0&&(i="value")),o+=ce;break;case"!":i==="value"&&e.indexOf("!important",t)===t?(F="important",t+=9):o+=ce;break;case";":switch(i){case"value":H.style.setProperty(N,o.trim(),F),F="",o="",i="before-name";break;case"atRule":o="",i="before-selector";break;case"importRule":V=new Be.CSSImportRule,V.parentStyleSheet=V.styleSheet.parentStyleSheet=h,V.cssText=o+ce,h.cssRules.push(V),o="",i="before-selector";break;default:o+=ce;break}break;case"}":switch(i){case"value":H.style.setProperty(N,o.trim(),F),F="";case"before-name":case"name":H.__ends=t+1,f&&(H.parentRule=f),H.parentStyleSheet=h,d.cssRules.push(H),o="",d.constructor===Be.CSSKeyframesRule?i="keyframeRule-begin":i="before-selector";break;case"keyframeRule-begin":case"before-selector":case"selector":for(f||Ue("Unexpected }"),x=v.length>0;v.length>0;){if(f=v.pop(),f.constructor.name==="CSSMediaRule"||f.constructor.name==="CSSSupportsRule"){E=d,d=f,d.cssRules.push(E);break}v.length===0&&(x=!1)}x||(d.__ends=t+1,h.cssRules.push(d),d=h,f=null),o="",i="before-selector";break}break;default:switch(i){case"before-selector":i="selector",H=new Be.CSSStyleRule,H.__starts=t;break;case"before-name":i="name";break;case"before-value":i="value";break;case"importRule-begin":i="importRule";break}o+=ce;break}return h},"parse");QC.parse=Be.parse;Be.CSSStyleSheet=yp().CSSStyleSheet;Be.CSSStyleRule=vp().CSSStyleRule;Be.CSSImportRule=Ox().CSSImportRule;Be.CSSGroupingRule=Ds().CSSGroupingRule;Be.CSSMediaRule=O1().CSSMediaRule;Be.CSSConditionRule=aa().CSSConditionRule;Be.CSSSupportsRule=L1().CSSSupportsRule;Be.CSSFontFaceRule=Lx().CSSFontFaceRule;Be.CSSHostRule=Dx().CSSHostRule;Be.CSSStyleDeclaration=Ls().CSSStyleDeclaration;Be.CSSKeyframeRule=D1().CSSKeyframeRule;Be.CSSKeyframesRule=B1().CSSKeyframesRule;Be.CSSValueExpression=Fx().CSSValueExpression;Be.CSSDocumentRule=Ux().CSSDocumentRule});var Ls=$(ZC=>{var ua={};ua.CSSStyleDeclaration=s(function(){this.length=0,this.parentRule=null,this._importants={}},"CSSStyleDeclaration");ua.CSSStyleDeclaration.prototype={constructor:ua.CSSStyleDeclaration,getPropertyValue:function(r){return this[r]||""},setProperty:function(r,e,t){if(this[r]){var i=Array.prototype.indexOf.call(this,r);i<0&&(this[this.length]=r,this.length++)}else this[this.length]=r,this.length++;this[r]=e+"",this._importants[r]=t},removeProperty:function(r){if(!(r in this))return"";var e=Array.prototype.indexOf.call(this,r);if(e<0)return"";var t=this[r];return this[r]="",Array.prototype.splice.call(this,e,1),t},getPropertyCSSValue:function(){},getPropertyPriority:function(r){return this._importants[r]||""},getPropertyShorthand:function(){},isPropertyImplicit:function(){},get cssText(){for(var r=[],e=0,t=this.length;e{var U1={CSSStyleSheet:yp().CSSStyleSheet,CSSRule:Xt().CSSRule,CSSStyleRule:vp().CSSStyleRule,CSSGroupingRule:Ds().CSSGroupingRule,CSSConditionRule:aa().CSSConditionRule,CSSMediaRule:O1().CSSMediaRule,CSSSupportsRule:L1().CSSSupportsRule,CSSStyleDeclaration:Ls().CSSStyleDeclaration,CSSKeyframeRule:D1().CSSKeyframeRule,CSSKeyframesRule:B1().CSSKeyframesRule};U1.clone=s(function r(e){var t=new U1.CSSStyleSheet,i=e.cssRules;if(!i)return t;for(var n=0,o=i.length;n{"use strict";Je.CSSStyleDeclaration=Ls().CSSStyleDeclaration;Je.CSSRule=Xt().CSSRule;Je.CSSGroupingRule=Ds().CSSGroupingRule;Je.CSSConditionRule=aa().CSSConditionRule;Je.CSSStyleRule=vp().CSSStyleRule;Je.MediaList=M1().MediaList;Je.CSSMediaRule=O1().CSSMediaRule;Je.CSSSupportsRule=L1().CSSSupportsRule;Je.CSSImportRule=Ox().CSSImportRule;Je.CSSFontFaceRule=Lx().CSSFontFaceRule;Je.CSSHostRule=Dx().CSSHostRule;Je.StyleSheet=Mx().StyleSheet;Je.CSSStyleSheet=yp().CSSStyleSheet;Je.CSSKeyframesRule=B1().CSSKeyframesRule;Je.CSSKeyframeRule=D1().CSSKeyframeRule;Je.MatcherList=Vx().MatcherList;Je.CSSDocumentRule=Ux().CSSDocumentRule;Je.CSSValue=Bx().CSSValue;Je.CSSValueExpression=Fx().CSSValueExpression;Je.parse=R1().parse;Je.clone=tN().clone});var aN=$((Xce,qx)=>{try{qx.exports=I2("canvas")}catch(r){class e{constructor(i,n){this.width=i,this.height=n}getContext(){return null}toDataURL(){return""}}s(e,"Canvas"),qx.exports={createCanvas:(t,i)=>new e(t,i)}}});var jy={};qa(jy,{DownloadError:()=>W0,InnertubeError:()=>S,MissingParamError:()=>Nt,NoStreamingDataError:()=>G0,OAuthError:()=>$i,ParsingError:()=>Zt,PlayerError:()=>Ii,SessionError:()=>to,UnavailableContentError:()=>Y0,debugFetch:()=>Wk,deepCompare:()=>H0,escapeStringRegexp:()=>Fy,generateRandomString:()=>Hn,generateSidAuth:()=>Vy,getRandomUserAgent:()=>Gn,getRuntime:()=>Ai,getStringBetweenStrings:()=>hi,hasKeys:()=>kr,isServer:()=>Uy,sha1Hash:()=>P2,streamToIterable:()=>qy,throwIfMissing:()=>Ee,timeToSeconds:()=>xt,u8ToBase64:()=>ei,uuidv4:()=>Ki});var sn={name:"youtubei.js",version:"2.3.2",description:"Full-featured wrapper around YouTube's private API.",main:"./dist/index.js",browser:"./bundle/browser.js",types:"./dist",author:"LuanRT (https://github.com/LuanRT)",funding:["https://github.com/sponsors/LuanRT"],contributors:["Wykerd (https://github.com/wykerd/)","MasterOfBob777 (https://github.com/MasterOfBob777)","patrickkfkan (https://github.com/patrickkfkan)","akkadaska (https://github.com/akkadaska)"],directories:{test:"./test",examples:"./examples",dist:"./dist"},scripts:{test:"npx jest --verbose",lint:"npx eslint ./src","lint:fix":"npx eslint --fix ./src",build:"npm run build:parser-map && npm run build:proto && npm run bundle:browser && npm run bundle:browser:prod && npm run build:node","build:node":"npx tsc","bundle:browser":'npx tsc --module esnext && npx esbuild ./dist/browser.js --banner:js="/* eslint-disable */" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser',"bundle:browser:prod":"npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify","build:parser-map":"node ./scripts/build-parser-map.js","build:proto":"npx protoc --ts_out ./src/proto --proto_path ./src/proto ./src/proto/youtube.proto",prepare:"npm run build",watch:"npx tsc --watch"},repository:{type:"git",url:"git+https://github.com/LuanRT/YouTube.js.git"},license:"MIT",dependencies:{"@protobuf-ts/runtime":"^2.7.0",jintr:"^0.3.1",linkedom:"^0.14.12",undici:"^5.7.0"},devDependencies:{"@protobuf-ts/plugin":"^2.7.0","@types/jest":"^28.1.7","@types/node":"^17.0.45","@typescript-eslint/eslint-plugin":"^5.30.6","@typescript-eslint/parser":"^5.30.6",esbuild:"^0.14.49",eslint:"^8.19.0","eslint-plugin-tsdoc":"^0.2.16",glob:"^8.0.3",jest:"^28.1.3","ts-jest":"^28.0.8",typescript:"^4.7.4"},bugs:{url:"https://github.com/LuanRT/YouTube.js/issues"},homepage:"https://github.com/LuanRT/YouTube.js#readme",keywords:["yt","dl","ytdl","youtube","youtubedl","youtube-dl","youtube-downloader","youtube-music","innertubeapi","innertube","unofficial","downloader","livechat","studio","upload","ytmusic","search","comment","music","api"]};var A2={desktop:["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.62","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.49","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"],mobile:["Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/17.0 Chrome/96.0.4664.104 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 11; SM-G781B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36","Mozilla/5.0 (Linux; arm_64; Android 12; RMX3081) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.148 YaBrowser/22.7.3.82.00 SA/3 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; Android 11; GM1900) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0675.117 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 11; 21061119BI) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 10; HarmonyOS; TEL-AN10; HMSCore 6.6.0.312) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.105 HuaweiBrowser/12.1.1.321 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; U; Android 8.0.0; zh-cn; Mi Note 2 Build/OPR1.170623.032) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/61.0.3163.128 Mobile Safari/537.36 XiaoMi/MiuiBrowser/10.1.1","Mozilla/5.0 (Linux; Android 12; IN2013) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 12; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/103.0.5060.63 Mobile/15E148 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/103.0.5060.63 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; Android 9; moto e6s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; Android 11; ONEPLUS A6013) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 12; SM-G986B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.25 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Prime) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"]};var R2=function(r,e,t,i){function n(o){return o instanceof t?o:new t(function(u){u(o)})}return s(n,"adopt"),new(t||(t=Promise))(function(o,u){function l(f){try{d(i.next(f))}catch(v){u(v)}}s(l,"fulfilled");function h(f){try{d(i.throw(f))}catch(v){u(v)}}s(h,"rejected");function d(f){f.done?o(f.value):n(f.value).then(l,h)}s(d,"step"),d((i=i.apply(r,e||[])).next())})},eo=function(r){return this instanceof eo?(this.v=r,this):new eo(r)},jk=function(r,e,t){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var i=t.apply(r,e||[]),n,o=[];return n={},u("next"),u("throw"),u("return"),n[Symbol.asyncIterator]=function(){return this},n;function u(x){i[x]&&(n[x]=function(E){return new Promise(function(N,F){o.push([x,E,N,F])>1||l(x,E)})})}function l(x,E){try{h(i[x](E))}catch(N){v(o[0][3],N)}}function h(x){x.value instanceof eo?Promise.resolve(x.value.v).then(d,f):v(o[0][2],x)}function d(x){l("next",x)}function f(x){l("throw",x)}function v(x,E){x(E),o.shift(),o.length&&l(o[0][0],o[0][1])}},S=class extends Error{constructor(e,t){super(e),t&&(this.info=t),this.date=new Date,this.version=sn.version}};s(S,"InnertubeError");var Zt=class extends S{};s(Zt,"ParsingError");var W0=class extends S{};s(W0,"DownloadError");var Nt=class extends S{};s(Nt,"MissingParamError");var Y0=class extends S{};s(Y0,"UnavailableContentError");var G0=class extends S{};s(G0,"NoStreamingDataError");var $i=class extends S{};s($i,"OAuthError");var Ii=class extends Error{};s(Ii,"PlayerError");var to=class extends Error{};s(to,"SessionError");function H0(r,e){return Reflect.ownKeys(r).some(i=>{var n;let o=((n=e[i])===null||n===void 0?void 0:n.constructor.name)==="Text";return!o&&typeof e[i]=="object"?JSON.stringify(r[i])===JSON.stringify(e[i]):r[i]===(o?e[i].toString():e[i])})}s(H0,"deepCompare");function hi(r,e,t){let i=new RegExp(`${Fy(e)}(.*?)${Fy(t)}`,"s"),n=r.match(i);return n?n[1]:void 0}s(hi,"getStringBetweenStrings");function Fy(r){return r.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d")}s(Fy,"escapeStringRegexp");function Gn(r){let e=A2[r],t=Math.floor(Math.random()*e.length);return e[t]}s(Gn,"getRandomUserAgent");function P2(r){return R2(this,void 0,void 0,function*(){let e=Ai()==="node"?Reflect.get(module,"require")("crypto").webcrypto.subtle:window.crypto.subtle,t=["00","01","02","03","04","05","06","07","08","09","0a","0b","0c","0d","0e","0f","10","11","12","13","14","15","16","17","18","19","1a","1b","1c","1d","1e","1f","20","21","22","23","24","25","26","27","28","29","2a","2b","2c","2d","2e","2f","30","31","32","33","34","35","36","37","38","39","3a","3b","3c","3d","3e","3f","40","41","42","43","44","45","46","47","48","49","4a","4b","4c","4d","4e","4f","50","51","52","53","54","55","56","57","58","59","5a","5b","5c","5d","5e","5f","60","61","62","63","64","65","66","67","68","69","6a","6b","6c","6d","6e","6f","70","71","72","73","74","75","76","77","78","79","7a","7b","7c","7d","7e","7f","80","81","82","83","84","85","86","87","88","89","8a","8b","8c","8d","8e","8f","90","91","92","93","94","95","96","97","98","99","9a","9b","9c","9d","9e","9f","a0","a1","a2","a3","a4","a5","a6","a7","a8","a9","aa","ab","ac","ad","ae","af","b0","b1","b2","b3","b4","b5","b6","b7","b8","b9","ba","bb","bc","bd","be","bf","c0","c1","c2","c3","c4","c5","c6","c7","c8","c9","ca","cb","cc","cd","ce","cf","d0","d1","d2","d3","d4","d5","d6","d7","d8","d9","da","db","dc","dd","de","df","e0","e1","e2","e3","e4","e5","e6","e7","e8","e9","ea","eb","ec","ed","ee","ef","f0","f1","f2","f3","f4","f5","f6","f7","f8","f9","fa","fb","fc","fd","fe","ff"];function i(n){let o=new Uint8Array(n),u=[];for(let l=0;lparseInt(t));switch(e.length){case 1:return e[0];case 2:return e[0]*60+e[1];case 3:return e[0]*3600+e[1]*60+e[2];default:throw new Error("Invalid time string")}}s(xt,"timeToSeconds");function Ee(r){for(let[e,t]of Object.entries(r))if(!t)throw new Nt(`${e} is missing`)}s(Ee,"throwIfMissing");function kr(r,...e){for(let t of e)if(!Reflect.has(r,t)||r[t]===void 0)return!1;return!0}s(kr,"hasKeys");function Ki(){var r;return Ai()==="node"?Reflect.get(module,"require")("crypto").webcrypto.randomUUID():!((r=globalThis.crypto)===null||r===void 0)&&r.randomUUID()?globalThis.crypto.randomUUID():"10000000-1000-4000-8000-100000000000".replace(/[018]/g,e=>{let t=parseInt(e);return(t^window.crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)})}s(Ki,"uuidv4");function Ai(){var r;return typeof process<"u"&&((r=process==null?void 0:process.versions)===null||r===void 0?void 0:r.node)?"node":Reflect.has(globalThis,"Deno")?"deno":"browser"}s(Ai,"getRuntime");function Uy(){return["node","deno"].includes(Ai())}s(Uy,"isServer");function qy(r){return jk(this,arguments,s(function*(){let t=r.getReader();try{for(;;){let{done:i,value:n}=yield eo(t.read());if(i)return yield eo(void 0);yield yield eo(n)}}finally{t.releaseLock()}},"streamToIterable_1"))}s(qy,"streamToIterable");var Wk=s((r,e)=>{let t=typeof r=="string"?new URL(r):r instanceof URL?r:new URL(r.url),i=e!=null&&e.headers?new Headers(e.headers):r instanceof Request?r.headers:new Headers,n=[...i],o=e!=null&&e.body?typeof e.body=="string"?i.get("content-type")==="application/json"?JSON.stringify(JSON.parse(e.body),null,2):e.body:" ":" (none)",u=n.length>0?`${n.map(([l,h])=>` ${l}: ${h}`).join(` 24 | +`:case"\f":l[i]&&(o+=ce);break;case'"':n=t+1;do n=e.indexOf('"',n)+1,n||Ue('Unmatched "');while(e[n-2]==="\\");switch(o+=e.slice(t,n),t=n-1,i){case"before-value":i="value";break;case"importRule-begin":i="importRule";break}break;case"'":n=t+1;do n=e.indexOf("'",n)+1,n||Ue("Unmatched '");while(e[n-2]==="\\");switch(o+=e.slice(t,n),t=n-1,i){case"before-value":i="value";break;case"importRule-begin":i="importRule";break}break;case"/":e.charAt(t+1)==="*"?(t+=2,n=e.indexOf("*/",t),n===-1?Ue("Missing */"):t=n+1):o+=ce,i==="importRule-begin"&&(o+=" ",i="importRule");break;case"@":if(e.indexOf("@-moz-document",t)===t){i="documentRule-begin",K=new Be.CSSDocumentRule,K.__starts=t,t+=13,o="";break}else if(e.indexOf("@media",t)===t){i="atBlock",G=new Be.CSSMediaRule,G.__starts=t,t+=5,o="";break}else if(e.indexOf("@supports",t)===t){i="conditionBlock",X=new Be.CSSSupportsRule,X.__starts=t,t+=8,o="";break}else if(e.indexOf("@host",t)===t){i="hostRule-begin",t+=4,le=new Be.CSSHostRule,le.__starts=t,o="";break}else if(e.indexOf("@import",t)===t){i="importRule-begin",t+=6,o+="@import";break}else if(e.indexOf("@font-face",t)===t){i="fontFaceRule-begin",t+=9,_=new Be.CSSFontFaceRule,_.__starts=t,o="";break}else{_e.lastIndex=t;var Ae=_e.exec(e);if(Ae&&Ae.index===t){i="keyframesRule-begin",W=new Be.CSSKeyframesRule,W.__starts=t,W._vendorPrefix=Ae[1],t+=Ae[0].length-1,o="";break}else i==="selector"&&(i="atRule")}o+=ce;break;case"{":i==="selector"||i==="atRule"?(H.selectorText=o.trim(),H.style.__starts=t,o="",i="before-name"):i==="atBlock"?(G.media.mediaText=o.trim(),f&&v.push(f),d=f=G,G.parentStyleSheet=h,o="",i="before-selector"):i==="conditionBlock"?(X.conditionText=o.trim(),f&&v.push(f),d=f=X,X.parentStyleSheet=h,o="",i="before-selector"):i==="hostRule-begin"?(f&&v.push(f),d=f=le,le.parentStyleSheet=h,o="",i="before-selector"):i==="fontFaceRule-begin"?(f&&(_.parentRule=f),_.parentStyleSheet=h,H=_,o="",i="before-name"):i==="keyframesRule-begin"?(W.name=o.trim(),f&&(v.push(f),W.parentRule=f),W.parentStyleSheet=h,d=f=W,o="",i="keyframeRule-begin"):i==="keyframeRule-begin"?(H=new Be.CSSKeyframeRule,H.keyText=o.trim(),H.__starts=t,o="",i="before-name"):i==="documentRule-begin"&&(K.matcher.matcherText=o.trim(),f&&(v.push(f),K.parentRule=f),d=f=K,K.parentStyleSheet=h,o="",i="before-selector");break;case":":i==="name"?(N=o.trim(),o="",i="before-value"):o+=ce;break;case"(":if(i==="value")if(o.trim()==="expression"){var Ft=new Be.CSSValueExpression(e,t).parse();Ft.error?Ue(Ft.error):(o+=Ft.expression,t=Ft.idx)}else i="value-parenthesis",u=1,o+=ce;else i==="value-parenthesis"&&u++,o+=ce;break;case")":i==="value-parenthesis"&&(u--,u===0&&(i="value")),o+=ce;break;case"!":i==="value"&&e.indexOf("!important",t)===t?(F="important",t+=9):o+=ce;break;case";":switch(i){case"value":H.style.setProperty(N,o.trim(),F),F="",o="",i="before-name";break;case"atRule":o="",i="before-selector";break;case"importRule":V=new Be.CSSImportRule,V.parentStyleSheet=V.styleSheet.parentStyleSheet=h,V.cssText=o+ce,h.cssRules.push(V),o="",i="before-selector";break;default:o+=ce;break}break;case"}":switch(i){case"value":H.style.setProperty(N,o.trim(),F),F="";case"before-name":case"name":H.__ends=t+1,f&&(H.parentRule=f),H.parentStyleSheet=h,d.cssRules.push(H),o="",d.constructor===Be.CSSKeyframesRule?i="keyframeRule-begin":i="before-selector";break;case"keyframeRule-begin":case"before-selector":case"selector":for(f||Ue("Unexpected }"),x=v.length>0;v.length>0;){if(f=v.pop(),f.constructor.name==="CSSMediaRule"||f.constructor.name==="CSSSupportsRule"){E=d,d=f,d.cssRules.push(E);break}v.length===0&&(x=!1)}x||(d.__ends=t+1,h.cssRules.push(d),d=h,f=null),o="",i="before-selector";break}break;default:switch(i){case"before-selector":i="selector",H=new Be.CSSStyleRule,H.__starts=t;break;case"before-name":i="name";break;case"before-value":i="value";break;case"importRule-begin":i="importRule";break}o+=ce;break}return h},"parse");QC.parse=Be.parse;Be.CSSStyleSheet=yp().CSSStyleSheet;Be.CSSStyleRule=vp().CSSStyleRule;Be.CSSImportRule=Ox().CSSImportRule;Be.CSSGroupingRule=Ds().CSSGroupingRule;Be.CSSMediaRule=O1().CSSMediaRule;Be.CSSConditionRule=aa().CSSConditionRule;Be.CSSSupportsRule=L1().CSSSupportsRule;Be.CSSFontFaceRule=Lx().CSSFontFaceRule;Be.CSSHostRule=Dx().CSSHostRule;Be.CSSStyleDeclaration=Ls().CSSStyleDeclaration;Be.CSSKeyframeRule=D1().CSSKeyframeRule;Be.CSSKeyframesRule=B1().CSSKeyframesRule;Be.CSSValueExpression=Fx().CSSValueExpression;Be.CSSDocumentRule=Ux().CSSDocumentRule});var Ls=$(ZC=>{var ua={};ua.CSSStyleDeclaration=s(function(){this.length=0,this.parentRule=null,this._importants={}},"CSSStyleDeclaration");ua.CSSStyleDeclaration.prototype={constructor:ua.CSSStyleDeclaration,getPropertyValue:function(r){return this[r]||""},setProperty:function(r,e,t){if(this[r]){var i=Array.prototype.indexOf.call(this,r);i<0&&(this[this.length]=r,this.length++)}else this[this.length]=r,this.length++;this[r]=e+"",this._importants[r]=t},removeProperty:function(r){if(!(r in this))return"";var e=Array.prototype.indexOf.call(this,r);if(e<0)return"";var t=this[r];return this[r]="",Array.prototype.splice.call(this,e,1),t},getPropertyCSSValue:function(){},getPropertyPriority:function(r){return this._importants[r]||""},getPropertyShorthand:function(){},isPropertyImplicit:function(){},get cssText(){for(var r=[],e=0,t=this.length;e{var U1={CSSStyleSheet:yp().CSSStyleSheet,CSSRule:Xt().CSSRule,CSSStyleRule:vp().CSSStyleRule,CSSGroupingRule:Ds().CSSGroupingRule,CSSConditionRule:aa().CSSConditionRule,CSSMediaRule:O1().CSSMediaRule,CSSSupportsRule:L1().CSSSupportsRule,CSSStyleDeclaration:Ls().CSSStyleDeclaration,CSSKeyframeRule:D1().CSSKeyframeRule,CSSKeyframesRule:B1().CSSKeyframesRule};U1.clone=s(function r(e){var t=new U1.CSSStyleSheet,i=e.cssRules;if(!i)return t;for(var n=0,o=i.length;n{"use strict";Je.CSSStyleDeclaration=Ls().CSSStyleDeclaration;Je.CSSRule=Xt().CSSRule;Je.CSSGroupingRule=Ds().CSSGroupingRule;Je.CSSConditionRule=aa().CSSConditionRule;Je.CSSStyleRule=vp().CSSStyleRule;Je.MediaList=M1().MediaList;Je.CSSMediaRule=O1().CSSMediaRule;Je.CSSSupportsRule=L1().CSSSupportsRule;Je.CSSImportRule=Ox().CSSImportRule;Je.CSSFontFaceRule=Lx().CSSFontFaceRule;Je.CSSHostRule=Dx().CSSHostRule;Je.StyleSheet=Mx().StyleSheet;Je.CSSStyleSheet=yp().CSSStyleSheet;Je.CSSKeyframesRule=B1().CSSKeyframesRule;Je.CSSKeyframeRule=D1().CSSKeyframeRule;Je.MatcherList=Vx().MatcherList;Je.CSSDocumentRule=Ux().CSSDocumentRule;Je.CSSValue=Bx().CSSValue;Je.CSSValueExpression=Fx().CSSValueExpression;Je.parse=R1().parse;Je.clone=tN().clone});var aN=$((Xce,qx)=>{try{qx.exports=I2("canvas")}catch(r){class e{constructor(i,n){this.width=i,this.height=n}getContext(){return null}toDataURL(){return""}}s(e,"Canvas"),qx.exports={createCanvas:(t,i)=>new e(t,i)}}});var jy={};qa(jy,{DownloadError:()=>W0,InnertubeError:()=>S,MissingParamError:()=>Nt,NoStreamingDataError:()=>G0,OAuthError:()=>$i,ParsingError:()=>Zt,PlayerError:()=>Ii,SessionError:()=>to,UnavailableContentError:()=>Y0,debugFetch:()=>Wk,deepCompare:()=>H0,escapeStringRegexp:()=>Fy,generateRandomString:()=>Hn,generateSidAuth:()=>Vy,getRandomUserAgent:()=>Gn,getRuntime:()=>Ai,getStringBetweenStrings:()=>hi,hasKeys:()=>kr,isServer:()=>Uy,sha1Hash:()=>P2,streamToIterable:()=>qy,throwIfMissing:()=>Ee,timeToSeconds:()=>xt,u8ToBase64:()=>ei,uuidv4:()=>Ki});var sn={name:"youtubei.js",version:"2.3.2",description:"Full-featured wrapper around YouTube's private API.",main:"./dist/index.js",browser:"./bundle/browser.js",types:"./dist",author:"LuanRT (https://github.com/LuanRT)",funding:["https://github.com/sponsors/LuanRT"],contributors:["Wykerd (https://github.com/wykerd/)","MasterOfBob777 (https://github.com/MasterOfBob777)","patrickkfkan (https://github.com/patrickkfkan)","akkadaska (https://github.com/akkadaska)"],directories:{test:"./test",examples:"./examples",dist:"./dist"},scripts:{test:"npx jest --verbose",lint:"npx eslint ./src","lint:fix":"npx eslint --fix ./src",build:"npm run build:parser-map && npm run build:proto && npm run bundle:browser && npm run bundle:browser:prod && npm run build:node","build:node":"npx tsc","bundle:browser":'npx tsc --module esnext && npx esbuild ./dist/browser.js --banner:js="/* eslint-disable */" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser',"bundle:browser:prod":"npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify","build:parser-map":"node ./scripts/build-parser-map.js","build:proto":"npx protoc --ts_out ./src/proto --proto_path ./src/proto ./src/proto/youtube.proto",prepare:"npm run build",watch:"npx tsc --watch"},repository:{type:"git",url:"git+https://github.com/LuanRT/YouTube.js.git"},license:"MIT",dependencies:{"@protobuf-ts/runtime":"^2.7.0",jintr:"^0.3.1",linkedom:"^0.14.12"},devDependencies:{"@protobuf-ts/plugin":"^2.7.0","@types/jest":"^28.1.7","@types/node":"^17.0.45","@typescript-eslint/eslint-plugin":"^5.30.6","@typescript-eslint/parser":"^5.30.6",esbuild:"^0.14.49",eslint:"^8.19.0","eslint-plugin-tsdoc":"^0.2.16",glob:"^8.0.3",jest:"^28.1.3","ts-jest":"^28.0.8",typescript:"^4.7.4"},bugs:{url:"https://github.com/LuanRT/YouTube.js/issues"},homepage:"https://github.com/LuanRT/YouTube.js#readme",keywords:["yt","dl","ytdl","youtube","youtubedl","youtube-dl","youtube-downloader","youtube-music","innertubeapi","innertube","unofficial","downloader","livechat","studio","upload","ytmusic","search","comment","music","api"]};var A2={desktop:["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.62","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.49","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"],mobile:["Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/17.0 Chrome/96.0.4664.104 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 11; SM-G781B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36","Mozilla/5.0 (Linux; arm_64; Android 12; RMX3081) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.148 YaBrowser/22.7.3.82.00 SA/3 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; Android 11; GM1900) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0675.117 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 11; 21061119BI) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 10; HarmonyOS; TEL-AN10; HMSCore 6.6.0.312) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.105 HuaweiBrowser/12.1.1.321 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; U; Android 8.0.0; zh-cn; Mi Note 2 Build/OPR1.170623.032) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/61.0.3163.128 Mobile Safari/537.36 XiaoMi/MiuiBrowser/10.1.1","Mozilla/5.0 (Linux; Android 12; IN2013) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 11; Redmi Note 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 12; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/103.0.5060.63 Mobile/15E148 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/103.0.5060.63 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; Android 9; moto e6s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; Android 11; ONEPLUS A6013) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (Linux; Android 12; SM-G986B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.25 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1","Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Prime) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Mobile Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"]};var R2=function(r,e,t,i){function n(o){return o instanceof t?o:new t(function(u){u(o)})}return s(n,"adopt"),new(t||(t=Promise))(function(o,u){function l(f){try{d(i.next(f))}catch(v){u(v)}}s(l,"fulfilled");function h(f){try{d(i.throw(f))}catch(v){u(v)}}s(h,"rejected");function d(f){f.done?o(f.value):n(f.value).then(l,h)}s(d,"step"),d((i=i.apply(r,e||[])).next())})},eo=function(r){return this instanceof eo?(this.v=r,this):new eo(r)},jk=function(r,e,t){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var i=t.apply(r,e||[]),n,o=[];return n={},u("next"),u("throw"),u("return"),n[Symbol.asyncIterator]=function(){return this},n;function u(x){i[x]&&(n[x]=function(E){return new Promise(function(N,F){o.push([x,E,N,F])>1||l(x,E)})})}function l(x,E){try{h(i[x](E))}catch(N){v(o[0][3],N)}}function h(x){x.value instanceof eo?Promise.resolve(x.value.v).then(d,f):v(o[0][2],x)}function d(x){l("next",x)}function f(x){l("throw",x)}function v(x,E){x(E),o.shift(),o.length&&l(o[0][0],o[0][1])}},S=class extends Error{constructor(e,t){super(e),t&&(this.info=t),this.date=new Date,this.version=sn.version}};s(S,"InnertubeError");var Zt=class extends S{};s(Zt,"ParsingError");var W0=class extends S{};s(W0,"DownloadError");var Nt=class extends S{};s(Nt,"MissingParamError");var Y0=class extends S{};s(Y0,"UnavailableContentError");var G0=class extends S{};s(G0,"NoStreamingDataError");var $i=class extends S{};s($i,"OAuthError");var Ii=class extends Error{};s(Ii,"PlayerError");var to=class extends Error{};s(to,"SessionError");function H0(r,e){return Reflect.ownKeys(r).some(i=>{var n;let o=((n=e[i])===null||n===void 0?void 0:n.constructor.name)==="Text";return!o&&typeof e[i]=="object"?JSON.stringify(r[i])===JSON.stringify(e[i]):r[i]===(o?e[i].toString():e[i])})}s(H0,"deepCompare");function hi(r,e,t){let i=new RegExp(`${Fy(e)}(.*?)${Fy(t)}`,"s"),n=r.match(i);return n?n[1]:void 0}s(hi,"getStringBetweenStrings");function Fy(r){return r.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d")}s(Fy,"escapeStringRegexp");function Gn(r){let e=A2[r],t=Math.floor(Math.random()*e.length);return e[t]}s(Gn,"getRandomUserAgent");function P2(r){return R2(this,void 0,void 0,function*(){let e=Ai()==="node"?Reflect.get(module,"require")("crypto").webcrypto.subtle:window.crypto.subtle,t=["00","01","02","03","04","05","06","07","08","09","0a","0b","0c","0d","0e","0f","10","11","12","13","14","15","16","17","18","19","1a","1b","1c","1d","1e","1f","20","21","22","23","24","25","26","27","28","29","2a","2b","2c","2d","2e","2f","30","31","32","33","34","35","36","37","38","39","3a","3b","3c","3d","3e","3f","40","41","42","43","44","45","46","47","48","49","4a","4b","4c","4d","4e","4f","50","51","52","53","54","55","56","57","58","59","5a","5b","5c","5d","5e","5f","60","61","62","63","64","65","66","67","68","69","6a","6b","6c","6d","6e","6f","70","71","72","73","74","75","76","77","78","79","7a","7b","7c","7d","7e","7f","80","81","82","83","84","85","86","87","88","89","8a","8b","8c","8d","8e","8f","90","91","92","93","94","95","96","97","98","99","9a","9b","9c","9d","9e","9f","a0","a1","a2","a3","a4","a5","a6","a7","a8","a9","aa","ab","ac","ad","ae","af","b0","b1","b2","b3","b4","b5","b6","b7","b8","b9","ba","bb","bc","bd","be","bf","c0","c1","c2","c3","c4","c5","c6","c7","c8","c9","ca","cb","cc","cd","ce","cf","d0","d1","d2","d3","d4","d5","d6","d7","d8","d9","da","db","dc","dd","de","df","e0","e1","e2","e3","e4","e5","e6","e7","e8","e9","ea","eb","ec","ed","ee","ef","f0","f1","f2","f3","f4","f5","f6","f7","f8","f9","fa","fb","fc","fd","fe","ff"];function i(n){let o=new Uint8Array(n),u=[];for(let l=0;lparseInt(t));switch(e.length){case 1:return e[0];case 2:return e[0]*60+e[1];case 3:return e[0]*3600+e[1]*60+e[2];default:throw new Error("Invalid time string")}}s(xt,"timeToSeconds");function Ee(r){for(let[e,t]of Object.entries(r))if(!t)throw new Nt(`${e} is missing`)}s(Ee,"throwIfMissing");function kr(r,...e){for(let t of e)if(!Reflect.has(r,t)||r[t]===void 0)return!1;return!0}s(kr,"hasKeys");function Ki(){var r;return Ai()==="node"?Reflect.get(module,"require")("crypto").webcrypto.randomUUID():!((r=globalThis.crypto)===null||r===void 0)&&r.randomUUID()?globalThis.crypto.randomUUID():"10000000-1000-4000-8000-100000000000".replace(/[018]/g,e=>{let t=parseInt(e);return(t^window.crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)})}s(Ki,"uuidv4");function Ai(){var r;return typeof process<"u"&&((r=process==null?void 0:process.versions)===null||r===void 0?void 0:r.node)?"node":Reflect.has(globalThis,"Deno")?"deno":"browser"}s(Ai,"getRuntime");function Uy(){return["node","deno"].includes(Ai())}s(Uy,"isServer");function qy(r){return jk(this,arguments,s(function*(){let t=r.getReader();try{for(;;){let{done:i,value:n}=yield eo(t.read());if(i)return yield eo(void 0);yield yield eo(n)}}finally{t.releaseLock()}},"streamToIterable_1"))}s(qy,"streamToIterable");var Wk=s((r,e)=>{let t=typeof r=="string"?new URL(r):r instanceof URL?r:new URL(r.url),i=e!=null&&e.headers?new Headers(e.headers):r instanceof Request?r.headers:new Headers,n=[...i],o=e!=null&&e.body?typeof e.body=="string"?i.get("content-type")==="application/json"?JSON.stringify(JSON.parse(e.body),null,2):e.body:" ":" (none)",u=n.length>0?`${n.map(([l,h])=>` ${l}: ${h}`).join(` 25 | `)}`:" (none)";return console.log(`YouTube.js Fetch: 26 | url: ${t.toString()} 27 | method: ${(e==null?void 0:e.method)||"GET"} 28 | diff --git a/dist/index.js b/dist/index.js 29 | index 840a92b65931c137cff2bbc5da59dc5d66058d15..4df1ea22017e94c79c18bcdef713732c3f884218 100644 30 | --- a/dist/index.js 31 | +++ b/dist/index.js 32 | @@ -19,23 +19,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) { 33 | Object.defineProperty(exports, "__esModule", { value: true }); 34 | exports.Innertube = exports.Parser = exports.YTNodes = void 0; 35 | const Utils_1 = require("./src/utils/Utils"); 36 | -// Polyfill fetch for node 37 | -if ((0, Utils_1.getRuntime)() === 'node') { 38 | - // eslint-disable-next-line 39 | - const undici = require('undici'); 40 | - Reflect.set(globalThis, 'fetch', undici.fetch); 41 | - Reflect.set(globalThis, 'Headers', undici.Headers); 42 | - Reflect.set(globalThis, 'Request', undici.Request); 43 | - Reflect.set(globalThis, 'Response', undici.Response); 44 | - Reflect.set(globalThis, 'FormData', undici.FormData); 45 | - Reflect.set(globalThis, 'File', undici.File); 46 | - try { 47 | - // eslint-disable-next-line 48 | - const { ReadableStream } = require('node:stream/web'); 49 | - Reflect.set(globalThis, 'ReadableStream', ReadableStream); 50 | - } 51 | - catch ( /* do nothing */_a) { /* do nothing */ } 52 | -} 53 | const Innertube_1 = __importDefault(require("./src/Innertube")); 54 | __exportStar(require("./src/utils"), exports); 55 | var map_1 = require("./src/parser/map"); 56 | diff --git a/dist/package.json b/dist/package.json 57 | index 9347b771e034e710e399d4c6bc441563118d7943..2067a65287e2d9ea49b9f903b992316fc81cefae 100644 58 | --- a/dist/package.json 59 | +++ b/dist/package.json 60 | @@ -1,87 +1,86 @@ 61 | { 62 | - "name": "youtubei.js", 63 | - "version": "2.3.2", 64 | - "description": "Full-featured wrapper around YouTube's private API.", 65 | - "main": "./dist/index.js", 66 | - "browser": "./bundle/browser.js", 67 | - "types": "./dist", 68 | - "author": "LuanRT (https://github.com/LuanRT)", 69 | - "funding": [ 70 | - "https://github.com/sponsors/LuanRT" 71 | - ], 72 | - "contributors": [ 73 | - "Wykerd (https://github.com/wykerd/)", 74 | - "MasterOfBob777 (https://github.com/MasterOfBob777)", 75 | - "patrickkfkan (https://github.com/patrickkfkan)", 76 | - "akkadaska (https://github.com/akkadaska)" 77 | - ], 78 | - "directories": { 79 | - "test": "./test", 80 | - "examples": "./examples", 81 | - "dist": "./dist" 82 | - }, 83 | - "scripts": { 84 | - "test": "npx jest --verbose", 85 | - "lint": "npx eslint ./src", 86 | - "lint:fix": "npx eslint --fix ./src", 87 | - "build": "npm run build:parser-map && npm run build:proto && npm run bundle:browser && npm run bundle:browser:prod && npm run build:node", 88 | - "build:node": "npx tsc", 89 | - "bundle:browser": "npx tsc --module esnext && npx esbuild ./dist/browser.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser", 90 | - "bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify", 91 | - "build:parser-map": "node ./scripts/build-parser-map.js", 92 | - "build:proto": "npx protoc --ts_out ./src/proto --proto_path ./src/proto ./src/proto/youtube.proto", 93 | - "prepare": "npm run build", 94 | - "watch": "npx tsc --watch" 95 | - }, 96 | - "repository": { 97 | - "type": "git", 98 | - "url": "git+https://github.com/LuanRT/YouTube.js.git" 99 | - }, 100 | - "license": "MIT", 101 | - "dependencies": { 102 | - "@protobuf-ts/runtime": "^2.7.0", 103 | - "jintr": "^0.3.1", 104 | - "linkedom": "^0.14.12", 105 | - "undici": "^5.7.0" 106 | - }, 107 | - "devDependencies": { 108 | - "@protobuf-ts/plugin": "^2.7.0", 109 | - "@types/jest": "^28.1.7", 110 | - "@types/node": "^17.0.45", 111 | - "@typescript-eslint/eslint-plugin": "^5.30.6", 112 | - "@typescript-eslint/parser": "^5.30.6", 113 | - "esbuild": "^0.14.49", 114 | - "eslint": "^8.19.0", 115 | - "eslint-plugin-tsdoc": "^0.2.16", 116 | - "glob": "^8.0.3", 117 | - "jest": "^28.1.3", 118 | - "ts-jest": "^28.0.8", 119 | - "typescript": "^4.7.4" 120 | - }, 121 | - "bugs": { 122 | - "url": "https://github.com/LuanRT/YouTube.js/issues" 123 | - }, 124 | - "homepage": "https://github.com/LuanRT/YouTube.js#readme", 125 | - "keywords": [ 126 | - "yt", 127 | - "dl", 128 | - "ytdl", 129 | - "youtube", 130 | - "youtubedl", 131 | - "youtube-dl", 132 | - "youtube-downloader", 133 | - "youtube-music", 134 | - "innertubeapi", 135 | - "innertube", 136 | - "unofficial", 137 | - "downloader", 138 | - "livechat", 139 | - "studio", 140 | - "upload", 141 | - "ytmusic", 142 | - "search", 143 | - "comment", 144 | - "music", 145 | - "api" 146 | - ] 147 | + "name": "youtubei.js", 148 | + "version": "2.3.2", 149 | + "description": "Full-featured wrapper around YouTube's private API.", 150 | + "main": "./dist/index.js", 151 | + "browser": "./bundle/browser.js", 152 | + "types": "./dist", 153 | + "author": "LuanRT (https://github.com/LuanRT)", 154 | + "funding": [ 155 | + "https://github.com/sponsors/LuanRT" 156 | + ], 157 | + "contributors": [ 158 | + "Wykerd (https://github.com/wykerd/)", 159 | + "MasterOfBob777 (https://github.com/MasterOfBob777)", 160 | + "patrickkfkan (https://github.com/patrickkfkan)", 161 | + "akkadaska (https://github.com/akkadaska)" 162 | + ], 163 | + "directories": { 164 | + "test": "./test", 165 | + "examples": "./examples", 166 | + "dist": "./dist" 167 | + }, 168 | + "scripts": { 169 | + "test": "npx jest --verbose", 170 | + "lint": "npx eslint ./src", 171 | + "lint:fix": "npx eslint --fix ./src", 172 | + "build": "npm run build:parser-map && npm run build:proto && npm run bundle:browser && npm run bundle:browser:prod && npm run build:node", 173 | + "build:node": "npx tsc", 174 | + "bundle:browser": "npx tsc --module esnext && npx esbuild ./dist/browser.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser", 175 | + "bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify", 176 | + "build:parser-map": "node ./scripts/build-parser-map.js", 177 | + "build:proto": "npx protoc --ts_out ./src/proto --proto_path ./src/proto ./src/proto/youtube.proto", 178 | + "prepare": "npm run build", 179 | + "watch": "npx tsc --watch" 180 | + }, 181 | + "repository": { 182 | + "type": "git", 183 | + "url": "git+https://github.com/LuanRT/YouTube.js.git" 184 | + }, 185 | + "license": "MIT", 186 | + "dependencies": { 187 | + "@protobuf-ts/runtime": "^2.7.0", 188 | + "jintr": "^0.3.1", 189 | + "linkedom": "^0.14.12" 190 | + }, 191 | + "devDependencies": { 192 | + "@protobuf-ts/plugin": "^2.7.0", 193 | + "@types/jest": "^28.1.7", 194 | + "@types/node": "^17.0.45", 195 | + "@typescript-eslint/eslint-plugin": "^5.30.6", 196 | + "@typescript-eslint/parser": "^5.30.6", 197 | + "esbuild": "^0.14.49", 198 | + "eslint": "^8.19.0", 199 | + "eslint-plugin-tsdoc": "^0.2.16", 200 | + "glob": "^8.0.3", 201 | + "jest": "^28.1.3", 202 | + "ts-jest": "^28.0.8", 203 | + "typescript": "^4.7.4" 204 | + }, 205 | + "bugs": { 206 | + "url": "https://github.com/LuanRT/YouTube.js/issues" 207 | + }, 208 | + "homepage": "https://github.com/LuanRT/YouTube.js#readme", 209 | + "keywords": [ 210 | + "yt", 211 | + "dl", 212 | + "ytdl", 213 | + "youtube", 214 | + "youtubedl", 215 | + "youtube-dl", 216 | + "youtube-downloader", 217 | + "youtube-music", 218 | + "innertubeapi", 219 | + "innertube", 220 | + "unofficial", 221 | + "downloader", 222 | + "livechat", 223 | + "studio", 224 | + "upload", 225 | + "ytmusic", 226 | + "search", 227 | + "comment", 228 | + "music", 229 | + "api" 230 | + ] 231 | } 232 | diff --git a/package.json b/package.json 233 | index b34043740a9a8223794fcede03c26e8fa19fd90e..2067a65287e2d9ea49b9f903b992316fc81cefae 100644 234 | --- a/package.json 235 | +++ b/package.json 236 | @@ -1,87 +1,86 @@ 237 | { 238 | - "name": "youtubei.js", 239 | - "version": "2.3.2", 240 | - "description": "Full-featured wrapper around YouTube's private API.", 241 | - "main": "./dist/index.js", 242 | - "browser": "./bundle/browser.js", 243 | - "types": "./dist", 244 | - "author": "LuanRT (https://github.com/LuanRT)", 245 | - "funding": [ 246 | - "https://github.com/sponsors/LuanRT" 247 | - ], 248 | - "contributors": [ 249 | - "Wykerd (https://github.com/wykerd/)", 250 | - "MasterOfBob777 (https://github.com/MasterOfBob777)", 251 | - "patrickkfkan (https://github.com/patrickkfkan)", 252 | - "akkadaska (https://github.com/akkadaska)" 253 | - ], 254 | - "directories": { 255 | - "test": "./test", 256 | - "examples": "./examples", 257 | - "dist": "./dist" 258 | - }, 259 | - "scripts": { 260 | - "test": "npx jest --verbose", 261 | - "lint": "npx eslint ./src", 262 | - "lint:fix": "npx eslint --fix ./src", 263 | - "build": "npm run build:parser-map && npm run build:proto && npm run bundle:browser && npm run bundle:browser:prod && npm run build:node", 264 | - "build:node": "npx tsc", 265 | - "bundle:browser": "npx tsc --module esnext && npx esbuild ./dist/browser.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser", 266 | - "bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify", 267 | - "build:parser-map": "node ./scripts/build-parser-map.js", 268 | - "build:proto": "npx protoc --ts_out ./src/proto --proto_path ./src/proto ./src/proto/youtube.proto", 269 | - "prepare": "npm run build", 270 | - "watch": "npx tsc --watch" 271 | - }, 272 | - "repository": { 273 | - "type": "git", 274 | - "url": "git+https://github.com/LuanRT/YouTube.js.git" 275 | - }, 276 | - "license": "MIT", 277 | - "dependencies": { 278 | - "@protobuf-ts/runtime": "^2.7.0", 279 | - "jintr": "^0.3.1", 280 | - "linkedom": "^0.14.12", 281 | - "undici": "^5.7.0" 282 | - }, 283 | - "devDependencies": { 284 | - "@protobuf-ts/plugin": "^2.7.0", 285 | - "@types/jest": "^28.1.7", 286 | - "@types/node": "^17.0.45", 287 | - "@typescript-eslint/eslint-plugin": "^5.30.6", 288 | - "@typescript-eslint/parser": "^5.30.6", 289 | - "esbuild": "^0.14.49", 290 | - "eslint": "^8.19.0", 291 | - "eslint-plugin-tsdoc": "^0.2.16", 292 | - "glob": "^8.0.3", 293 | - "jest": "^28.1.3", 294 | - "ts-jest": "^28.0.8", 295 | - "typescript": "^4.7.4" 296 | - }, 297 | - "bugs": { 298 | - "url": "https://github.com/LuanRT/YouTube.js/issues" 299 | - }, 300 | - "homepage": "https://github.com/LuanRT/YouTube.js#readme", 301 | - "keywords": [ 302 | - "yt", 303 | - "dl", 304 | - "ytdl", 305 | - "youtube", 306 | - "youtubedl", 307 | - "youtube-dl", 308 | - "youtube-downloader", 309 | - "youtube-music", 310 | - "innertubeapi", 311 | - "innertube", 312 | - "unofficial", 313 | - "downloader", 314 | - "livechat", 315 | - "studio", 316 | - "upload", 317 | - "ytmusic", 318 | - "search", 319 | - "comment", 320 | - "music", 321 | - "api" 322 | - ] 323 | + "name": "youtubei.js", 324 | + "version": "2.3.2", 325 | + "description": "Full-featured wrapper around YouTube's private API.", 326 | + "main": "./dist/index.js", 327 | + "browser": "./bundle/browser.js", 328 | + "types": "./dist", 329 | + "author": "LuanRT (https://github.com/LuanRT)", 330 | + "funding": [ 331 | + "https://github.com/sponsors/LuanRT" 332 | + ], 333 | + "contributors": [ 334 | + "Wykerd (https://github.com/wykerd/)", 335 | + "MasterOfBob777 (https://github.com/MasterOfBob777)", 336 | + "patrickkfkan (https://github.com/patrickkfkan)", 337 | + "akkadaska (https://github.com/akkadaska)" 338 | + ], 339 | + "directories": { 340 | + "test": "./test", 341 | + "examples": "./examples", 342 | + "dist": "./dist" 343 | + }, 344 | + "scripts": { 345 | + "test": "npx jest --verbose", 346 | + "lint": "npx eslint ./src", 347 | + "lint:fix": "npx eslint --fix ./src", 348 | + "build": "npm run build:parser-map && npm run build:proto && npm run bundle:browser && npm run bundle:browser:prod && npm run build:node", 349 | + "build:node": "npx tsc", 350 | + "bundle:browser": "npx tsc --module esnext && npx esbuild ./dist/browser.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser", 351 | + "bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify", 352 | + "build:parser-map": "node ./scripts/build-parser-map.js", 353 | + "build:proto": "npx protoc --ts_out ./src/proto --proto_path ./src/proto ./src/proto/youtube.proto", 354 | + "prepare": "npm run build", 355 | + "watch": "npx tsc --watch" 356 | + }, 357 | + "repository": { 358 | + "type": "git", 359 | + "url": "git+https://github.com/LuanRT/YouTube.js.git" 360 | + }, 361 | + "license": "MIT", 362 | + "dependencies": { 363 | + "@protobuf-ts/runtime": "^2.7.0", 364 | + "jintr": "^0.3.1", 365 | + "linkedom": "^0.14.12" 366 | + }, 367 | + "devDependencies": { 368 | + "@protobuf-ts/plugin": "^2.7.0", 369 | + "@types/jest": "^28.1.7", 370 | + "@types/node": "^17.0.45", 371 | + "@typescript-eslint/eslint-plugin": "^5.30.6", 372 | + "@typescript-eslint/parser": "^5.30.6", 373 | + "esbuild": "^0.14.49", 374 | + "eslint": "^8.19.0", 375 | + "eslint-plugin-tsdoc": "^0.2.16", 376 | + "glob": "^8.0.3", 377 | + "jest": "^28.1.3", 378 | + "ts-jest": "^28.0.8", 379 | + "typescript": "^4.7.4" 380 | + }, 381 | + "bugs": { 382 | + "url": "https://github.com/LuanRT/YouTube.js/issues" 383 | + }, 384 | + "homepage": "https://github.com/LuanRT/YouTube.js#readme", 385 | + "keywords": [ 386 | + "yt", 387 | + "dl", 388 | + "ytdl", 389 | + "youtube", 390 | + "youtubedl", 391 | + "youtube-dl", 392 | + "youtube-downloader", 393 | + "youtube-music", 394 | + "innertubeapi", 395 | + "innertube", 396 | + "unofficial", 397 | + "downloader", 398 | + "livechat", 399 | + "studio", 400 | + "upload", 401 | + "ytmusic", 402 | + "search", 403 | + "comment", 404 | + "music", 405 | + "api" 406 | + ] 407 | } 408 | --------------------------------------------------------------------------------