diff --git a/deno/package.json b/deno/package.json index 034dbcf7..57de4827 100644 --- a/deno/package.json +++ b/deno/package.json @@ -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": { diff --git a/deno/src/Innertube.ts b/deno/src/Innertube.ts index f92507b9..95c24883 100644 --- a/deno/src/Innertube.ts +++ b/deno/src/Innertube.ts @@ -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 { 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(this.#session.actions, { parse: true }); + + if (response.on_response_received_actions?.[0].is(NavigateAction)) { + response = await response.on_response_received_actions[0].endpoint.call(this.#session.actions, { parse: true }); + } + + return new Channel(this.actions, response, true); } async getNotifications(): Promise { diff --git a/deno/src/core/Player.ts b/deno/src/core/Player.ts index 9c6d4b18..86837f32 100644 --- a/deno/src/core/Player.ts +++ b/deno/src/core/Player.ts @@ -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 { - 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(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 { @@ -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);`; } diff --git a/deno/src/core/Session.ts b/deno/src/core/Session.ts index d23454b7..d4a00536 100644 --- a/deno/src/core/Session.ts +++ b/deno/src/core/Session.ts @@ -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(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 { diff --git a/deno/src/utils/BinarySerializer.ts b/deno/src/utils/BinarySerializer.ts new file mode 100644 index 00000000..a83ec861 --- /dev/null +++ b/deno/src/utils/BinarySerializer.ts @@ -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(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); +} diff --git a/deno/src/utils/index.ts b/deno/src/utils/index.ts index cb002514..28983f7e 100644 --- a/deno/src/utils/index.ts +++ b/deno/src/utils/index.ts @@ -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'; \ No newline at end of file