chore: tidy things up

Move a few things here and there. Organization makes life easier.
This commit is contained in:
LuanRT
2022-08-03 03:34:59 -03:00
parent 3cdaab8b7a
commit af6856ced4
8 changed files with 313 additions and 257 deletions

View File

@@ -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 :

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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');