mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-25 15:52:13 +00:00
chore: tidy things up
Move a few things here and there. Organization makes life easier.
This commit is contained in:
@@ -1,27 +1,34 @@
|
||||
import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction';
|
||||
import Parser, { ParsedResponse, ReloadContinuationItemsCommand } from '../parser/index';
|
||||
import { Memo, ObservedArray } from '../parser/helpers';
|
||||
import { InnertubeError } from '../utils/Utils';
|
||||
import Actions from './Actions';
|
||||
|
||||
import Post from '../parser/classes/Post';
|
||||
import BackstagePost from '../parser/classes/BackstagePost';
|
||||
|
||||
import Channel from '../parser/classes/Channel';
|
||||
import CompactVideo from '../parser/classes/CompactVideo';
|
||||
import ContinuationItem from '../parser/classes/ContinuationItem';
|
||||
|
||||
import GridChannel from '../parser/classes/GridChannel';
|
||||
import GridPlaylist from '../parser/classes/GridPlaylist';
|
||||
import GridVideo from '../parser/classes/GridVideo';
|
||||
|
||||
import Playlist from '../parser/classes/Playlist';
|
||||
import PlaylistPanelVideo from '../parser/classes/PlaylistPanelVideo';
|
||||
import PlaylistVideo from '../parser/classes/PlaylistVideo';
|
||||
import Post from '../parser/classes/Post';
|
||||
|
||||
import Tab from '../parser/classes/Tab';
|
||||
import ReelShelf from '../parser/classes/ReelShelf';
|
||||
import RichShelf from '../parser/classes/RichShelf';
|
||||
import Shelf from '../parser/classes/Shelf';
|
||||
import Tab from '../parser/classes/Tab';
|
||||
|
||||
import TwoColumnBrowseResults from '../parser/classes/TwoColumnBrowseResults';
|
||||
import TwoColumnSearchResults from '../parser/classes/TwoColumnSearchResults';
|
||||
import Video from '../parser/classes/Video';
|
||||
import WatchCardCompactVideo from '../parser/classes/WatchCardCompactVideo';
|
||||
import { Memo, ObservedArray } from '../parser/helpers';
|
||||
import Parser, { ParsedResponse, ReloadContinuationItemsCommand } from '../parser/index';
|
||||
import { InnertubeError } from '../utils/Utils';
|
||||
import Actions from './Actions';
|
||||
import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction';
|
||||
import ContinuationItem from '../parser/classes/ContinuationItem';
|
||||
|
||||
import Video from '../parser/classes/Video';
|
||||
|
||||
// TODO: add a way subdivide into sections and return subfeeds?
|
||||
class Feed {
|
||||
@@ -37,6 +44,7 @@ class Feed {
|
||||
this.#page = Parser.parseResponse(data);
|
||||
}
|
||||
|
||||
// Xxx: this can be extremely confusing — maybe refactor?
|
||||
const memo =
|
||||
this.#page.on_response_received_commands ?
|
||||
this.#page.on_response_received_commands_memo :
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Session from './Session';
|
||||
import Constants from '../utils/Constants';
|
||||
import { OAuthError, uuidv4 } from '../utils/Utils';
|
||||
import Session from './Session';
|
||||
|
||||
export interface Credentials {
|
||||
/**
|
||||
@@ -61,10 +61,6 @@ class OAuth {
|
||||
await this.#session.cache?.set('youtubei_oauth_credentials', data.buffer);
|
||||
}
|
||||
|
||||
async removeCache() {
|
||||
await this.#session.cache?.remove('youtubei_oauth_credentials');
|
||||
}
|
||||
|
||||
async #loadCachedCredentials() {
|
||||
const data = await this.#session.cache?.get('youtubei_oauth_credentials');
|
||||
if (!data) return false;
|
||||
@@ -86,6 +82,10 @@ class OAuth {
|
||||
return true;
|
||||
}
|
||||
|
||||
async removeCache() {
|
||||
await this.#session.cache?.remove('youtubei_oauth_credentials');
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the server for a user code and verification URL.
|
||||
*/
|
||||
@@ -266,4 +266,5 @@ class OAuth {
|
||||
Reflect.has(this.#credentials, 'expires') || false;
|
||||
}
|
||||
}
|
||||
export default OAuth;
|
||||
|
||||
export default OAuth;
|
||||
@@ -1,9 +1,11 @@
|
||||
import { getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils';
|
||||
import Constants from '../utils/Constants';
|
||||
import Signature from '../deciphers/Signature';
|
||||
import NToken from '../deciphers/NToken';
|
||||
import UniversalCache from '../utils/Cache';
|
||||
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';
|
||||
|
||||
export default class Player {
|
||||
#ntoken;
|
||||
@@ -18,87 +20,6 @@ export default class Player {
|
||||
this.#player_id = player_id;
|
||||
}
|
||||
|
||||
static async fromCache(cache: UniversalCache, player_id: string) {
|
||||
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_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);
|
||||
|
||||
return new Player(Signature.fromArrayBuffer(sig_decipher_buf), NToken.fromArrayBuffer(ntoken_transform_buf), sig_timestamp, player_id);
|
||||
}
|
||||
|
||||
static async fromSource(cache: UniversalCache | undefined, sig_timestamp: number, sig_decipher_sc: string, ntoken_sc: string, player_id: string) {
|
||||
const player = new Player(Signature.fromSourceCode(sig_decipher_sc), NToken.fromSourceCode(ntoken_sc), sig_timestamp, player_id);
|
||||
await player.cache(cache);
|
||||
return player;
|
||||
}
|
||||
|
||||
async cache(cache?: UniversalCache) {
|
||||
if (!cache) return;
|
||||
|
||||
const ntokenBuf = this.#ntoken.toArrayBuffer();
|
||||
const sigDecipherBuf = this.#signature.toArrayBuffer();
|
||||
const buffer = new ArrayBuffer(12 + sigDecipherBuf.byteLength + ntokenBuf.byteLength);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
view.setUint32(0, Player.LIBRARY_VERSION, true);
|
||||
view.setUint32(4, this.#signature_timestamp, true);
|
||||
view.setUint32(8, sigDecipherBuf.byteLength, true);
|
||||
|
||||
new Uint8Array(buffer).set(new Uint8Array(sigDecipherBuf), 12);
|
||||
new Uint8Array(buffer).set(new Uint8Array(ntokenBuf), 12 + sigDecipherBuf.byteLength);
|
||||
|
||||
await cache.set(this.#player_id, new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: 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);
|
||||
|
||||
url_components.searchParams.set('ratebypass', 'yes');
|
||||
|
||||
if (signature_cipher || cipher) {
|
||||
const signature = this.#signature.decipher(url);
|
||||
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 ntoken = this.#ntoken.transform(n);
|
||||
url_components.searchParams.set('n', ntoken);
|
||||
}
|
||||
|
||||
return url_components.toString();
|
||||
}
|
||||
|
||||
get url() {
|
||||
return new URL(`/s/player/${this.#player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString();
|
||||
}
|
||||
|
||||
get sts() {
|
||||
return this.#signature_timestamp;
|
||||
}
|
||||
|
||||
static async create(cache: UniversalCache | undefined, fetch: FetchFunction = globalThis.fetch) {
|
||||
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
|
||||
const res = await fetch(url);
|
||||
@@ -141,6 +62,80 @@ export default class Player {
|
||||
return await Player.fromSource(cache, sig_timestamp, sig_decipher_sc, ntoken_sc, player_id);
|
||||
}
|
||||
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: 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);
|
||||
|
||||
url_components.searchParams.set('ratebypass', 'yes');
|
||||
|
||||
if (signature_cipher || cipher) {
|
||||
const signature = this.#signature.decipher(url);
|
||||
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 ntoken = this.#ntoken.transform(n);
|
||||
url_components.searchParams.set('n', ntoken);
|
||||
}
|
||||
|
||||
return url_components.toString();
|
||||
}
|
||||
|
||||
static async fromCache(cache: UniversalCache, player_id: string) {
|
||||
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_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);
|
||||
|
||||
return new Player(Signature.fromArrayBuffer(sig_decipher_buf), NToken.fromArrayBuffer(ntoken_transform_buf), sig_timestamp, player_id);
|
||||
}
|
||||
|
||||
static async fromSource(cache: UniversalCache | undefined, sig_timestamp: number, sig_decipher_sc: string, ntoken_sc: string, player_id: string) {
|
||||
const player = new Player(Signature.fromSourceCode(sig_decipher_sc), NToken.fromSourceCode(ntoken_sc), sig_timestamp, player_id);
|
||||
await player.cache(cache);
|
||||
return player;
|
||||
}
|
||||
|
||||
async cache(cache?: UniversalCache) {
|
||||
if (!cache) return;
|
||||
|
||||
const ntoken_buf = this.#ntoken.toArrayBuffer();
|
||||
const sig_decipher_buf = this.#signature.toArrayBuffer();
|
||||
const buffer = new ArrayBuffer(12 + sig_decipher_buf.byteLength + ntoken_buf.byteLength);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
view.setUint32(0, Player.LIBRARY_VERSION, true);
|
||||
view.setUint32(4, this.#signature_timestamp, 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);
|
||||
|
||||
await cache.set(this.#player_id, new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the signature timestamp from the player source code.
|
||||
*/
|
||||
@@ -173,6 +168,14 @@ export default class Player {
|
||||
return sc;
|
||||
}
|
||||
|
||||
get url() {
|
||||
return new URL(`/s/player/${this.#player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString();
|
||||
}
|
||||
|
||||
get sts() {
|
||||
return this.#signature_timestamp;
|
||||
}
|
||||
|
||||
static get LIBRARY_VERSION() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import HTTPClient, { FetchFunction } from '../utils/HTTPClient';
|
||||
import { DeviceCategory, generateRandomString, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth';
|
||||
|
||||
export enum ClientType {
|
||||
WEB = 'WEB',
|
||||
MUSIC = 'WEB_REMIX',
|
||||
ANDROID = 'ANDROID',
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
client: {
|
||||
hl: string;
|
||||
@@ -39,12 +45,6 @@ export interface Context {
|
||||
};
|
||||
}
|
||||
|
||||
export enum ClientType {
|
||||
WEB = 'WEB',
|
||||
MUSIC = 'WEB_REMIX',
|
||||
ANDROID = 'ANDROID',
|
||||
}
|
||||
|
||||
export interface SessionOptions {
|
||||
lang?: string;
|
||||
device_category?: DeviceCategory;
|
||||
@@ -60,6 +60,7 @@ export default class Session extends EventEmitterLike {
|
||||
#key;
|
||||
#context;
|
||||
#player;
|
||||
|
||||
oauth;
|
||||
http;
|
||||
logged_in;
|
||||
@@ -96,49 +97,6 @@ export default class Session extends EventEmitterLike {
|
||||
super.once(type, listener);
|
||||
}
|
||||
|
||||
async signIn(credentials?: Credentials): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const error_handler: OAuthAuthErrorEventHandler = (err) => {
|
||||
reject(err);
|
||||
};
|
||||
|
||||
this.once('auth', (data) => {
|
||||
this.off('auth-error', error_handler);
|
||||
|
||||
if (data.status === 'SUCCESS') {
|
||||
this.logged_in = true;
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject(data);
|
||||
});
|
||||
|
||||
this.once('auth-error', error_handler);
|
||||
|
||||
try {
|
||||
await this.oauth.init(credentials);
|
||||
|
||||
if (this.oauth.validateCredentials()) {
|
||||
await this.oauth.refreshIfRequired();
|
||||
this.logged_in = true;
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
if (!this.logged_in)
|
||||
throw new InnertubeError('You are not signed in');
|
||||
|
||||
const response = await this.oauth.revokeCredentials();
|
||||
this.logged_in = false;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async create(options: SessionOptions = {}) {
|
||||
const { context, api_key, api_version } = await Session.getSessionData(options.lang, options.device_category, options.client_type, options.timezone, options.fetch);
|
||||
return new Session(context, api_key, api_version, await Player.create(options.cache, options.fetch), options.cookie, options.fetch, options.cache);
|
||||
@@ -146,8 +104,8 @@ export default class Session extends EventEmitterLike {
|
||||
|
||||
static async getSessionData(
|
||||
lang = 'en-US',
|
||||
deviceCategory: DeviceCategory = 'desktop',
|
||||
clientName: ClientType = ClientType.WEB,
|
||||
device_category: DeviceCategory = 'desktop',
|
||||
client_name: ClientType = ClientType.WEB,
|
||||
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
fetch: FetchFunction = globalThis.fetch
|
||||
) {
|
||||
@@ -187,11 +145,11 @@ export default class Session extends EventEmitterLike {
|
||||
remoteHost: device_info[3],
|
||||
visitorData: visitor_data,
|
||||
userAgent: device_info[14],
|
||||
clientName,
|
||||
clientName: client_name,
|
||||
clientVersion: device_info[16],
|
||||
osName: device_info[17],
|
||||
osVersion: device_info[18],
|
||||
platform: deviceCategory.toUpperCase(),
|
||||
platform: device_category.toUpperCase(),
|
||||
clientFormFactor: 'UNKNOWN_FORM_FACTOR',
|
||||
userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',
|
||||
timeZone: device_info[79],
|
||||
@@ -210,11 +168,48 @@ export default class Session extends EventEmitterLike {
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
context,
|
||||
api_key,
|
||||
api_version
|
||||
};
|
||||
return { context, api_key, api_version };
|
||||
}
|
||||
|
||||
async signIn(credentials?: Credentials): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const error_handler: OAuthAuthErrorEventHandler = (err) => reject(err);
|
||||
|
||||
this.once('auth', (data) => {
|
||||
this.off('auth-error', error_handler);
|
||||
|
||||
if (data.status === 'SUCCESS') {
|
||||
this.logged_in = true;
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject(data);
|
||||
});
|
||||
|
||||
this.once('auth-error', error_handler);
|
||||
|
||||
try {
|
||||
await this.oauth.init(credentials);
|
||||
|
||||
if (this.oauth.validateCredentials()) {
|
||||
await this.oauth.refreshIfRequired();
|
||||
this.logged_in = true;
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
if (!this.logged_in)
|
||||
throw new InnertubeError('You are not signed in');
|
||||
|
||||
const response = await this.oauth.revokeCredentials();
|
||||
this.logged_in = false;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
get key() {
|
||||
|
||||
@@ -27,6 +27,7 @@ class TabbedFeed extends Feed {
|
||||
return this;
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Failed to call endpoint');
|
||||
|
||||
|
||||
@@ -2,25 +2,25 @@ 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,
|
||||
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
|
||||
FUNC,
|
||||
N_ARR,
|
||||
LITERAL,
|
||||
REF
|
||||
}
|
||||
|
||||
const OP_LOOKUP: Record<string, NTokenTransformOperation> = {
|
||||
@@ -96,39 +96,39 @@ export class NTokenTransforms {
|
||||
}
|
||||
|
||||
static push(arr: any[], item: any) {
|
||||
if (Array.isArray(arr?.[0])) arr.push([ NTokenTransformOpType.LITERAL, item ]);
|
||||
else arr.push(item);
|
||||
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
|
||||
}
|
||||
];
|
||||
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?];
|
||||
@@ -136,16 +136,22 @@ 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) => {
|
||||
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') {
|
||||
@@ -156,6 +162,7 @@ export default class NToken {
|
||||
} else if (el != null) {
|
||||
el = [ NTokenTransformOpType.LITERAL, el ];
|
||||
}
|
||||
|
||||
return el;
|
||||
});
|
||||
|
||||
@@ -165,22 +172,23 @@ export default class NToken {
|
||||
|
||||
// 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
|
||||
|
||||
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 ]);
|
||||
@@ -220,6 +228,7 @@ export default class NToken {
|
||||
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('');
|
||||
}
|
||||
|
||||
@@ -241,6 +250,7 @@ export default class NToken {
|
||||
// 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:
|
||||
@@ -258,6 +268,7 @@ export default class NToken {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const call of this.transformer[1]) {
|
||||
size += 2 + call[1].length;
|
||||
}
|
||||
@@ -266,21 +277,28 @@ export default class NToken {
|
||||
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;
|
||||
}
|
||||
@@ -297,17 +315,23 @@ export default class NToken {
|
||||
{
|
||||
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;
|
||||
@@ -317,11 +341,14 @@ export default class NToken {
|
||||
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;
|
||||
@@ -334,19 +361,25 @@ export default class NToken {
|
||||
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:
|
||||
{
|
||||
@@ -355,17 +388,16 @@ export default class NToken {
|
||||
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;
|
||||
@@ -373,27 +405,34 @@ export default class NToken {
|
||||
} 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 ];
|
||||
}
|
||||
|
||||
@@ -423,4 +462,4 @@ export default class NToken {
|
||||
.replace(/\[b/g, '["b"').replace(/}]/g, '"]').replace(/},/g, '}",')
|
||||
.replace(/""/g, '').replace(/length]\)}"/g, 'length])}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
import { SIG_REGEX } from '../utils/Constants';
|
||||
|
||||
export enum SignatureOperation {
|
||||
REVERSE,
|
||||
SPLICE,
|
||||
SWAP
|
||||
REVERSE,
|
||||
SPLICE,
|
||||
SWAP
|
||||
}
|
||||
|
||||
export type SignatureInstruction = [SignatureOperation, number];
|
||||
export type SignatureInstruction = [ SignatureOperation, number ];
|
||||
|
||||
export default class Signature {
|
||||
private actionSequence;
|
||||
private action_sequence;
|
||||
|
||||
constructor(actionSequence: SignatureInstruction[]) {
|
||||
this.actionSequence = actionSequence;
|
||||
constructor(action_sequence: SignatureInstruction[]) {
|
||||
this.action_sequence = action_sequence;
|
||||
}
|
||||
|
||||
static fromSourceCode(sigDecipherSc: string) {
|
||||
static fromSourceCode(raw_sc: string) {
|
||||
let func: RegExpExecArray | null;
|
||||
const functions = [];
|
||||
|
||||
while ((func = SIG_REGEX.FUNCTIONS.exec(sigDecipherSc)) !== null) {
|
||||
while ((func = SIG_REGEX.FUNCTIONS.exec(raw_sc)) !== null) {
|
||||
if (func[0].includes('reverse')) {
|
||||
functions[0] = func[1];
|
||||
} else if (func[0].includes('splice')) {
|
||||
@@ -31,51 +31,49 @@ export default class Signature {
|
||||
|
||||
let actions: RegExpExecArray | null;
|
||||
|
||||
const actionSequence: SignatureInstruction[] = [];
|
||||
const action_sequence: SignatureInstruction[] = [];
|
||||
|
||||
while ((actions = SIG_REGEX.ACTIONS.exec(sigDecipherSc)) !== null) {
|
||||
while ((actions = SIG_REGEX.ACTIONS.exec(raw_sc)) !== null) {
|
||||
const action = actions.groups;
|
||||
if (!action) continue;
|
||||
switch (action.name) {
|
||||
case functions[0]:
|
||||
actionSequence.push([ SignatureOperation.REVERSE, 0 ]);
|
||||
action_sequence.push([ SignatureOperation.REVERSE, 0 ]);
|
||||
break;
|
||||
case functions[1]:
|
||||
actionSequence.push([ SignatureOperation.SPLICE, parseInt(action.param) ]);
|
||||
action_sequence.push([ SignatureOperation.SPLICE, parseInt(action.param) ]);
|
||||
break;
|
||||
case functions[2]:
|
||||
actionSequence.push([ SignatureOperation.SWAP, parseInt(action.param) ]);
|
||||
action_sequence.push([ SignatureOperation.SWAP, parseInt(action.param) ]);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return new Signature(actionSequence);
|
||||
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.actionSequence) {
|
||||
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 origArrI = signature[0];
|
||||
const orig_arr = signature[0];
|
||||
signature[0] = signature[index % signature.length];
|
||||
signature[index % signature.length] = origArrI;
|
||||
signature[index % signature.length] = orig_arr;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -85,43 +83,55 @@ export default class Signature {
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return [ ...this.actionSequence ];
|
||||
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.actionSequence.length * (1 + 2));
|
||||
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.actionSequence.length, true);
|
||||
|
||||
view.setUint32(offset, this.action_sequence.length, true);
|
||||
offset += 4;
|
||||
for (let i = 0; i < this.actionSequence.length; i++) {
|
||||
view.setUint8(offset, this.actionSequence[i][0]);
|
||||
|
||||
for (let i = 0; i < this.action_sequence.length; i++) {
|
||||
view.setUint8(offset, this.action_sequence[i][0]);
|
||||
offset += 1;
|
||||
view.setUint16(offset, this.actionSequence[i][1], true);
|
||||
|
||||
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 actionSequenceLength = view.getUint32(offset, true);
|
||||
const action_sequenceLength = view.getUint32(offset, true);
|
||||
offset += 4;
|
||||
const actionSequence = new Array<SignatureInstruction>(actionSequenceLength);
|
||||
for (let i = 0; i < actionSequenceLength; i++) {
|
||||
actionSequence[i] = [ view.getUint8(offset), view.getUint16(offset + 1, true) ];
|
||||
|
||||
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(actionSequence);
|
||||
|
||||
return new Signature(action_sequence);
|
||||
}
|
||||
|
||||
static get LIBRARY_VERSION() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
// @generated by protobuf-ts 2.7.0
|
||||
// @generated from protobuf file "youtube.proto" (package "youtube", syntax proto2)
|
||||
// tslint:disable
|
||||
|
||||
Reference in New Issue
Block a user