mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-26 08:08:54 +00:00
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:
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
64
src/utils/LZW.ts
Normal 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('');
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user