mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-07-04 03:51:00 +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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user