mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-16 19:12:24 +00:00
* dev: add response types * dev: refactor `Parser#parseResponse()` * dev: update YouTube parsers * dev: update YouTube Music classes * dev: update YouTube Kids classes * dev: update core classes * dev(Parser): fix some inconsistencies * chore: update docs * chore: update docs x2 * fix: export response types * chore(docs): update parser example
206 lines
6.2 KiB
TypeScript
206 lines
6.2 KiB
TypeScript
import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.js';
|
|
|
|
import Constants from '../utils/Constants.js';
|
|
|
|
import { ICache } from '../types/Cache.js';
|
|
import { FetchFunction } from '../types/PlatformShim.js';
|
|
|
|
export default class Player {
|
|
#nsig_sc;
|
|
#sig_sc;
|
|
#sig_sc_timestamp;
|
|
#player_id;
|
|
|
|
constructor(signature_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) {
|
|
this.#nsig_sc = nsig_sc;
|
|
this.#sig_sc = sig_sc;
|
|
|
|
this.#sig_sc_timestamp = signature_timestamp;
|
|
|
|
this.#player_id = player_id;
|
|
}
|
|
|
|
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise<Player> {
|
|
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
|
|
const res = await fetch(url);
|
|
|
|
if (res.status !== 200)
|
|
throw new PlayerError('Failed to request player id');
|
|
|
|
const js = await res.text();
|
|
|
|
const player_id = getStringBetweenStrings(js, 'player\\/', '\\/');
|
|
|
|
if (!player_id)
|
|
throw new PlayerError('Failed to get player id');
|
|
|
|
// We have the playerID now we can check if we have a cached player
|
|
if (cache) {
|
|
const cached_player = await Player.fromCache(cache, player_id);
|
|
if (cached_player)
|
|
return cached_player;
|
|
}
|
|
|
|
const player_url = new URL(`/s/player/${player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE);
|
|
|
|
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 sig_timestamp = this.extractSigTimestamp(player_js);
|
|
|
|
const sig_sc = this.extractSigSourceCode(player_js);
|
|
const nsig_sc = this.extractNSigSourceCode(player_js);
|
|
|
|
return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id);
|
|
}
|
|
|
|
decipher(url?: string, signature_cipher?: string, cipher?: string): string {
|
|
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 (signature_cipher || cipher) {
|
|
const signature = Platform.shim.eval(this.#sig_sc, {
|
|
sig: args.get('s')
|
|
});
|
|
|
|
if (typeof signature !== 'string')
|
|
throw new PlayerError('Failed to decipher signature');
|
|
|
|
const sp = args.get('sp');
|
|
|
|
sp ?
|
|
url_components.searchParams.set(sp, signature) :
|
|
url_components.searchParams.set('signature', signature);
|
|
}
|
|
|
|
const n = url_components.searchParams.get('n');
|
|
|
|
if (n) {
|
|
const nsig = Platform.shim.eval(this.#nsig_sc, {
|
|
nsig: n
|
|
});
|
|
|
|
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!');
|
|
}
|
|
|
|
url_components.searchParams.set('n', nsig);
|
|
}
|
|
|
|
return url_components.toString();
|
|
}
|
|
|
|
static async fromCache(cache: ICache, player_id: string): Promise<Player | null> {
|
|
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 decoder = new TextDecoder();
|
|
|
|
const sig_sc = decoder.decode(sig_buf);
|
|
const nsig_sc = decoder.decode(nsig_buf);
|
|
|
|
return new Player(sig_timestamp, sig_sc, nsig_sc, player_id);
|
|
}
|
|
|
|
static async fromSource(cache: ICache | undefined, sig_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string): Promise<Player> {
|
|
const player = new Player(sig_timestamp, sig_sc, nsig_sc, player_id);
|
|
await player.cache(cache);
|
|
return player;
|
|
}
|
|
|
|
async cache(cache?: ICache): Promise<void> {
|
|
if (!cache) return;
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
const sig_buf = encoder.encode(this.#sig_sc);
|
|
const nsig_buf = encoder.encode(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.#sig_sc_timestamp, 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: string): number {
|
|
return parseInt(getStringBetweenStrings(data, 'signatureTimestamp:', ',') || '0');
|
|
}
|
|
|
|
static extractSigSourceCode(data: string): string {
|
|
const calls = getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
|
|
const obj_name = calls?.split(/\.|\[/)?.[0]?.replace(';', '')?.trim();
|
|
const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};');
|
|
|
|
if (!functions || !calls)
|
|
console.warn(new PlayerError('Failed to extract signature decipher algorithm'));
|
|
|
|
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`;
|
|
}
|
|
|
|
static extractNSigSourceCode(data: string): string {
|
|
const sc = `function descramble_nsig(a) { let b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join(""); } descramble_nsig(nsig)`;
|
|
|
|
if (!sc)
|
|
console.warn(new PlayerError('Failed to extract n-token decipher algorithm'));
|
|
|
|
return sc;
|
|
}
|
|
|
|
get url(): string {
|
|
return new URL(`/s/player/${this.#player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString();
|
|
}
|
|
|
|
get sts(): number {
|
|
return this.#sig_sc_timestamp;
|
|
}
|
|
|
|
get nsig_sc(): string {
|
|
return this.#nsig_sc;
|
|
}
|
|
|
|
get sig_sc(): string {
|
|
return this.#sig_sc;
|
|
}
|
|
|
|
static get LIBRARY_VERSION(): number {
|
|
return 2;
|
|
}
|
|
} |