From cf29664d376ff792602400ef9a4ac301c676735c Mon Sep 17 00:00:00 2001 From: Luan Date: Mon, 3 Jun 2024 18:21:48 -0300 Subject: [PATCH] perf(general): Add session cache and LZW compression (#663) * feat(utils): Implement LZW compression module * feat(Session): Implement cache for sessions This should improve performance quite a bit for those who are not using the `generate_session_locally` option (like me :P). * refactor(Player): Add LZW compression This considerably reduces the size of the cache. --- src/core/Actions.ts | 14 +- src/core/Player.ts | 78 ++++------ src/core/Session.ts | 370 +++++++++++++++++++++++++++----------------- src/utils/LZW.ts | 64 ++++++++ src/utils/index.ts | 3 +- 5 files changed, 332 insertions(+), 197 deletions(-) create mode 100644 src/utils/LZW.ts diff --git a/src/core/Actions.ts b/src/core/Actions.ts index 6a008659..de49bd88 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -29,14 +29,10 @@ export type ParsedResponse = IParsedResponse; export default class Actions { - #session: Session; + session: Session; constructor(session: Session) { - this.#session = session; - } - - get session(): Session { - return this.#session; + this.session = session; } /** @@ -69,7 +65,7 @@ export default class Actions { s_url.searchParams.set(key, params[key]); } - const response = await this.#session.http.fetch(s_url); + const response = await this.session.http.fetch(s_url); return response; } @@ -88,7 +84,7 @@ export default class Actions { data = { ...args }; if (Reflect.has(data, 'browseId')) { - if (this.#needsLogin(data.browseId) && !this.#session.logged_in) + if (this.#needsLogin(data.browseId) && !this.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); } @@ -131,7 +127,7 @@ export default class Actions { const target_endpoint = Reflect.has(args || {}, 'override_endpoint') ? args?.override_endpoint : endpoint; - const response = await this.#session.http.fetch(target_endpoint, { + const response = await this.session.http.fetch(target_endpoint, { method: 'POST', body: args?.protobuf ? data : JSON.stringify((data || {})), headers: { diff --git a/src/core/Player.ts b/src/core/Player.ts index 4d848613..4c8648d1 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -1,23 +1,23 @@ -import { Log, Constants } from '../utils/index.js'; +import { Log, LZW, Constants } from '../utils/index.js'; import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.js'; import type { ICache, FetchFunction } from '../types/index.js'; +const TAG = 'Player'; + /** * Represents YouTube's player script. This is required to decipher signatures. */ export default class Player { - static TAG = 'Player'; - - #nsig_sc; - #sig_sc; - #sig_sc_timestamp; - #player_id; + nsig_sc; + sig_sc; + sts; + player_id; constructor(signature_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) { - this.#nsig_sc = nsig_sc; - this.#sig_sc = sig_sc; - this.#sig_sc_timestamp = signature_timestamp; - this.#player_id = player_id; + this.nsig_sc = nsig_sc; + this.sig_sc = sig_sc; + this.sts = signature_timestamp; + this.player_id = player_id; } static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise { @@ -31,7 +31,7 @@ export default class Player { const player_id = getStringBetweenStrings(js, 'player\\/', '\\/'); - Log.info(Player.TAG, `Got player id (${player_id}). Checking for cached players..`); + Log.info(TAG, `Got player id (${player_id}). Checking for cached players..`); if (!player_id) throw new PlayerError('Failed to get player id'); @@ -40,14 +40,14 @@ export default class Player { if (cache) { const cached_player = await Player.fromCache(cache, player_id); if (cached_player) { - Log.info(Player.TAG, 'Found a cached player.'); + Log.info(TAG, 'Found up-to-date player data in cache.'); return cached_player; } } const player_url = new URL(`/s/player/${player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE); - Log.info(Player.TAG, `Could not find any cached player. Will download a new player from ${player_url}.`); + Log.info(TAG, `Could not find any cached player. Will download a new player from ${player_url}.`); const player_res = await fetch(player_url, { headers: { @@ -65,7 +65,7 @@ export default class Player { const sig_sc = this.extractSigSourceCode(player_js); const nsig_sc = this.extractNSigSourceCode(player_js); - Log.info(Player.TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`); + Log.info(TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`); return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id); } @@ -80,11 +80,11 @@ export default class Player { const url_components = new URL(args.get('url') || url); if (signature_cipher || cipher) { - const signature = Platform.shim.eval(this.#sig_sc, { + const signature = Platform.shim.eval(this.sig_sc, { sig: args.get('s') }); - Log.info(Player.TAG, `Transformed signature ${args.get('s')} to ${signature}.`); + Log.info(TAG, `Transformed signature from ${args.get('s')} to ${signature}.`); if (typeof signature !== 'string') throw new PlayerError('Failed to decipher signature'); @@ -104,17 +104,17 @@ export default class Player { if (this_response_nsig_cache && this_response_nsig_cache.has(n)) { nsig = this_response_nsig_cache.get(n) as string; } else { - nsig = Platform.shim.eval(this.#nsig_sc, { + nsig = Platform.shim.eval(this.nsig_sc, { nsig: n }); - Log.info(Player.TAG, `Transformed nsig ${n} to ${nsig}.`); + Log.info(TAG, `Transformed n signature from ${n} to ${nsig}.`); if (typeof nsig !== 'string') throw new PlayerError('Failed to decipher nsig'); if (nsig.startsWith('enhanced_except_')) { - Log.warn(Player.TAG, 'Could not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!'); + Log.warn(TAG, 'Could not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!'); } else if (this_response_nsig_cache) { this_response_nsig_cache.set(n, nsig); } @@ -148,7 +148,7 @@ export default class Player { const result = url_components.toString(); - Log.info(Player.TAG, `Full deciphered URL: ${result}`); + Log.info(TAG, `Deciphered URL: ${result}`); return url_components.toString(); } @@ -171,10 +171,8 @@ export default class Player { const sig_buf = buffer.slice(12, 12 + sig_len); const nsig_buf = buffer.slice(12 + sig_len); - const decoder = new TextDecoder(); - - const sig_sc = decoder.decode(sig_buf); - const nsig_sc = decoder.decode(nsig_buf); + const sig_sc = LZW.decompress(new TextDecoder().decode(sig_buf)); + const nsig_sc = LZW.decompress(new TextDecoder().decode(nsig_buf)); return new Player(sig_timestamp, sig_sc, nsig_sc, player_id); } @@ -188,22 +186,20 @@ export default class Player { async cache(cache?: ICache): Promise { if (!cache) return; - const encoder = new TextEncoder(); - - const sig_buf = encoder.encode(this.#sig_sc); - const nsig_buf = encoder.encode(this.#nsig_sc); + const sig_buf = Buffer.from(LZW.compress(this.sig_sc)); + const nsig_buf = Buffer.from(LZW.compress(this.nsig_sc)); const buffer = new ArrayBuffer(12 + sig_buf.byteLength + nsig_buf.byteLength); const view = new DataView(buffer); view.setUint32(0, Player.LIBRARY_VERSION, true); - view.setUint32(4, this.#sig_sc_timestamp, true); + view.setUint32(4, this.sts, true); view.setUint32(8, sig_buf.byteLength, true); new Uint8Array(buffer).set(sig_buf, 12); new Uint8Array(buffer).set(nsig_buf, 12 + sig_buf.byteLength); - await cache.set(this.#player_id, new Uint8Array(buffer)); + await cache.set(this.player_id, new Uint8Array(buffer)); } static extractSigTimestamp(data: string): number { @@ -216,7 +212,7 @@ export default class Player { const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};'); if (!functions || !calls) - Log.warn(Player.TAG, 'Failed to extract signature decipher algorithm.'); + Log.warn(TAG, 'Failed to extract signature decipher algorithm.'); return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`; } @@ -225,28 +221,16 @@ export default class Player { const sc = `function descramble_nsig(a) { let b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join(""); } descramble_nsig(nsig)`; if (!sc) - Log.warn(Player.TAG, 'Failed to extract n-token decipher algorithm'); + Log.warn(TAG, 'Failed to extract n-token decipher algorithm'); return sc; } get url(): string { - return new URL(`/s/player/${this.#player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString(); - } - - get sts(): number { - return this.#sig_sc_timestamp; - } - - get nsig_sc(): string { - return this.#nsig_sc; - } - - get sig_sc(): string { - return this.#sig_sc; + return new URL(`/s/player/${this.player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString(); } static get LIBRARY_VERSION(): number { - return 2; + return 10; } } \ No newline at end of file diff --git a/src/core/Session.ts b/src/core/Session.ts index e986d27b..518a9c05 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -1,5 +1,5 @@ import OAuth2 from './OAuth2.js'; -import { Log, EventEmitter, HTTPClient } from '../utils/index.js'; +import { Log, EventEmitter, HTTPClient, LZW } from '../utils/index.js'; import * as Constants from '../utils/Constants.js'; import * as Proto from '../proto/index.js'; import Actions from './Actions.js'; @@ -25,7 +25,7 @@ export enum ClientType { TV_EMBEDDED = 'TVHTML5_SIMPLY_EMBEDDED_PLAYER' } -export interface Context { +export type Context = { client: { hl: string; gl: string; @@ -52,6 +52,13 @@ export interface Context { deviceMake: string; deviceModel: string; utcOffsetMinutes: number; + mainAppWebInfo?: { + graftUrl: string; + pwaInstallabilityStatus: string; + webDisplayMode: string; + isWebNativeShareAvailable: boolean; + }; + memoryTotalKbytes?: string; kidsAppInfo?: { categorySettings: { enabledCategories: string[]; @@ -76,7 +83,26 @@ export interface Context { }; } -export interface SessionOptions { +type ContextData = { + hl: string; + gl: string; + remote_host?: string; + visitor_data: string; + client_name: string; + client_version: string; + os_name: string; + os_version: string; + device_category: string; + time_zone: string; + enable_safety_mode: boolean; + browser_name?: string; + browser_version?: string; + device_make: string; + device_model: string; + on_behalf_of_user?: string; +} + +export type SessionOptions = { /** * Language. */ @@ -87,8 +113,8 @@ export interface SessionOptions { location?: string; /** * The account index to use. This is useful if you have multiple accounts logged in. - * **NOTE:** - * Only works if you are signed in with cookies. + * + * **NOTE:** Only works if you are signed in with cookies. */ account_index?: number; /** @@ -97,6 +123,7 @@ export interface SessionOptions { on_behalf_of_user?: string; /** * Specifies whether to retrieve the JS player. Disabling this will make session creation faster. + * * **NOTE:** Deciphering formats is not possible without the JS player. */ retrieve_player?: boolean; @@ -107,6 +134,9 @@ export interface SessionOptions { /** * Specifies whether to generate the session data locally or retrieve it from YouTube. * This can be useful if you need more performance. + * + * **NOTE:** If you are using the cache option and a session has already been generated, this will be ignored. + * If you want to force a new session to be generated, you must clear the cache. */ generate_session_locally?: boolean; /** @@ -122,7 +152,7 @@ export interface SessionOptions { */ timezone?: string; /** - * Used to cache the deciphering functions from the JS player. + * Used to cache algorithms, session data, and OAuth2 tokens. */ cache?: ICache; /** @@ -140,12 +170,18 @@ export interface SessionOptions { fetch?: FetchFunction; } -export interface SessionData { +export type SessionData = { context: Context; api_key: string; api_version: string; } +export type SWSessionData = { + context_data: ContextData; + api_key: string; + api_version: string; +} + export type SessionArgs = { lang: string; location: string; @@ -157,36 +193,35 @@ export type SessionArgs = { on_behalf_of_user: string | undefined; } +const TAG = 'Session'; + /** * Represents an InnerTube session. This holds all the data needed to make requests to YouTube. */ export default class Session extends EventEmitter { - static TAG = 'Session'; - - #api_version: string; - #key: string; - #context: Context; - #account_index: number; - #player?: Player; - + context: Context; + player?: Player; oauth: OAuth2; http: HTTPClient; logged_in: boolean; actions: Actions; cache?: ICache; + key: string; + api_version: string; + account_index: number; constructor(context: Context, api_key: string, api_version: string, account_index: number, player?: Player, cookie?: string, fetch?: FetchFunction, cache?: ICache) { super(); - this.#context = context; - this.#account_index = account_index; - this.#key = api_key; - this.#api_version = api_version; - this.#player = player; this.http = new HTTPClient(this, cookie, fetch); this.actions = new Actions(this); this.oauth = new OAuth2(this); this.logged_in = !!cookie; this.cache = cache; + this.account_index = account_index; + this.key = api_key; + this.api_version = api_version; + this.context = context; + this.player = player; } on(type: 'auth', listener: OAuth2AuthEventHandler): void; @@ -218,7 +253,8 @@ export default class Session extends EventEmitter { options.client_type, options.timezone, options.fetch, - options.on_behalf_of_user + options.on_behalf_of_user, + options.cache ); return new Session( @@ -228,6 +264,47 @@ export default class Session extends EventEmitter { ); } + /** + * Retrieves session data from cache. + * @param cache - A valid cache implementation. + * @param session_args - User provided session arguments. + */ + static async fromCache(cache: ICache, session_args: SessionArgs): Promise { + const buffer = await cache.get('innertube_session_data'); + + if (!buffer) + return null; + + const data = new TextDecoder().decode(buffer.slice(4)); + + try { + const result = JSON.parse(LZW.decompress(data)) as SessionData; + + if (session_args.visitor_data) { + result.context.client.visitorData = session_args.visitor_data; + } + + if (session_args.lang) + result.context.client.hl = session_args.lang; + + if (session_args.location) + result.context.client.gl = session_args.location; + + if (session_args.on_behalf_of_user) + result.context.user.onBehalfOfUser = session_args.on_behalf_of_user; + + result.context.client.timeZone = session_args.time_zone; + result.context.client.platform = session_args.device_category.toUpperCase(); + result.context.client.clientName = session_args.client_name; + result.context.user.enableSafetyMode = session_args.enable_safety_mode; + + return result; + } catch (error) { + Log.error(TAG, 'Failed to parse session data from cache.', error); + return null; + } + } + static async getSessionData( lang = '', location = '', @@ -239,53 +316,99 @@ export default class Session extends EventEmitter { client_name: ClientType = ClientType.WEB, tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone, fetch: FetchFunction = Platform.shim.fetch, - on_behalf_of_user?: string + on_behalf_of_user?: string, + cache?: ICache ) { - let session_data: SessionData; - const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user }; - Log.info(Session.TAG, 'Retrieving InnerTube session.'); + let session_data: SessionData | undefined; - if (generate_session_locally) { - session_data = this.#generateSessionData(session_args); - } else { - try { - // This can fail if the data changes or the request is blocked for some reason. - session_data = await this.#retrieveSessionData(session_args, fetch); - } catch (err) { - Log.error(Session.TAG, 'Failed to retrieve session data from server. Will try to generate it locally.'); - session_data = this.#generateSessionData(session_args); + if (cache) { + const cached_session_data = await this.fromCache(cache, session_args); + if (cached_session_data) { + Log.info(TAG, 'Found session data in cache.'); + session_data = cached_session_data; } } - Log.info(Session.TAG, 'Got session data.\n', session_data); + if (!session_data) { + Log.info(TAG, 'Generating session data.'); + + let api_key = Constants.CLIENTS.WEB.API_KEY; + let api_version = Constants.CLIENTS.WEB.API_VERSION; + + let context_data: ContextData = { + hl: lang || 'en', + gl: location || 'US', + remote_host: '', + visitor_data: visitor_data || Proto.encodeVisitorData(generateRandomString(11), Math.floor(Date.now() / 1000)), + client_name: client_name, + client_version: Constants.CLIENTS.WEB.VERSION, + device_category: device_category.toUpperCase(), + os_name: 'Windows', + os_version: '10.0', + time_zone: tz, + browser_name: 'Chrome', + browser_version: '125.0.0.0', + device_make: '', + device_model: '', + enable_safety_mode: enable_safety_mode + }; + + if (!generate_session_locally) { + try { + const sw_session_data = await this.#getSessionData(session_args, fetch); + api_key = sw_session_data.api_key; + api_version = sw_session_data.api_version; + context_data = sw_session_data.context_data; + } catch (error) { + Log.error(TAG, 'Failed to retrieve session data from server. Session data generated locally will be used instead.', error); + } + } + + session_data = { + api_key, + api_version, + context: this.#buildContext(context_data) + }; + + await this.#storeSession(session_data, cache); + } + + Log.debug(TAG, 'Session data:', session_data); return { ...session_data, account_index }; } - static #getVisitorID(visitor_data: string) { - const decoded_visitor_data = Proto.decodeVisitorData(visitor_data); - Log.info(Session.TAG, 'Custom visitor data decoded successfully.\n', decoded_visitor_data); - return decoded_visitor_data.id; + static async #storeSession(session_data: SessionData, cache?: ICache) { + if (!cache) return; + + Log.info(TAG, 'Compressing and caching session data.'); + + const compressed_session_data = Buffer.from(LZW.compress(JSON.stringify(session_data))); + + const buffer = new ArrayBuffer(4 + compressed_session_data.byteLength); + new DataView(buffer).setUint32(0, compressed_session_data.byteLength, true); // (Luan) XX: Leave this here for debugging purposes + new Uint8Array(buffer).set(compressed_session_data, 4); + + await cache.set('innertube_session_data', new Uint8Array(buffer)); } - static async #retrieveSessionData(options: SessionArgs, fetch: FetchFunction = Platform.shim.fetch): Promise { - const url = new URL('/sw.js_data', Constants.URLS.YT_BASE); - + static async #getSessionData(options: SessionArgs, fetch: FetchFunction = Platform.shim.fetch): Promise { let visitor_id = generateRandomString(11); - if (options.visitor_data) { + if (options.visitor_data) visitor_id = this.#getVisitorID(options.visitor_data); - } + + const url = new URL('/sw.js_data', Constants.URLS.YT_BASE); const res = await fetch(url, { headers: { - 'accept-language': options.lang || 'en-US', - 'user-agent': getRandomUserAgent('desktop'), - 'accept': '*/*', - 'referer': 'https://www.youtube.com/sw.js', - 'cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${visitor_id};` + 'Accept-Language': options.lang || 'en-US', + 'User-Agent': getRandomUserAgent('desktop'), + 'Accept': '*/*', + 'Referer': `${Constants.URLS.YT_BASE}/sw.js`, + 'Cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${visitor_id};` } }); @@ -293,6 +416,10 @@ export default class Session extends EventEmitter { throw new SessionError(`Failed to retrieve session data: ${res.status}`); const text = await res.text(); + + if (!text.startsWith(')]}\'')) + throw new SessionError('Invalid JSPB response'); + const data = JSON.parse(text.replace(/^\)\]\}'/, '')); const ytcfg = data[0][2]; @@ -301,78 +428,62 @@ export default class Session extends EventEmitter { const [ [ device_info ], api_key ] = ytcfg; - const context: Context = { - client: { - hl: device_info[0], - gl: options.location || device_info[2], - remoteHost: device_info[3], - screenDensityFloat: 1, - screenHeightPoints: 1080, - screenPixelDensity: 1, - screenWidthPoints: 1920, - visitorData: device_info[13], - clientName: options.client_name, - clientVersion: device_info[16], - osName: device_info[17], - osVersion: device_info[18], - platform: options.device_category.toUpperCase(), - clientFormFactor: 'UNKNOWN_FORM_FACTOR', - userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT', - timeZone: device_info[79] || options.time_zone, - browserName: device_info[86], - browserVersion: device_info[87], - originalUrl: Constants.URLS.YT_BASE, - deviceMake: device_info[11], - deviceModel: device_info[12], - utcOffsetMinutes: -new Date().getTimezoneOffset() - }, - user: { - enableSafetyMode: options.enable_safety_mode, - lockedSafetyMode: false - }, - request: { - useSsl: true, - internalExperimentFlags: [] - } + const context_info = { + hl: options.lang || device_info[0], + gl: options.location || device_info[2], + remote_host: device_info[3], + visitor_data: device_info[13], + client_name: options.client_name, + client_version: device_info[16], + os_name: device_info[17], + os_version: device_info[18], + time_zone: device_info[79] || options.time_zone, + device_category: options.device_category, + browser_name: device_info[86], + browser_version: device_info[87], + device_make: device_info[11], + device_model: device_info[12], + enable_safety_mode: options.enable_safety_mode }; - if (options.on_behalf_of_user) - context.user.onBehalfOfUser = options.on_behalf_of_user; - - return { context, api_key, api_version }; + return { context_data: context_info, api_key, api_version }; } - static #generateSessionData(options: SessionArgs): SessionData { - let visitor_id = generateRandomString(11); - - if (options.visitor_data) { - visitor_id = this.#getVisitorID(options.visitor_data); - } - + static #buildContext(args: ContextData) { const context: Context = { client: { - hl: options.lang || 'en', - gl: options.location || 'US', + hl: args.hl, + gl: args.gl, + remoteHost: args.remote_host, screenDensityFloat: 1, - screenHeightPoints: 1080, + screenHeightPoints: 1440, screenPixelDensity: 1, - screenWidthPoints: 1920, - visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)), - clientName: options.client_name, - clientVersion: Constants.CLIENTS.WEB.VERSION, - osName: 'Windows', - osVersion: '10.0', - platform: options.device_category.toUpperCase(), + screenWidthPoints: 2560, + visitorData: args.visitor_data, + clientName: args.client_name, + clientVersion: args.client_version, + osName: args.os_name, + osVersion: args.os_version, + platform: args.device_category.toUpperCase(), clientFormFactor: 'UNKNOWN_FORM_FACTOR', userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT', - timeZone: options.time_zone, + timeZone: args.time_zone, originalUrl: Constants.URLS.YT_BASE, - deviceMake: '', - deviceModel: '', - utcOffsetMinutes: -new Date().getTimezoneOffset() + deviceMake: args.device_make, + deviceModel: args.device_model, + browserName: args.browser_name, + browserVersion: args.browser_version, + utcOffsetMinutes: -new Date().getTimezoneOffset(), + memoryTotalKbytes: '8000000', + mainAppWebInfo: { + graftUrl: Constants.URLS.YT_BASE, + pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN', + webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER', + isWebNativeShareAvailable: true + } }, user: { - enableSafetyMode: options.enable_safety_mode, + enableSafetyMode: args.enable_safety_mode, lockedSafetyMode: false }, request: { @@ -381,10 +492,15 @@ export default class Session extends EventEmitter { } }; - if (options.on_behalf_of_user) - context.user.onBehalfOfUser = options.on_behalf_of_user; + if (args.on_behalf_of_user) + context.user.onBehalfOfUser = args.on_behalf_of_user; - return { context, api_key: Constants.CLIENTS.WEB.API_KEY, api_version: Constants.CLIENTS.WEB.API_VERSION }; + return context; + } + + static #getVisitorID(visitor_data: string) { + const decoded_visitor_data = Proto.decodeVisitorData(visitor_data); + return decoded_visitor_data.id; } async signIn(credentials?: OAuth2Tokens): Promise { @@ -420,41 +536,15 @@ export default class Session extends EventEmitter { return response; } - /** - * InnerTube API key. - */ - get key(): string { - return this.#key; - } - - /** - * InnerTube API version. - */ - get api_version(): string { - return this.#api_version; - } - get client_version(): string { - return this.#context.client.clientVersion; + return this.context.client.clientVersion; } get client_name(): string { - return this.#context.client.clientName; - } - - get account_index(): number { - return this.#account_index; - } - - get context(): Context { - return this.#context; - } - - get player(): Player | undefined { - return this.#player; + return this.context.client.clientName; } get lang(): string { - return this.#context.client.hl; + return this.context.client.hl; } } \ No newline at end of file diff --git a/src/utils/LZW.ts b/src/utils/LZW.ts new file mode 100644 index 00000000..3616dc64 --- /dev/null +++ b/src/utils/LZW.ts @@ -0,0 +1,64 @@ +/** + * Compresses a string using the LZW compression algorithm. + * @param input - The data to compress. + */ +export function compress(input: string): string { + const output: number[] = []; + const dictionary: Record = {}; + + for (let i = 0; i < 256; i++) { + dictionary[String.fromCharCode(i)] = i; + } + + let current_string = ''; + let dictionary_size = 256; + + for (let i = 0; i < input.length; i++) { + const current_char = input[i]; + const combined_string = current_string + current_char; + + if (dictionary.hasOwnProperty(combined_string)) { + current_string = combined_string; + } else { + output.push(dictionary[current_string]); + dictionary[combined_string] = dictionary_size++; + current_string = current_char; + } + } + + if (current_string !== '') { + output.push(dictionary[current_string]); + } + + return output.map((code) => String.fromCharCode(code)).join(''); +} + +/** + * Decompresses data that was compressed using the LZW compression algorithm. + * @param input - The data to be decompressed. + */ +export function decompress(input: string): string { + const dictionary: Record = {}; + const input_data = input.split(''); + const output: string[] = [ input_data.shift() as string ]; + const input_length = input_data.length >>> 0; // Convert to unsigned 32-bit integer + + let dictionary_code = 256; + let current_char = output[0]; + let current_string = current_char; + + for (let i = 0; i < input_length; ++i) { + const current_code = input_data[i].charCodeAt(0); + const entry = + current_code < 256 ? input_data[i] : (dictionary[current_code] ? + dictionary[current_code] : (current_string + current_char)); + + output.push(entry); + + current_char = entry.charAt(0); + dictionary[dictionary_code++] = current_string + current_char; + current_string = entry; + } + + return output.join(''); +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 8d9004d9..ffe84174 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,4 +12,5 @@ export * from './HTTPClient.js'; export { Platform } from './Utils.js'; export * as Utils from './Utils.js'; -export { default as Log } from './Log.js'; \ No newline at end of file +export { default as Log } from './Log.js'; +export * as LZW from './LZW.js'; \ No newline at end of file