@imput/youtubei.js
Version:
A JavaScript client for YouTube's private API, known as InnerTube. Fork of youtubei.js
236 lines • 11.7 kB
JavaScript
import { Jinter } from 'jintr';
import { Constants, Log, LZW } from '../utils/index.js';
import { findFunction, findVariable, getRandomUserAgent, getStringBetweenStrings, Platform, PlayerError } from '../utils/Utils.js';
const TAG = 'Player';
/**
* Represents YouTube's player script. This is required to decipher signatures.
*/
export default class Player {
constructor(player_id, signature_timestamp, sig_sc, nsig_sc) {
this.player_id = player_id;
this.sts = signature_timestamp;
this.nsig_sc = nsig_sc;
this.sig_sc = sig_sc;
}
static async create(cache, fetch = Platform.shim.fetch, po_token, player_id) {
if (!player_id) {
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
const res = await fetch(url);
if (!res.ok)
throw new PlayerError(`Failed to get player id: ${res.status} (${res.statusText})`);
const js = await res.text();
player_id = getStringBetweenStrings(js, 'player\\/', '\\/');
}
Log.info(TAG, `Using player id (${player_id}). Checking for cached players..`);
if (!player_id)
throw new PlayerError('Failed to get player id');
// We have the player id, now we can check if we have a cached player.
if (cache) {
const cached_player = await Player.fromCache(cache, player_id);
if (cached_player) {
Log.info(TAG, 'Found up-to-date player data in cache.');
cached_player.po_token = po_token;
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(TAG, `Could not find any cached player. Will download a new player from ${player_url}.`);
const player_res = await fetch(player_url, {
headers: {
'user-agent': getRandomUserAgent('desktop')
}
});
if (!player_res.ok) {
throw new PlayerError(`Failed to get player data: ${player_res.status}`);
}
const player_js = await player_res.text();
const ast = Jinter.parseScript(player_js, { ecmaVersion: 'latest', ranges: true });
const sig_timestamp = this.extractSigTimestamp(player_js);
const global_variable = this.extractGlobalVariable(player_js, ast);
const sig_sc = this.extractSigSourceCode(player_js, global_variable);
const nsig_sc = this.extractNSigSourceCode(player_js, ast, global_variable);
Log.info(TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`);
const player = await Player.fromSource(player_id, sig_timestamp, cache, sig_sc, nsig_sc);
player.po_token = po_token;
return player;
}
decipher(url, signature_cipher, cipher, this_response_nsig_cache) {
url = url || signature_cipher || cipher;
if (!url)
throw new PlayerError('No valid URL to decipher');
const args = new URLSearchParams(url);
const url_components = new URL(args.get('url') || url);
if (this.sig_sc && (signature_cipher || cipher)) {
const signature = Platform.shim.eval(this.sig_sc, {
sig: args.get('s')
});
Log.info(TAG, `Transformed signature from ${args.get('s')} to ${signature}.`);
if (typeof signature !== 'string')
throw new PlayerError('Failed to decipher signature');
const sp = args.get('sp');
if (sp) {
url_components.searchParams.set(sp, signature);
}
else {
url_components.searchParams.set('signature', signature);
}
}
const n = url_components.searchParams.get('n');
if (this.nsig_sc && n) {
let nsig;
if (this_response_nsig_cache && this_response_nsig_cache.has(n)) {
nsig = this_response_nsig_cache.get(n);
}
else {
nsig = Platform.shim.eval(this.nsig_sc, {
nsig: n
});
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(TAG, 'Something went wrong while deciphering nsig.');
}
else if (this_response_nsig_cache) {
this_response_nsig_cache.set(n, nsig);
}
}
url_components.searchParams.set('n', nsig);
}
// @NOTE: SABR requests should include the PoToken (not base64d, but as bytes!) in the payload.
if (url_components.searchParams.get('sabr') !== '1' && this.po_token)
url_components.searchParams.set('pot', this.po_token);
const client = url_components.searchParams.get('c');
switch (client) {
case 'WEB':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB.VERSION);
break;
case 'MWEB':
url_components.searchParams.set('cver', Constants.CLIENTS.MWEB.VERSION);
break;
case 'WEB_REMIX':
url_components.searchParams.set('cver', Constants.CLIENTS.YTMUSIC.VERSION);
break;
case 'WEB_KIDS':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB_KIDS.VERSION);
break;
case 'TVHTML5':
url_components.searchParams.set('cver', Constants.CLIENTS.TV.VERSION);
break;
case 'TVHTML5_SIMPLY':
url_components.searchParams.set('cver', Constants.CLIENTS.TV_SIMPLY.VERSION);
break;
case 'TVHTML5_SIMPLY_EMBEDDED_PLAYER':
url_components.searchParams.set('cver', Constants.CLIENTS.TV_EMBEDDED.VERSION);
break;
case 'WEB_EMBEDDED_PLAYER':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB_EMBEDDED.VERSION);
break;
}
const result = url_components.toString();
Log.info(TAG, `Deciphered URL: ${result}`);
return url_components.toString();
}
static async fromCache(cache, player_id) {
const buffer = await cache.get(player_id);
if (!buffer)
return null;
const view = new DataView(buffer);
const version = view.getUint32(0, true);
if (version !== Player.LIBRARY_VERSION)
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, sig_timestamp, cache, sig_sc, nsig_sc) {
const player = new Player(player_id, sig_timestamp, sig_sc, nsig_sc);
await player.cache(cache);
return player;
}
async cache(cache) {
if (!cache || !this.sig_sc || !this.nsig_sc)
return;
const encoder = new TextEncoder();
const sig_buf = encoder.encode(LZW.compress(this.sig_sc));
const nsig_buf = encoder.encode(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.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));
}
static extractSigTimestamp(data) {
return parseInt(getStringBetweenStrings(data, 'signatureTimestamp:', ',') || '0');
}
static extractGlobalVariable(data, ast) {
let variable = findVariable(data, { includes: '-_w8_', ast });
// For redundancy/the above fails:
if (!variable)
variable = findVariable(data, { includes: 'Untrusted URL{', ast });
if (!variable)
variable = findVariable(data, { includes: '1969', ast });
if (!variable)
variable = findVariable(data, { includes: '1970', ast });
if (!variable)
variable = findVariable(data, { includes: 'playerfallback', ast });
return variable;
}
static extractSigSourceCode(data, global_variable) {
// Classic static split/join.
const split_join_regex = /function\(([A-Za-z_0-9]+)\)\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\.split\((?:[^)]+)\)(.+?)\.join\((?:[^)]+)\))\}/;
// Using the global lookup variable.
const lookup_var = global_variable?.name?.replace(/[$^\\.*+?()[\]{}|]/g, '\\$&');
const lookup_regex = lookup_var
? new RegExp(`function\\(([A-Za-z_0-9]+)\\)\\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\\[${lookup_var}\\[\\d+\\]\\]\\([^)]*\\)([\\s\\S]+?)\\[${lookup_var}\\[\\d+\\]\\]\\([^)]*\\))\\}`)
: null;
const match = data.match(split_join_regex) || (lookup_regex ? data.match(lookup_regex) : null);
if (!match) {
Log.warn(TAG, 'Failed to extract signature decipher algorithm.');
return;
}
const var_name = match[1];
const obj_name = match[3].split(/\.|\[/)[0]?.replace(';', '').trim();
const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};');
if (!functions || !var_name)
Log.warn(TAG, 'Failed to extract signature decipher algorithm.');
return `${global_variable?.result || ''} function descramble_sig(${var_name}) { let ${obj_name}={${functions}}; ${match[2]} } descramble_sig(sig);`;
}
static extractNSigSourceCode(data, ast, global_variable) {
let nsig_function;
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);`;
}
get url() {
return new URL(`/s/player/${this.player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString();
}
static get LIBRARY_VERSION() {
return 14;
}
}
//# sourceMappingURL=Player.js.map