diff --git a/src/core/Player.ts b/src/core/Player.ts index 5a07170a..4eb2577b 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -10,6 +10,7 @@ import type { FetchFunction } from '../types/PlatformShim.js'; */ export default class Player { #nsig_sc; + #nsig_cache; #sig_sc; #sig_sc_timestamp; #player_id; @@ -21,6 +22,8 @@ export default class Player { this.#sig_sc_timestamp = signature_timestamp; this.#player_id = player_id; + + this.#nsig_cache = new Map(); } static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise { @@ -66,7 +69,7 @@ export default class Player { return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id); } - decipher(url?: string, signature_cipher?: string, cipher?: string): string { + decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map): string { url = url || signature_cipher || cipher; if (!url) @@ -93,15 +96,23 @@ export default class Player { const n = url_components.searchParams.get('n'); if (n) { - const nsig = Platform.shim.eval(this.#nsig_sc, { - nsig: n - }); + let nsig; - if (typeof nsig !== 'string') - throw new PlayerError('Failed to decipher nsig'); + 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: n + }); - if (nsig.startsWith('enhanced_except_')) { - console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!'); + if (typeof nsig !== 'string') + throw new PlayerError('Failed to decipher nsig'); + + if (nsig.startsWith('enhanced_except_')) { + console.warn('Warning:\nCould 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); + } } url_components.searchParams.set('n', nsig); diff --git a/src/parser/classes/misc/Format.ts b/src/parser/classes/misc/Format.ts index 5ae529c2..bf564bc1 100644 --- a/src/parser/classes/misc/Format.ts +++ b/src/parser/classes/misc/Format.ts @@ -3,6 +3,8 @@ import { InnertubeError } from '../../../utils/Utils.js'; import type { RawNode } from '../../index.js'; export default class Format { + #this_response_nsig_cache?: Map; + itag: number; mime_type: string; is_type_otf: boolean; @@ -52,7 +54,11 @@ export default class Format { matrix_coefficients?: string; }; - constructor(data: RawNode) { + constructor(data: RawNode, this_response_nsig_cache?: Map) { + if (this_response_nsig_cache) { + this.#this_response_nsig_cache = this_response_nsig_cache; + } + this.itag = data.itag; this.mime_type = data.mimeType; this.is_type_otf = data.type === 'FORMAT_STREAM_TYPE_OTF'; @@ -122,6 +128,6 @@ export default class Format { */ decipher(player: Player | undefined): string { if (!player) throw new InnertubeError('Cannot decipher format, this session appears to have no valid player.'); - return player.decipher(this.url, this.signature_cipher, this.cipher); + return player.decipher(this.url, this.signature_cipher, this.cipher, this.#this_response_nsig_cache); } } \ No newline at end of file diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 78926087..3ee24c95 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -367,15 +367,21 @@ export function parseResponse(data: parsed_data.playability_status = playability_status; } - const streaming_data = data.streamingData ? { - expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000), - formats: parseFormats(data.streamingData.formats), - adaptive_formats: parseFormats(data.streamingData.adaptiveFormats), - dash_manifest_url: data.streamingData.dashManifestUrl || null, - hls_manifest_url: data.streamingData.hlsManifestUrl || null - } : undefined; + if (data.streamingData) { + // Currently each response with streaming data only has two n param values + // One for the adaptive formats and another for the combined formats + // As they are the same for a response, we only need to decipher them once + // For all futher deciphering calls on formats from that response, we can use the cached output, given the same input n param + const this_response_nsig_cache = new Map(); + + const streaming_data = { + expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000), + formats: parseFormats(data.streamingData.formats, this_response_nsig_cache), + adaptive_formats: parseFormats(data.streamingData.adaptiveFormats, this_response_nsig_cache), + dash_manifest_url: data.streamingData.dashManifestUrl || null, + hls_manifest_url: data.streamingData.hlsManifestUrl || null + }; - if (streaming_data) { parsed_data.streaming_data = streaming_data; } @@ -598,8 +604,8 @@ export function parseActions(data: RawData) { return new SuperParsedResult(parseItem(data)); } -export function parseFormats(formats: RawNode[]): Format[] { - return formats?.map((format) => new Format(format)) || []; +export function parseFormats(formats: RawNode[], this_response_nsig_cache: Map): Format[] { + return formats?.map((format) => new Format(format, this_response_nsig_cache)) || []; } export function applyMutations(memo: Memo, mutations: RawNode[]) {