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.
This commit is contained in:
Luan
2024-06-03 18:21:48 -03:00
committed by GitHub
parent 4015a5e560
commit cf29664d37
5 changed files with 332 additions and 197 deletions

View File

@@ -29,14 +29,10 @@ export type ParsedResponse<T> =
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: {

View File

@@ -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<Player> {
@@ -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<void> {
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;
}
}

View File

@@ -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<SessionData | null> {
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<SessionData> {
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
static async #getSessionData(options: SessionArgs, fetch: FetchFunction = Platform.shim.fetch): Promise<SWSessionData> {
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<void> {
@@ -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;
}
}

64
src/utils/LZW.ts Normal file
View File

@@ -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<string, number> = {};
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<number, string> = {};
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('');
}

View File

@@ -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';
export { default as Log } from './Log.js';
export * as LZW from './LZW.js';