chore: v15.0.1 release

This commit is contained in:
LuanRT
2025-07-22 20:00:08 +00:00
parent ddc35db72e
commit fc0ba30d1e
6 changed files with 120 additions and 63 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "15.0.0",
"version": "15.0.1",
"description": "A JavaScript client for YouTube's private API, known as InnerTube.",
"type": "module",
"types": "./dist/src/platform/lib.d.ts",
@@ -103,7 +103,6 @@
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"jintr": "^3.3.1",
"tslib": "^2.5.0",
"undici": "^6.21.3"
},
"overrides": {

View File

@@ -19,6 +19,7 @@ import {
} from './parser/youtube/index.ts';
import { ShortFormVideoInfo } from './parser/ytshorts/index.ts';
import { NavigateAction } from './parser/continuations.ts';
import NavigationEndpoint from './parser/classes/NavigationEndpoint.ts';
import type Format from './parser/classes/misc/Format.ts';
@@ -390,8 +391,13 @@ export default class Innertube {
async getChannel(id: string): Promise<Channel> {
throwIfMissing({ id });
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: id } });
const response = await browse_endpoint.call(this.#session.actions);
return new Channel(this.actions, response);
let response = await browse_endpoint.call<IBrowseResponse>(this.#session.actions, { parse: true });
if (response.on_response_received_actions?.[0].is(NavigateAction)) {
response = await response.on_response_received_actions[0].endpoint.call<IBrowseResponse>(this.#session.actions, { parse: true });
}
return new Channel(this.actions, response, true);
}
async getNotifications(): Promise<NotificationsMenu> {

View File

@@ -1,6 +1,6 @@
import { Jinter } from 'jsr:@luanrt/jintr';
import type { FetchFunction, ICache } from '../types/index.ts';
import { Constants, Log, LZW } from '../utils/index.ts';
import { Constants, BinarySerializer, Log } from '../utils/index.ts';
import {
type ASTLookupResult,
findFunction,
@@ -13,6 +13,14 @@ import {
const TAG = 'Player';
interface SerializablePlayer {
player_id: string;
sts: number;
sig_sc?: string;
nsig_sc?: string;
library_version: number;
}
/**
* Represents YouTube's player script. This is required to decipher signatures.
*/
@@ -31,7 +39,6 @@ export default class Player {
}
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch, po_token?: string, player_id?: string): Promise<Player> {
if (!player_id) {
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
const res = await fetch(url);
@@ -191,22 +198,20 @@ export default class Player {
if (!buffer)
return null;
const view = new DataView(buffer);
const version = view.getUint32(0, true);
try {
const current_library_version = parseInt(Platform.shim.info.version.split('.')[0]);
const player_data = BinarySerializer.deserialize<SerializablePlayer>(new Uint8Array(buffer));
if (version !== Player.LIBRARY_VERSION)
if (player_data.library_version !== current_library_version) {
Log.warn(TAG, `Cached player data is from a different library version (${player_data.library_version}). Ignoring it.`);
return null;
}
return new Player(player_data.player_id, player_data.sts, player_data.sig_sc, player_data.nsig_sc);
} catch (e) {
Log.error(TAG, 'Failed to deserialize player data from cache:', e);
return null;
const sig_timestamp = view.getUint32(4, true);
const sig_len = view.getUint32(8, true);
const sig_buf = buffer.slice(12, 12 + sig_len);
const nsig_buf = buffer.slice(12 + sig_len);
const sig_sc = LZW.decompress(new TextDecoder().decode(sig_buf));
const nsig_sc = LZW.decompress(new TextDecoder().decode(nsig_buf));
return new Player(player_id, sig_timestamp, sig_sc, nsig_sc);
}
}
static async fromSource(player_id: string, sig_timestamp: number, cache?: ICache, sig_sc?: string, nsig_sc?: string): Promise<Player> {
@@ -219,22 +224,17 @@ export default class Player {
if (!cache || !this.sig_sc || !this.nsig_sc)
return;
const encoder = new TextEncoder();
const current_library_version = parseInt(Platform.shim.info.version.split('.')[0]);
const sig_buf = encoder.encode(LZW.compress(this.sig_sc));
const nsig_buf = encoder.encode(LZW.compress(this.nsig_sc));
const buffer = BinarySerializer.serialize({
player_id: this.player_id,
sts: this.sts,
sig_sc: this.sig_sc,
nsig_sc: this.nsig_sc,
library_version: current_library_version
});
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.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, buffer);
}
static extractSigTimestamp(data: string): number {
@@ -294,29 +294,29 @@ export default class Player {
if (global_variable) {
nsig_function = findFunction(data, { includes: `new Date(${global_variable.name}`, ast });
// For redundancy/the above fails:
if (!nsig_function)
nsig_function = findFunction(data, { includes: '.push(String.fromCharCode(', ast });
if (!nsig_function)
nsig_function = findFunction(data, { includes: '.reverse().forEach(function', ast });
if (nsig_function)
return `${global_variable.result} var ${nsig_function.result} ${nsig_function.name}(nsig);`;
}
// This is the suffix of the error tag.
nsig_function = findFunction(data, { includes: '-_w8_', ast });
// Usually, only this function uses these dates in the entire script.
if (!nsig_function)
nsig_function = findFunction(data, { includes: '1969', ast });
// This used to be the prefix of the error tag (leaving it here for reference).
if (!nsig_function)
nsig_function = findFunction(data, { includes: 'enhanced_except', ast });
if (nsig_function)
return `let ${nsig_function.result} ${nsig_function.name}(nsig);`;
}

View File

@@ -2,8 +2,7 @@ import Actions from './Actions.ts';
import OAuth2 from './OAuth2.ts';
import Player from './Player.ts';
import * as Constants from '../utils/Constants.ts';
import { EventEmitter, HTTPClient, Log, LZW, ProtoUtils } from '../utils/index.ts';
import { EventEmitter, HTTPClient, BinarySerializer, Log, ProtoUtils } from '../utils/index.ts';
import {
generateRandomString, getRandomUserAgent,
InnertubeError, Platform, SessionError
@@ -221,6 +220,10 @@ export type SessionData = {
config_data?: string;
}
interface SerializableSession extends SessionData {
library_version: number;
}
export type SWSessionData = {
context_data: ContextData;
api_key: string;
@@ -326,42 +329,45 @@ export default class Session extends EventEmitter {
if (!buffer)
return null;
const data = new TextDecoder().decode(buffer.slice(4));
try {
const result = JSON.parse(LZW.decompress(data)) as SessionData;
const session_data = BinarySerializer.deserialize<SerializableSession>(new Uint8Array(buffer));
if (session_data.library_version !== parseInt(Platform.shim.info.version.split('.')[0])) {
Log.warn(TAG, `Cached session data is from a different library version (${session_data.library_version}). Regenerating session data.`);
return null;
}
if (session_args.visitor_data) {
result.context.client.visitorData = session_args.visitor_data;
session_data.context.client.visitorData = session_args.visitor_data;
}
if (session_args.lang)
result.context.client.hl = session_args.lang;
session_data.context.client.hl = session_args.lang;
if (session_args.location)
result.context.client.gl = session_args.location;
session_data.context.client.gl = session_args.location;
if (session_args.on_behalf_of_user)
result.context.user.onBehalfOfUser = session_args.on_behalf_of_user;
session_data.context.user.onBehalfOfUser = session_args.on_behalf_of_user;
if (session_args.user_agent)
result.context.client.userAgent = session_args.user_agent;
session_data.context.client.userAgent = session_args.user_agent;
if (session_args.client_name) {
const client = Object.values(Constants.CLIENTS).find((c) => c.NAME === session_args.client_name);
if (client) {
result.context.client.clientName = client.NAME;
result.context.client.clientVersion = client.VERSION;
session_data.context.client.clientName = client.NAME;
session_data.context.client.clientVersion = client.VERSION;
} else Log.warn(TAG, `Unknown client name: ${session_args.client_name}.`);
}
result.context.client.timeZone = session_args.time_zone;
result.context.client.platform = session_args.device_category.toUpperCase();
result.context.user.enableSafetyMode = session_args.enable_safety_mode;
session_data.context.client.timeZone = session_args.time_zone;
session_data.context.client.platform = session_args.device_category.toUpperCase();
session_data.context.user.enableSafetyMode = session_args.enable_safety_mode;
return result;
return session_data;
} catch (error) {
Log.error(TAG, 'Failed to parse session data from cache.', error);
Log.error(TAG, 'Failed to deserialize session data from cache.', error);
return null;
}
}
@@ -509,13 +515,12 @@ export default class Session extends EventEmitter {
Log.info(TAG, 'Compressing and caching session data.');
const compressed_session_data = new TextEncoder().encode(LZW.compress(JSON.stringify(session_data)));
const buffer = BinarySerializer.serialize({
...session_data,
library_version: parseInt(Platform.shim.info.version)
});
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));
await cache.set('innertube_session_data', buffer);
}
static async #getSessionData(options: SessionArgs, fetch: FetchFunction = Platform.shim.fetch): Promise<SWSessionData> {

View File

@@ -0,0 +1,46 @@
import { compress, decompress } from './LZW.ts';
export const MAGIC_HEADER = 0x594254; // 'YTB' in hex...
export const VERSION = 1;
export function serialize(data: any): Uint8Array {
const json_str = JSON.stringify(data);
const compressed = compress(json_str);
const compressed_bytes = new TextEncoder().encode(compressed);
const buffer = new ArrayBuffer(12 + compressed_bytes.byteLength);
const view = new DataView(buffer);
view.setUint32(0, MAGIC_HEADER, true);
view.setUint32(4, VERSION, true);
view.setUint32(8, compressed_bytes.byteLength, true);
new Uint8Array(buffer).set(compressed_bytes, 12);
return new Uint8Array(buffer);
}
export function deserialize<T>(buffer: Uint8Array): T {
if (buffer.byteLength < 12)
throw new Error('Invalid binary format: buffer too short');
const view = new DataView(buffer.buffer, buffer.byteOffset);
const magic = view.getUint32(0, true);
if (magic !== MAGIC_HEADER) {
throw new Error('Invalid binary format: magic header mismatch');
}
const version = view.getUint32(4, true);
if (version !== VERSION) {
throw new Error(`Unsupported binary format version: ${version}`);
}
const data_length = view.getUint32(8, true);
const compressed_data = buffer.slice(12, 12 + data_length);
const compressed = new TextDecoder().decode(compressed_data);
const json_str = decompress(compressed);
return JSON.parse(json_str);
}

View File

@@ -14,5 +14,6 @@ export * as Utils from './Utils.ts';
export * as Log from './Log.ts';
export * as LZW from './LZW.ts';
export * as BinarySerializer from './BinarySerializer.ts';
export * as ProtoUtils from './ProtoUtils.ts';