mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-19 04:21:35 +00:00
chore: v15.0.1 release
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);`;
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
46
deno/src/utils/BinarySerializer.ts
Normal file
46
deno/src/utils/BinarySerializer.ts
Normal 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);
|
||||
}
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user