feat(download): bring back WEB client (#156)

* refactor: remove dead code and integrate with Jinter

* chore: tidy up
This commit is contained in:
LuanRT
2022-08-29 04:48:33 -03:00
committed by GitHub
parent 173aec65f5
commit 317bca261c
9 changed files with 132 additions and 728 deletions

View File

@@ -48,6 +48,8 @@ export interface SearchFilters {
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count';
}
export type InnerTubeClient = 'ANDROID' | 'WEB' | 'YTMUSIC';
class Innertube {
session;
account;
@@ -74,16 +76,10 @@ class Innertube {
/**
* Retrieves video info.
*/
async getInfo(video_id: string | undefined) {
throwIfMissing({ video_id });
async getInfo(video_id: string, client?: InnerTubeClient) {
const cpn = generateRandomString(16);
const initial_info = await this.actions.execute('/player', {
client: 'ANDROID',
videoId: video_id
});
const initial_info = await this.actions.getVideoInfo(video_id, cpn, client);
const continuation = this.actions.next({ video_id });
const response = await Promise.all([ initial_info, continuation ]);
@@ -93,15 +89,9 @@ class Innertube {
/**
* Retrieves basic video info.
*/
async getBasicInfo(video_id: string | undefined) {
throwIfMissing({ video_id });
async getBasicInfo(video_id: string, client?: InnerTubeClient) {
const cpn = generateRandomString(16);
const response = await this.actions.execute('/player', {
client: 'ANDROID',
videoId: video_id
});
const response = await this.actions.getVideoInfo(video_id, cpn, client);
return new VideoInfo([ response ], this.actions, this.session.player, cpn);
}
@@ -246,13 +236,12 @@ class Innertube {
}
/**
* Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}.
* Downloads a given video. If you only need the direct download link see {@link getStreamingData}.
*
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
*/
async download(video_id: string | undefined, options?: DownloadOptions) {
throwIfMissing({ video_id });
const info = await this.getBasicInfo(video_id);
async download(video_id: string, options?: DownloadOptions) {
const info = await this.getBasicInfo(video_id, options?.client);
return info.download(options);
}

View File

@@ -1,25 +1,24 @@
import { FetchFunction } from '../utils/HTTPClient';
import { getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils';
import Constants from '../utils/Constants';
import UniversalCache from '../utils/Cache';
// Import NToken from '../deciphers/NToken';
// Import Signature from '../deciphers/Signature';
// See https://github.com/LuanRT/Jinter
import Jinter from 'jintr';
export default class Player {
// #ntoken;
// #signature;
#signature_timestamp;
#nsig_sc;
#sig_sc;
#sig_sc_timestamp;
#player_id;
constructor(signature_timestamp: number, player_id: string) {
// This.#ntoken = ntoken;
// This.#signature = signature;
constructor(signature_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) {
this.#nsig_sc = nsig_sc;
this.#sig_sc = sig_sc;
this.#signature_timestamp = signature_timestamp;
this.#sig_sc_timestamp = signature_timestamp;
this.#player_id = player_id;
}
@@ -61,14 +60,13 @@ export default class Player {
const sig_timestamp = this.extractSigTimestamp(player_js);
// Const sig_decipher_sc = this.extractSigDecipherSc(player_js);
// Const ntoken_sc = this.extractNTokenSc(player_js);
const sig_sc = this.extractSigSourceCode(player_js);
const nsig_sc = this.extractNSigSourceCode(player_js);
return await Player.fromSource(cache, sig_timestamp, player_id);
return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id);
}
/*
Decipher(url?: string, signature_cipher?: string, cipher?: string) {
decipher(url?: string, signature_cipher?: string, cipher?: string) {
url = url || signature_cipher || cipher;
if (!url)
@@ -80,7 +78,11 @@ export default class Player {
url_components.searchParams.set('ratebypass', 'yes');
if (signature_cipher || cipher) {
const signature = this.#signature.decipher(url);
const sig_decipher = new Jinter(this.#sig_sc);
sig_decipher.scope.set('sig', args.get('s'));
const signature = sig_decipher.interpret();
const sp = args.get('sp');
sp ?
@@ -91,12 +93,20 @@ export default class Player {
const n = url_components.searchParams.get('n');
if (n) {
const ntoken = this.#ntoken.transform(n);
url_components.searchParams.set('n', ntoken);
const nsig_decipher = new Jinter(this.#nsig_sc);
nsig_decipher.scope.set('nsig', n);
const nsig = nsig_decipher.interpret();
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: UniversalCache, player_id: string) {
const buffer = await cache.get(player_id);
@@ -112,17 +122,20 @@ export default class Player {
const sig_timestamp = view.getUint32(4, true);
/*
* Const sig_decipher_len = view.getUint32(8, true);
* const sig_decipher_buf = buffer.slice(12, 12 + sig_decipher_len);
* const ntoken_transform_buf = buffer.slice(12 + sig_decipher_len);
*/
const sig_len = view.getUint32(8, true);
const sig_buf = buffer.slice(12, 12 + sig_len);
const nsig_buf = buffer.slice(12 + sig_len);
return new Player(sig_timestamp, player_id);
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: UniversalCache | undefined, sig_timestamp: number, player_id: string) {
const player = new Player(sig_timestamp, player_id);
static async fromSource(cache: UniversalCache | undefined, sig_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) {
const player = new Player(sig_timestamp, sig_sc, nsig_sc, player_id);
await player.cache(cache);
return player;
}
@@ -130,52 +143,41 @@ export default class Player {
async cache(cache?: UniversalCache) {
if (!cache) return;
/**
* Const ntoken_buf = this.#ntoken.toArrayBuffer();
* const sig_decipher_buf = this.#signature.toArrayBuffer();
*/
const encoder = new TextEncoder();
const buffer = new ArrayBuffer(12 /* + sig_decipher_buf.byteLength + ntoken_buf.byteLength */);
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.#signature_timestamp, true);
view.setUint32(4, this.#sig_sc_timestamp, true);
view.setUint32(8, sig_buf.byteLength, true);
// View.setUint32(8, sig_decipher_buf.byteLength, true);
// New Uint8Array(buffer).set(new Uint8Array(sig_decipher_buf), 12);
// New Uint8Array(buffer).set(new Uint8Array(ntoken_buf), 12 + sig_decipher_buf.byteLength);
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));
}
/**
* Extracts the signature timestamp from the player source code.
*/
static extractSigTimestamp(data: string) {
return parseInt(getStringBetweenStrings(data, 'signatureTimestamp:', ',') || '0');
}
/**
* Extracts the signature decipher algorithm.
*/
static extractSigDecipherSc(data: string) {
const sig_alg_sc = getStringBetweenStrings(data, 'this.audioTracks};var', '};');
const sig_data = getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
static extractSigSourceCode(data: string) {
const funcs = getStringBetweenStrings(data, 'this.audioTracks};var', '};');
const calls = getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
if (!sig_alg_sc || !sig_data)
if (!funcs || !calls)
throw new PlayerError('Failed to extract signature decipher algorithm');
return sig_alg_sc + sig_data;
return `function descramble_sig(a) { a = a.split(""); ${funcs}}${calls} return a.join("") } descramble_sig(sig);`;
}
/**
* Extracts the n-token decipher algorithm.
*/
static extractNTokenSc(data: string) {
const sc = `var b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join("");`;
static extractNSigSourceCode(data: 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)`;
console.log(sc);
if (!sc)
throw new PlayerError('Failed to extract n-token decipher algorithm');
@@ -187,10 +189,18 @@ export default class Player {
}
get sts() {
return this.#signature_timestamp;
return this.#sig_sc_timestamp;
}
get nsig_sc() {
return this.#nsig_sc;
}
get sig_sc() {
return this.#sig_sc;
}
static get LIBRARY_VERSION() {
return 1;
return 2;
}
}

View File

@@ -1,465 +0,0 @@
import { NTOKEN_REGEX, BASE64_DIALECT } from '../utils/Constants';
import { InnertubeError } from '../utils/Utils';
export enum NTokenTransformOperation {
NO_OP = 0,
PUSH,
REVERSE_1,
REVERSE_2,
SPLICE,
SWAP0_1,
SWAP0_2,
ROTATE_1,
ROTATE_2,
BASE64_DIA,
TRANSLATE_1,
TRANSLATE_2,
}
export enum NTokenTransformOpType {
FUNC,
N_ARR,
LITERAL,
REF
}
const OP_LOOKUP: Record<string, NTokenTransformOperation> = {
'd.push(e)': NTokenTransformOperation.PUSH,
'd.reverse()': NTokenTransformOperation.REVERSE_1,
'function(d){for(var': NTokenTransformOperation.REVERSE_2,
'd.length;d.splice(e,1)': NTokenTransformOperation.SPLICE,
'd[0])[0])': NTokenTransformOperation.SWAP0_1,
'f=d[0];d[0]': NTokenTransformOperation.SWAP0_2,
'reverse().forEach': NTokenTransformOperation.ROTATE_1,
'unshift(d.pop())': NTokenTransformOperation.ROTATE_2,
'function(){for(var': NTokenTransformOperation.BASE64_DIA,
'function(d,e){for(var f': NTokenTransformOperation.TRANSLATE_1,
'function(d,e,f){var': NTokenTransformOperation.TRANSLATE_2
};
export class NTokenTransforms {
/**
* Gets a base64 alphabet and uses it as a lookup table to modify n.
*/
static translate1(arr: any[], token: string, is_reverse_base64: boolean) {
const characters = is_reverse_base64 && BASE64_DIALECT.REVERSE || BASE64_DIALECT.NORMAL;
const that = token.split('');
arr.forEach((char, index, loc) => {
that.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(that[index]) + 64) % characters.length]);
});
}
static translate2(arr: any[], token: string, characters: string[]) {
let chars_length = characters.length;
const that = token.split('');
arr.forEach((char, index, loc) => {
that.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(that[index]) + index + chars_length--) % characters.length]);
});
}
/**
* Returns the requested base64 dialect, currently this is only used by 'translate2'.
*/
static getBase64Dia(is_reverse_base64: boolean) {
const characters = is_reverse_base64 && BASE64_DIALECT.REVERSE || BASE64_DIALECT.NORMAL;
return characters;
}
/**
* Swaps the first element with the one at the given index.
*/
static swap0(arr: any[], index: number) {
const old_elem = arr[0];
index = (index % arr.length + arr.length) % arr.length;
arr[0] = arr[index];
arr[index] = old_elem;
}
/**
* Rotates elements of the array.
*/
static rotate(arr: any[], index: number) {
index = (index % arr.length + arr.length) % arr.length;
arr.splice(-index).reverse().forEach((el) => arr.unshift(el));
}
/**
* Deletes one element at the given index.
*/
static splice(arr: any[], index: number) {
index = (index % arr.length + arr.length) % arr.length;
arr.splice(index, 1);
}
static reverse(arr: any[]) {
arr.reverse();
}
static push(arr: any[], item: any) {
if (Array.isArray(arr?.[0])) {
arr.push([ NTokenTransformOpType.LITERAL, item ]);
} else {
arr.push(item);
}
}
}
const TRANSFORM_FUNCTIONS: [Record<number, any>, Record<number, any>] = [ {
[NTokenTransformOperation.PUSH]: NTokenTransforms.push,
[NTokenTransformOperation.SPLICE]: NTokenTransforms.splice,
[NTokenTransformOperation.SWAP0_1]: NTokenTransforms.swap0,
[NTokenTransformOperation.SWAP0_2]: NTokenTransforms.swap0,
[NTokenTransformOperation.ROTATE_1]: NTokenTransforms.rotate,
[NTokenTransformOperation.ROTATE_2]: NTokenTransforms.rotate,
[NTokenTransformOperation.REVERSE_1]: NTokenTransforms.reverse,
[NTokenTransformOperation.REVERSE_2]: NTokenTransforms.reverse,
[NTokenTransformOperation.BASE64_DIA]: () => NTokenTransforms.getBase64Dia(false),
[NTokenTransformOperation.TRANSLATE_1]: (...args: any[]) => NTokenTransforms.translate1.apply(null, [ ...args, false ] as any),
[NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2
}, {
[NTokenTransformOperation.PUSH]: NTokenTransforms.push,
[NTokenTransformOperation.SPLICE]: NTokenTransforms.splice,
[NTokenTransformOperation.SWAP0_1]: NTokenTransforms.swap0,
[NTokenTransformOperation.SWAP0_2]: NTokenTransforms.swap0,
[NTokenTransformOperation.ROTATE_1]: NTokenTransforms.rotate,
[NTokenTransformOperation.ROTATE_2]: NTokenTransforms.rotate,
[NTokenTransformOperation.REVERSE_1]: NTokenTransforms.reverse,
[NTokenTransformOperation.REVERSE_2]: NTokenTransforms.reverse,
[NTokenTransformOperation.BASE64_DIA]: () => NTokenTransforms.getBase64Dia(true),
[NTokenTransformOperation.TRANSLATE_1]: (...args: any[]) => NTokenTransforms.translate1.apply(null, [ ...args, true ] as any),
[NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2
} ];
export type NTokenCall = [number, number[]];
export type NTokenInstruction = [NTokenTransformOpType, (NTokenTransformOperation | number)?, number?];
export type NTokenTransformer = [NTokenInstruction[], NTokenCall[]];
export default class NToken {
private transformer: NTokenTransformer;
constructor(transformer: NTokenTransformer) {
this.transformer = transformer;
}
static fromSourceCode(raw: string) {
const transformation_data = NToken.getTransformationData(raw);
const transformations = transformation_data.map((el) => {
if (el != null && typeof el != 'number') {
const is_reverse_base64 = el.includes('case 65:');
const func = NToken.getFunc(el)?.[0];
const opcode = func ? OP_LOOKUP[func] : undefined;
if (opcode) {
el = [ NTokenTransformOpType.FUNC, opcode, 0 + is_reverse_base64 ];
} else if (el == 'b') {
el = [ NTokenTransformOpType.N_ARR ];
} else {
el = [ NTokenTransformOpType.LITERAL, el ];
}
} else if (el != null) {
el = [ NTokenTransformOpType.LITERAL, el ];
}
return el;
});
// Fills all placeholders with the transformations array
const placeholder_indexes = [ ...raw.matchAll(NTOKEN_REGEX.PLACEHOLDERS) ].map((item) => parseInt(item[1]));
placeholder_indexes.forEach((i) => transformations[i] = [ NTokenTransformOpType.REF ]);
// Parses and emulates calls to the functions of the transformations array
const function_body = raw.replace(/\n/g, '').match(/try\{(.*?)\}catch/s)?.[1];
if (!function_body) {
throw new InnertubeError('Invalid NToken transformation function.', { transformation: raw });
}
const function_calls = [ ...function_body.matchAll(NTOKEN_REGEX.CALLS) ].map((params) => [
parseInt(params[1]),
params[2].split(',').map((param: string) => {
const param_value = param.match(/c\[(.*?)\]/)?.[1];
if (!param_value) {
throw new InnertubeError('Unexpected NToken transformation function parameter.', { transformation: raw, param });
}
return parseInt(param_value);
})
] as NTokenCall
);
return new NToken([ transformations, function_calls ]);
}
private evaluate(i: NTokenInstruction, nToken: string[], transformer: NTokenTransformer) {
switch (i[0]) {
case NTokenTransformOpType.FUNC:
if (i[1] === undefined || i[2] === undefined)
throw new InnertubeError('Invalid NTokenInstruction.', { transformation: nToken, instruction: i });
return TRANSFORM_FUNCTIONS[i[2]][i[1]];
case NTokenTransformOpType.N_ARR:
return nToken;
case NTokenTransformOpType.LITERAL:
return i[1];
case NTokenTransformOpType.REF:
return transformer[0];
}
}
transform(n: string) {
const nToken = n.split('');
// We must copy since we will modify the array
const transformer: NTokenTransformer = this.getTransformerClone();
try {
transformer[1].forEach(([ index, param_index ]) => {
const base64_dia = (param_index[2] !== undefined && this.evaluate(transformer[0][param_index[2]], nToken, transformer)());
this.evaluate(transformer[0][index], nToken, transformer)(
param_index[0] !== undefined && this.evaluate(transformer[0][param_index[0]], nToken, transformer) || undefined,
param_index[1] !== undefined && this.evaluate(transformer[0][param_index[1]], nToken, transformer) || undefined,
base64_dia || undefined
);
});
} catch (e) {
console.error(new Error(`Could not transform n-token, download may be throttled.\nOriginal Token:${n}\nError:\n${(e as Error).stack}`));
return n;
}
return nToken.join('');
}
private getTransformerClone(): NTokenTransformer {
return [ [ ...this.transformer[0] ], [ ...this.transformer[1] ] ];
}
toJSON() {
return this.getTransformerClone();
}
toArrayBuffer() {
// (16 bit FUNC instructions) 2 bit op - 1 bit is_reverse_base64 - 4 bit nonce - 8 bit operation
// (8 bit N_ARG and REF) 2 bit op - 6 bit nonce
// (40 bit LITERAL) 2 bit op - 6 bit nonce - 32 bit value
// NTokenCall will be 8 bit for the index, 8 bit for the number of parameters, and 8 bit for each parameter
// We've got a 3 * 32 bit header to store the library version and the size of the two arrays
let size = 4 * 3;
for (const instruction of this.transformer[0]) {
switch (instruction[0]) {
case NTokenTransformOpType.FUNC:
size += 2;
break;
case NTokenTransformOpType.N_ARR:
case NTokenTransformOpType.REF:
size += 1;
break;
case NTokenTransformOpType.LITERAL:
if (typeof instruction[1] === 'string') {
size += 1 + 4 + new TextEncoder().encode(instruction[1] as string).byteLength;
}
size += 4 + 1;
break;
}
}
for (const call of this.transformer[1]) {
size += 2 + call[1].length;
}
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
let offset = 0;
view.setUint32(offset, NToken.LIBRARY_VERSION, true);
offset += 4;
view.setUint32(offset, this.transformer[0].length, true);
offset += 4;
view.setUint32(offset, this.transformer[1].length, true);
offset += 4;
for (const instruction of this.transformer[0]) {
switch (instruction[0]) {
case NTokenTransformOpType.FUNC:
{
if (instruction[1] === undefined || instruction[2] === undefined)
throw new InnertubeError('Invalid NTokenInstruction.', { transformation: this.transformer, instruction });
const opcode = (instruction[0] << 6) | instruction[2];
view.setUint8(offset, opcode);
offset += 1;
view.setUint8(offset, instruction[1]);
offset += 1;
}
break;
case NTokenTransformOpType.N_ARR:
case NTokenTransformOpType.REF:
{
const opcode = (instruction[0] << 6);
view.setUint8(offset, opcode);
offset += 1;
}
break;
case NTokenTransformOpType.LITERAL:
{
if (instruction[1] === undefined)
throw new InnertubeError('Invalid NTokenInstruction.', { transformation: this.transformer, instruction });
const type = typeof instruction[1] === 'string' ? 1 : 0;
const opcode = (instruction[0] << 6) | type;
view.setUint8(offset, opcode);
offset += 1;
if (type === 0) {
view.setInt32(offset, instruction[1], true);
offset += 4;
} else {
const encoded = new TextEncoder().encode(instruction[1] as any);
view.setUint32(offset, encoded.byteLength, true);
offset += 4;
for (let i = 0; i < encoded.byteLength; i++) {
view.setUint8(offset, encoded[i]);
offset += 1;
}
}
}
break;
}
}
for (const call of this.transformer[1]) {
view.setUint8(offset, call[0]);
offset += 1;
view.setUint8(offset, call[1].length);
offset += 1;
for (const param of call[1]) {
view.setUint8(offset, param);
offset += 1;
}
}
return buffer;
}
static fromArrayBuffer(buffer: ArrayBuffer) {
const view = new DataView(buffer);
let offset = 0;
const version = view.getUint32(offset, true);
offset += 4;
if (version !== NToken.LIBRARY_VERSION)
throw new TypeError('Invalid library version');
const transformations_length = view.getUint32(offset, true);
offset += 4;
const function_calls_length = view.getUint32(offset, true);
offset += 4;
const transformations = new Array<NTokenInstruction>(transformations_length);
for (let i = 0; i < transformations_length; i++) {
const opcode = view.getUint8(offset++);
const op = opcode >> 6;
switch (op) {
case NTokenTransformOpType.FUNC:
{
const is_reverse_base64 = opcode & 0b00000001;
const operation = view.getUint8(offset++);
transformations[i] = [ op, operation, is_reverse_base64 ];
}
break;
case NTokenTransformOpType.N_ARR:
case NTokenTransformOpType.REF:
{
transformations[i] = [ op ];
}
break;
case NTokenTransformOpType.LITERAL:
{
const type = opcode & 0b00000001;
if (type === 0) {
const literal = view.getInt32(offset, true);
offset += 4;
transformations[i] = [ op, literal ];
} else {
const length = view.getUint32(offset, true);
offset += 4;
const literal = new Uint8Array(length);
for (let i = 0; i < length; i++) {
literal[i] = view.getUint8(offset++);
}
transformations[i] = [ op, new TextDecoder().decode(literal) as any ];
}
}
break;
default:
throw new Error('Invalid opcode');
}
}
const function_calls = new Array<NTokenCall>(function_calls_length);
for (let i = 0; i < function_calls_length; i++) {
const index = view.getUint8(offset++);
const num_params = view.getUint8(offset++);
const params = new Array<number>(num_params);
for (let j = 0; j < num_params; j++) {
params[j] = view.getUint8(offset++);
}
function_calls[i] = [ index, params ];
}
return new NToken([ transformations, function_calls ]);
}
static get LIBRARY_VERSION(): number {
return 1;
}
static getFunc(el: string) {
return el.match(NTOKEN_REGEX.FUNCTIONS);
}
static getTransformationData(raw: string) {
const data = `[${raw.replace(/\n/g, '').match(/c=\[(.*?)\];c/s)?.[1]}]`;
return JSON.parse(this.refineNTokenData(data)) as any[];
}
static refineNTokenData(data: string) {
// TODO: refactor this
return data
.replace(/function\(d,e\)/g, '"function(d,e)').replace(/function\(d\)/g, '"function(d)')
.replace(/function\(\)/g, '"function()').replace(/function\(d,e,f\)/g, '"function(d,e,f)')
.replace(/\[function\(d,e,f\)/g, '["function(d,e,f)').replace(/,b,/g, ',"b",')
.replace(/,b/g, ',"b"').replace(/b,/g, '"b",').replace(/b]/g, '"b"]')
.replace(/\[b/g, '["b"').replace(/}]/g, '"]').replace(/},/g, '}",')
.replace(/""/g, '').replace(/length]\)}"/g, 'length])}');
}
}

View File

@@ -1,140 +0,0 @@
import { SIG_REGEX } from '../utils/Constants';
export enum SignatureOperation {
REVERSE,
SPLICE,
SWAP
}
export type SignatureInstruction = [ SignatureOperation, number ];
export default class Signature {
private action_sequence;
constructor(action_sequence: SignatureInstruction[]) {
this.action_sequence = action_sequence;
}
static fromSourceCode(raw_sc: string) {
let func: RegExpExecArray | null;
const functions = [];
while ((func = SIG_REGEX.FUNCTIONS.exec(raw_sc)) !== null) {
if (func[0].includes('reverse')) {
functions[0] = func[1];
} else if (func[0].includes('splice')) {
functions[1] = func[1];
} else {
functions[2] = func[1];
}
}
let actions: RegExpExecArray | null;
const action_sequence: SignatureInstruction[] = [];
while ((actions = SIG_REGEX.ACTIONS.exec(raw_sc)) !== null) {
const action = actions.groups;
if (!action) continue;
switch (action.name) {
case functions[0]:
action_sequence.push([ SignatureOperation.REVERSE, 0 ]);
break;
case functions[1]:
action_sequence.push([ SignatureOperation.SPLICE, parseInt(action.param) ]);
break;
case functions[2]:
action_sequence.push([ SignatureOperation.SWAP, parseInt(action.param) ]);
break;
default:
}
}
return new Signature(action_sequence);
}
decipher(url: string) {
const args = new URLSearchParams(url);
const signature = args.get('s')?.split('');
if (!signature)
throw new TypeError('Invalid input signature');
for (const action of this.action_sequence) {
switch (action[0]) {
case SignatureOperation.REVERSE:
signature.reverse();
break;
case SignatureOperation.SPLICE:
signature.splice(0, action[1]);
break;
case SignatureOperation.SWAP:
const index = action[1];
const orig_arr = signature[0];
signature[0] = signature[index % signature.length];
signature[index % signature.length] = orig_arr;
break;
default:
break;
}
}
return signature.join('');
}
toJSON() {
return [ ...this.action_sequence ];
}
toArrayBuffer() {
// Array buffer encoding assumes that the index of the action is a short (16 bit unsigned)
const buffer = new ArrayBuffer(4 + 4 + this.action_sequence.length * (1 + 2));
const view = new DataView(buffer);
let offset = 0;
view.setUint32(offset, Signature.LIBRARY_VERSION, true);
offset += 4;
view.setUint32(offset, this.action_sequence.length, true);
offset += 4;
for (let i = 0; i < this.action_sequence.length; i++) {
view.setUint8(offset, this.action_sequence[i][0]);
offset += 1;
view.setUint16(offset, this.action_sequence[i][1], true);
offset += 2;
}
return buffer;
}
static fromArrayBuffer(buffer: ArrayBuffer) {
const view = new DataView(buffer);
let offset = 0;
const version = view.getUint32(offset, true);
offset += 4;
if (version !== Signature.LIBRARY_VERSION)
throw new TypeError('Invalid library version');
const action_sequenceLength = view.getUint32(offset, true);
offset += 4;
const action_sequence = new Array<SignatureInstruction>(action_sequenceLength);
for (let i = 0; i < action_sequenceLength; i++) {
action_sequence[i] = [ view.getUint8(offset), view.getUint16(offset + 1, true) ];
offset += 3;
}
return new Signature(action_sequence);
}
static get LIBRARY_VERSION() {
return 1;
}
}

View File

@@ -1,3 +1,5 @@
import Player from '../../../core/Player';
class Format {
itag: string;
mime_type: string;
@@ -71,8 +73,8 @@ class Format {
* Decipher the streaming url of the format.
* @returns Deciphered URL.
*/
decipher(): string {
return this.url;
decipher(player: Player): string {
return player.decipher(this.url, this.signature_cipher, this.cipher);
}
}

View File

@@ -17,6 +17,7 @@ import PlayerOverlay from '../classes/PlayerOverlay';
import ToggleButton from '../classes/ToggleButton';
import CommentsEntryPointHeader from '../classes/comments/CommentsEntryPointHeader';
import ContinuationItem from '../classes/ContinuationItem';
import PlayerMicroformat from '../classes/PlayerMicroformat';
import LiveChat from '../classes/LiveChat';
import LiveChatWrap from './LiveChat';
@@ -42,6 +43,10 @@ export interface FormatOptions {
* File format, use 'any' to download any format
*/
format?: string;
/**
* InnerTube client, can be ANDROID, WEB or YTMUSIC
*/
client?: 'ANDROID' | 'WEB' | 'YTMUSIC'
}
export interface DownloadOptions extends FormatOptions {
@@ -95,8 +100,22 @@ class VideoInfo {
if (info.playability_status?.status === 'ERROR')
throw new InnertubeError('This video is unavailable', info.playability_status);
if (info.microformat && !info.microformat?.is(PlayerMicroformat))
throw new InnertubeError('Invalid microformat', info.microformat);
this.basic_info = { // This type is inferred so no need for an explicit type
...info.video_details,
...{
/**
* Microformat is a bit redundant, so only
* a few things there are interesting to us.
*/
embed: info.microformat?.embed,
channel: info.microformat?.channel,
is_unlisted: info.microformat?.is_unlisted,
is_family_safe: info.microformat?.is_family_safe,
has_ypc_metadata: info.microformat?.has_ypc_metadata
},
like_count: undefined as number | undefined,
is_liked: undefined as boolean | undefined,
is_disliked: undefined as boolean | undefined
@@ -423,7 +442,7 @@ class VideoInfo {
if (!format.index_range || !format.init_range)
throw new InnertubeError('Index and init ranges not available', { format });
const url = new URL(format.decipher());
const url = new URL(format.decipher(this.#player));
url.searchParams.set('cpn', this.#cpn);
set.appendChild(this.#el(document, 'Representation', {
@@ -453,7 +472,7 @@ class VideoInfo {
if (!format.index_range || !format.init_range)
throw new InnertubeError('Index and init ranges not available', { format });
const url = new URL(format.decipher());
const url = new URL(format.decipher(this.#player));
url.searchParams.set('cpn', this.#cpn);
set.appendChild(this.#el(document, 'Representation', {
@@ -498,7 +517,7 @@ class VideoInfo {
};
const format = this.chooseFormat(opts);
const format_url = format.decipher();
const format_url = format.decipher(this.#player);
// If we're not downloading the video in chunks, we just use fetch once.
if (opts.type === 'video+audio' && !options.range) {

View File

@@ -47,7 +47,6 @@ export const CLIENTS = Object.freeze({
});
export const STREAM_HEADERS = Object.freeze({
'accept': '*/*',
// XXX: undici doesnt like this, 'connection': 'keep-alive',
'origin': 'https://www.youtube.com',
'referer': 'https://www.youtube.com',
'DNT': '?1'
@@ -69,41 +68,12 @@ export const ACCOUNT_SETTINGS = Object.freeze({
PLAYLISTS_PRIVACY: 'PRIVACY_DISCOVERABLE_SAVED_PLAYLISTS',
SUBSCRIPTIONS_PRIVACY: 'PRIVACY_DISCOVERABLE_SUBSCRIPTIONS'
});
export const BASE64_DIALECT = Object.freeze({
NORMAL: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''),
REVERSE: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('')
});
export const SIG_REGEX = Object.freeze({
ACTIONS: /;.{2}\.(?<name>.{2})\(.*?,(?<param>.*?)\)/g,
FUNCTIONS: /(?<name>.{2}):function\(.*?\){(.*?)}/g
});
export const NTOKEN_REGEX = Object.freeze({
CALLS: /c\[(.*?)\]\((.+?)\)/g,
PLACEHOLDERS: /c\[(.*?)\]=c/g,
FUNCTIONS: /d\.push\(e\)|d\.reverse\(\)|d\[0\]\)\[0\]\)|f=d\[0];d\[0\]|d\.length;d\.splice\(e,1\)|function\(\){for\(var|function\(d,e,f\){var|function\(d\){for\(var|reverse\(\)\.forEach|unshift\(d\.pop\(\)\)|function\(d,e\){for\(var f/
});
export const FUNCS = Object.freeze({
PUSH: 'd.push(e)',
REVERSE_1: 'd.reverse()',
REVERSE_2: 'function(d){for(var',
SPLICE: 'd.length;d.splice(e,1)',
SWAP0_1: 'd[0])[0])',
SWAP0_2: 'f=d[0];d[0]',
ROTATE_1: 'reverse().forEach',
ROTATE_2: 'unshift(d.pop())',
BASE64_DIA: 'function(){for(var',
TRANSLATE_1: 'function(d,e){for(var f',
TRANSLATE_2: 'function(d,e,f){var'
});
export default {
URLS,
OAUTH,
CLIENTS,
STREAM_HEADERS,
INNERTUBE_HEADERS_BASE,
ACCOUNT_SETTINGS,
BASE64_DIALECT,
SIG_REGEX,
NTOKEN_REGEX,
FUNCS
};
ACCOUNT_SETTINGS
};