Files
YouTube.js/lib/deciphers/NToken.ts
Daniel Wykerd fb68e6bcfe feat!: better cross runtime support (#97)
* refactor: remove dependancies

removes node-forge and uuid in favor of Web APIs

* refactor!: commonjs to es6

To aid with #93 I will make all my changes in TypeScript instead.
This is the first step into making that happen.

Used: https://github.com/wessberg/cjstoesm

* refactor!: NToken and Signature TS files

Bring this PR up to speed with #93

* feat: cross platform cache (WIP)

this is untested!
should remove idb as dependecy.

* feat: EventEmitter polyfill

* refactor: remove events

* feat: HTTPClient based on Fetch API (WIP)

* refactor!: parsers refactor (WIP)

Initial TS support for parsers as per #93

This adds several type safety checks to the parser which'll help to
ensure valid data is returned by the parser.

* refactor!: parsers refactor (WIP)

Bring more in line with the existing implementations & make less verbose

* refactor!: parser refactor

I was overcomplicating things, this is much simpler and compatible with
the existing JS API

* fix: some missed parsers while refactoring

* fix: better type inferance for parseResponse

* feat(TS): typesafe YTNode casts

* feat: more type safety in YTNode and Parser

* refactor: VideoInfo download with fetch & TS (WIP)

Again, this also does some work for #93

* fix: LiveChat in VideoInfo

* refactor!: more typesafety in parser

* refactor!: VideoInfo almost completed

* refactor!: player and session refactors

- Remove the Player class' dependance on Session.
- Add additional context to the Session.

* refactor!: move auth logic to Session (WIP)

* refactor: TS port for Actions and Innertube

My fingers hurt from typing out all those types :-P

* refactor: NavigationEndpoint TS

this is still a WIP and should be improved.
NavigationEndpoint should probably be refactored further.

* refactor!: VideoInfo compiles without errors

* chore: delete old player

* fix: import errors

It compiles and runs!!

* fix: Utils import fixes

* fix: several runtime errors

* fix: video streaming

* chore: remove console.log debugging

Whoops, forgot to remove these before I pushed the previous commit

* chore: remove old unused dependencies

* fix: typescript errors

Now emitting declarations and source maps

* refactor: TS feed

* chore: delete old Feed

* refactor: move streamToIterable into Utils

* refactor: AccountManager TS

* refactor: FilterableFeed to TS

* refactor: InteractionManager to TS

* refactor: PlaylistManager to TS

* refactor: TabbedFeed to TS

* refactor: Music to TS (WIP)

more work to be done, see TODO comments

* fix: getting the tests to pass (6/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1152 ms)
      ✕ Should search on YouTube Music (705 ms)
      ✕ Should retrieve YouTube search suggestions (722 ms)
      ✓ Should retrieve YouTube Music search suggestions (233 ms)
    Comments
      ✓ Should retrieve comments (585 ms)
      ✕ Should retrieve next batch of comments (221 ms)
      ✕ Should retrieve comment replies (1 ms)
    General
      ✕ Should retrieve playlist with YouTube (732 ms)
      ✓ Should retrieve home feed (838 ms)
      ✓ Should retrieve trending content (543 ms)
      ✓ Should retrieve video info (639 ms)
      ✕ Should download video (5 ms)

* fix: tests (7/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1984 ms)
      ✕ Should search on YouTube Music (1139 ms)
      ✕ Should retrieve YouTube search suggestions (1433 ms)
      ✓ Should retrieve YouTube Music search suggestions (529 ms)
    Comments
      ✓ Should retrieve comments (324 ms)
      ✓ Should retrieve next batch of comments (395 ms)
      ✕ Should retrieve comment replies
    General
      ✕ Should retrieve playlist with YouTube (653 ms)
      ✓ Should retrieve home feed (1085 ms)
      ✓ Should retrieve trending content (513 ms)
      ✓ Should retrieve video info (921 ms)
      ✕ Should download video (3 ms)

* fix: download tests (8/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1293 ms)
      ✕ Should search on YouTube Music (927 ms)
      ✕ Should retrieve YouTube search suggestions (1250 ms)
      ✓ Should retrieve YouTube Music search suggestions (258 ms)
    Comments
      ✓ Should retrieve comments (803 ms)
      ✓ Should retrieve next batch of comments (511 ms)
      ✕ Should retrieve comment replies
    General
      ✕ Should retrieve playlist with YouTube (528 ms)
      ✓ Should retrieve home feed (1047 ms)
      ✓ Should retrieve trending content (548 ms)
      ✓ Should retrieve video info (825 ms)
      ✓ Should download video (1779 ms)

* fix: tests (9/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1276 ms)
      ✕ Should search on YouTube Music (955 ms)
      ✓ Should retrieve YouTube search suggestions (661 ms)
      ✓ Should retrieve YouTube Music search suggestions (491 ms)
    Comments
      ✓ Should retrieve comments (624 ms)
      ✓ Should retrieve next batch of comments (353 ms)
      ✕ Should retrieve comment replies
    General
      ✕ Should retrieve playlist with YouTube (672 ms)
      ✓ Should retrieve home feed (1277 ms)
      ✓ Should retrieve trending content (999 ms)
      ✓ Should retrieve video info (1106 ms)
      ✓ Should download video (2514 ms)

* feat: key based type validation for parsers

* fix: comments tests pass (10/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (938 ms)
      ✕ Should search on YouTube Music (850 ms)
      ✓ Should retrieve YouTube search suggestions (528 ms)
      ✓ Should retrieve YouTube Music search suggestions (224 ms)
    Comments
      ✓ Should retrieve comments (518 ms)
      ✓ Should retrieve next batch of comments (337 ms)
      ✓ Should retrieve comment replies (358 ms)
    General
      ✕ Should retrieve playlist with YouTube (466 ms)
      ✓ Should retrieve home feed (1051 ms)
      ✓ Should retrieve trending content (623 ms)
      ✓ Should retrieve video info (863 ms)
      ✓ Should download video (2656 ms)

* refactor: type safety checks removing @ts-ignore

* fix: playlist tests pass (11/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (991 ms)
      ✕ Should search on YouTube Music (924 ms)
      ✓ Should retrieve YouTube search suggestions (606 ms)
      ✓ Should retrieve YouTube Music search suggestions (225 ms)
    Comments
      ✓ Should retrieve comments (393 ms)
      ✓ Should retrieve next batch of comments (284 ms)
      ✓ Should retrieve comment replies (252 ms)
    General
      ✓ Should retrieve playlist with YouTube (578 ms)
      ✓ Should retrieve home feed (1148 ms)
      ✓ Should retrieve trending content (541 ms)
      ✓ Should retrieve video info (799 ms)
      ✓ Should download video (1419 ms)

* fix: all tests pass for node 🎉

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1053 ms)
      ✓ Should search on YouTube Music (761 ms)
      ✓ Should retrieve YouTube search suggestions (453 ms)
      ✓ Should retrieve YouTube Music search suggestions (221 ms)
    Comments
      ✓ Should retrieve comments (627 ms)
      ✓ Should retrieve next batch of comments (412 ms)
      ✓ Should retrieve comment replies (268 ms)
    General
      ✓ Should retrieve playlist with YouTube (565 ms)
      ✓ Should retrieve home feed (775 ms)
      ✓ Should retrieve trending content (498 ms)
      ✓ Should retrieve video info (875 ms)
      ✓ Should download video (1364 ms)

* build: working Deno bundle

Still need to test whether this bundle works in the browser

* docs: update deno example to download video

* refactor: MusicResponsiveListItem to TS

* docs: TSDoc for Parser helpers

* docs: Parser documentation for TS

* docs: add note about parseItem and parseArray

* test: remove browser tests since they're identical

* feat: browser support and proxy example

* fix: PlaylistManager TS after merge

* feat: in-browser video streaming

* refactor: cleanup the Dash example

* feat: allow custom fetch implementations

* feat: fetch debugger

* fix: OAuth login

* refactor: remove file extensions from imports

* refactor: build scripts

* fix: CustomEvent on node

* fix: LiveChat

* fix: linting

* fix: liniting in build-parser-json

* chore: update test workflow

* fix: NToken errors after lint fixes

* fix: codacy complaints

* docs: update to reflect changes

Definitly needs more work but its a start

* refactor: cleanup imports/exports

* fix: browser example

- Remove user-agent before making request.
- Fix cache on browsers

* fix: cache on node

* fix: stupid mistake

* refactor: Session#signIn to wait untill success

This also splits the 'auth' event up into 3 distinct events:
- 'auth' -> fired on success
- 'auth-pending' -> fired when pending authentication
- 'auth-error' -> fired when an error occurred

* refactor: freeze Constants

* refactor: cleanup HTTPClient Request

* refactor: debugFetch readability

* chore: lint

* refactor: replace jsdoc with tsdoc eslint plugin

remove @param annotations without descriptions

* fix: bunch of liniting warnings

* refactor: better inference on YTNode#is

As suggested by @MasterOfBob777

* fix: linting warnings

* revert: undici import

* refactor: rename `list_type` to `item_type`
2022-07-20 14:06:12 -03:00

427 lines
15 KiB
TypeScript

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 transformationData = NToken.getTransformationData(raw);
const transformations = transformationData.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])}');
}
}