@imput/youtubei.js
Version:
A JavaScript client for YouTube's private API, known as InnerTube. Fork of youtubei.js
350 lines • 16.3 kB
JavaScript
var _a, _Session_storeSession, _Session_getSessionData, _Session_buildContext, _Session_getVisitorID;
import { __classPrivateFieldGet } from "tslib";
import OAuth2 from './OAuth2.js';
import { Log, EventEmitter, HTTPClient, LZW, ProtoUtils } from '../utils/index.js';
import * as Constants from '../utils/Constants.js';
import Actions from './Actions.js';
import Player from './Player.js';
import { generateRandomString, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.js';
export const ClientType = {
WEB: "WEB",
MWEB: "MWEB",
KIDS: "WEB_KIDS",
MUSIC: "WEB_REMIX",
IOS: "iOS",
ANDROID: "ANDROID",
ANDROID_MUSIC: "ANDROID_MUSIC",
ANDROID_CREATOR: "ANDROID_CREATOR",
TV: "TVHTML5",
TV_SIMPLY: "TVHTML5_SIMPLY",
TV_EMBEDDED: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
WEB_EMBEDDED: "WEB_EMBEDDED_PLAYER",
WEB_CREATOR: "WEB_CREATOR"
};
const TAG = 'Session';
/**
* Represents an InnerTube session. This holds all the data needed to make requests to YouTube.
*/
class Session extends EventEmitter {
constructor(context, api_key, api_version, account_index, config_data, player, cookie, fetch, cache, po_token) {
super();
this.context = context;
this.api_key = api_key;
this.api_version = api_version;
this.account_index = account_index;
this.config_data = config_data;
this.player = player;
this.cookie = cookie;
this.cache = cache;
this.po_token = po_token;
this.http = new HTTPClient(this, cookie, fetch);
this.actions = new Actions(this);
this.oauth = new OAuth2(this);
this.logged_in = !!cookie;
this.user_agent = context.client.userAgent;
}
on(type, listener) {
super.on(type, listener);
}
once(type, listener) {
super.once(type, listener);
}
static async create(options = {}) {
const { context, api_key, api_version, account_index, config_data } = await _a.getSessionData(options.lang, options.location, options.account_index, options.visitor_data, options.user_agent, options.enable_safety_mode, options.generate_session_locally, options.device_category, options.client_type, options.timezone, options.fetch, options.on_behalf_of_user, options.cache, options.enable_session_cache, options.po_token, options.retrieve_innertube_config);
return new _a(context, api_key, api_version, account_index, config_data, options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch, options.po_token, options.player_id), options.cookie, options.fetch, options.cache, options.po_token);
}
/**
* Retrieves session data from cache.
* @param cache - A valid cache implementation.
* @param session_args - User provided session arguments.
*/
static async fromCache(cache, session_args) {
const buffer = await cache.get('innertube_session_data');
if (!buffer)
return null;
const data = new TextDecoder().decode(buffer.slice(4));
try {
const result = JSON.parse(LZW.decompress(data));
if (session_args.visitor_data) {
result.context.client.visitorData = session_args.visitor_data;
}
if (session_args.lang)
result.context.client.hl = session_args.lang;
if (session_args.location)
result.context.client.gl = session_args.location;
if (session_args.on_behalf_of_user)
result.context.user.onBehalfOfUser = session_args.on_behalf_of_user;
if (session_args.user_agent)
result.context.client.userAgent = session_args.user_agent;
if (session_args.client_name) {
const client = Object.values(Constants.CLIENTS).find((c) => c.NAME === session_args.client_name);
if (client) {
result.context.client.clientName = client.NAME;
result.context.client.clientVersion = client.VERSION;
}
else
Log.warn(TAG, `Unknown client name: ${session_args.client_name}.`);
}
result.context.client.timeZone = session_args.time_zone;
result.context.client.platform = session_args.device_category.toUpperCase();
result.context.user.enableSafetyMode = session_args.enable_safety_mode;
return result;
}
catch (error) {
Log.error(TAG, 'Failed to parse session data from cache.', error);
return null;
}
}
static async getSessionData(lang = '', location = '', account_index = 0, visitor_data = '', user_agent = getRandomUserAgent('desktop'), enable_safety_mode = false, generate_session_locally = false, device_category = 'desktop', client_name = ClientType.WEB, tz = Intl.DateTimeFormat().resolvedOptions().timeZone, fetch = Platform.shim.fetch, on_behalf_of_user, cache, enable_session_cache = true, po_token, retrieve_innertube_config = true) {
const session_args = {
lang,
location,
time_zone: tz,
user_agent,
device_category,
client_name,
enable_safety_mode,
visitor_data,
on_behalf_of_user,
po_token
};
let session_data;
if (cache && enable_session_cache) {
const cached_session_data = await this.fromCache(cache, session_args);
if (cached_session_data) {
Log.info(TAG, 'Found session data in cache.');
session_data = cached_session_data;
}
}
if (!session_data) {
Log.info(TAG, 'Generating session data.');
let api_key = Constants.CLIENTS.WEB.API_KEY;
let api_version = Constants.CLIENTS.WEB.API_VERSION;
let context_data = {
hl: lang || 'en',
gl: location || 'US',
remote_host: '',
user_agent: user_agent,
visitor_data: visitor_data || ProtoUtils.encodeVisitorData(generateRandomString(11), Math.floor(Date.now() / 1000)),
client_name: client_name,
client_version: Object.values(Constants.CLIENTS).find((v) => v.NAME === client_name)?.VERSION ?? Constants.CLIENTS.WEB.VERSION,
device_category: device_category.toUpperCase(),
os_name: 'Windows',
os_version: '10.0',
time_zone: tz,
browser_name: 'Chrome',
browser_version: '125.0.0.0',
device_make: '',
device_model: '',
enable_safety_mode: enable_safety_mode
};
if (!generate_session_locally) {
try {
const sw_session_data = await __classPrivateFieldGet(this, _a, "m", _Session_getSessionData).call(this, session_args, fetch);
api_key = sw_session_data.api_key;
api_version = sw_session_data.api_version;
context_data = sw_session_data.context_data;
}
catch (error) {
Log.error(TAG, 'Failed to retrieve session data from server. Session data generated locally will be used instead.', error);
}
}
if (on_behalf_of_user) {
context_data.on_behalf_of_user = on_behalf_of_user;
}
session_data = {
api_key,
api_version,
context: __classPrivateFieldGet(this, _a, "m", _Session_buildContext).call(this, context_data)
};
if (retrieve_innertube_config) {
try {
Log.info(TAG, 'Retrieving InnerTube config data.');
const config_headers = {
'Accept-Language': lang,
'Accept': '*/*',
'Referer': Constants.URLS.YT_BASE,
'X-Goog-Visitor-Id': context_data.visitor_data,
'X-Origin': Constants.URLS.YT_BASE,
'X-Youtube-Client-Version': context_data.client_version
};
if (Platform.shim.server) {
config_headers['User-Agent'] = user_agent;
config_headers['Origin'] = Constants.URLS.YT_BASE;
}
const config = await fetch(`${Constants.URLS.API.PRODUCTION_1}v1/config?prettyPrint=false`, {
headers: config_headers,
method: 'POST',
body: JSON.stringify({ context: session_data.context })
});
const configJson = await config.json();
const coldConfigData = configJson.responseContext?.globalConfigGroup?.rawColdConfigGroup?.configData;
const coldHashData = configJson.responseContext?.globalConfigGroup?.coldHashData;
const hotHashData = configJson.responseContext?.globalConfigGroup?.hotHashData;
session_data.config_data = configJson.configData;
session_data.context.client.configInfo = {
...session_data.context.client.configInfo,
coldConfigData,
coldHashData,
hotHashData
};
}
catch (error) {
Log.error(TAG, 'Failed to retrieve config data.', error);
}
}
if (enable_session_cache)
await __classPrivateFieldGet(this, _a, "m", _Session_storeSession).call(this, session_data, cache);
}
Log.debug(TAG, 'Session data:', session_data);
return { ...session_data, account_index };
}
async signIn(credentials) {
return new Promise(async (resolve, reject) => {
const error_handler = (err) => reject(err);
this.once('auth-error', error_handler);
this.once('auth', () => {
this.off('auth-error', error_handler);
this.logged_in = true;
resolve();
});
try {
await this.oauth.init(credentials);
}
catch (err) {
reject(err);
}
});
}
/**
* Signs out of the current account and revokes the credentials.
*/
async signOut() {
if (!this.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const response = await this.oauth.revokeCredentials();
this.logged_in = false;
return response;
}
get client_version() {
return this.context.client.clientVersion;
}
get client_name() {
return this.context.client.clientName;
}
get lang() {
return this.context.client.hl;
}
}
_a = Session, _Session_storeSession = async function _Session_storeSession(session_data, cache) {
if (!cache)
return;
Log.info(TAG, 'Compressing and caching session data.');
const compressed_session_data = new TextEncoder().encode(LZW.compress(JSON.stringify(session_data)));
const buffer = new ArrayBuffer(4 + compressed_session_data.byteLength);
new DataView(buffer).setUint32(0, compressed_session_data.byteLength, true); // (Luan) XX: Leave this here for debugging purposes
new Uint8Array(buffer).set(compressed_session_data, 4);
await cache.set('innertube_session_data', new Uint8Array(buffer));
}, _Session_getSessionData = async function _Session_getSessionData(options, fetch = Platform.shim.fetch) {
let visitor_id = generateRandomString(11);
if (options.visitor_data)
visitor_id = __classPrivateFieldGet(this, _a, "m", _Session_getVisitorID).call(this, options.visitor_data);
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
const res = await fetch(url, {
headers: {
'Accept-Language': options.lang || 'en-US',
'User-Agent': options.user_agent,
'Accept': '*/*',
'Referer': `${Constants.URLS.YT_BASE}/sw.js`,
'Cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${visitor_id};`
}
});
if (!res.ok)
throw new SessionError(`Failed to retrieve session data: ${res.status}`);
const text = await res.text();
if (!text.startsWith(')]}\''))
throw new SessionError('Invalid JSPB response');
const data = JSON.parse(text.replace(/^\)\]\}'/, ''));
const ytcfg = data[0][2];
const api_version = Constants.CLIENTS.WEB.API_VERSION;
const [[device_info], api_key] = ytcfg;
const config_info = device_info[61];
const app_install_data = config_info[config_info.length - 1];
const context_info = {
hl: options.lang || device_info[0],
gl: options.location || device_info[1],
remote_host: device_info[3],
visitor_data: options.visitor_data || device_info[13],
user_agent: options.user_agent,
client_name: options.client_name,
client_version: options.client_name === 'WEB' ? device_info[16] : Object.values(Constants.CLIENTS).find((c) => c.NAME === options.client_name)?.VERSION || device_info[16],
os_name: device_info[17],
os_version: device_info[18],
time_zone: device_info[79] || options.time_zone,
device_category: options.device_category,
browser_name: device_info[86],
browser_version: device_info[87],
device_make: device_info[11],
device_model: device_info[12],
app_install_data: app_install_data,
device_experiment_id: device_info[103],
rollout_token: device_info[107],
enable_safety_mode: options.enable_safety_mode
};
return { context_data: context_info, api_key, api_version };
}, _Session_buildContext = function _Session_buildContext(args) {
const context = {
client: {
hl: args.hl || 'en',
gl: args.gl || 'US',
remoteHost: args.remote_host,
screenDensityFloat: 1,
screenHeightPoints: 1440,
screenPixelDensity: 1,
screenWidthPoints: 2560,
visitorData: args.visitor_data,
clientName: args.client_name,
clientVersion: args.client_version,
osName: args.os_name,
osVersion: args.os_version,
userAgent: args.user_agent,
platform: args.device_category.toUpperCase(),
clientFormFactor: 'UNKNOWN_FORM_FACTOR',
userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',
timeZone: args.time_zone,
originalUrl: Constants.URLS.YT_BASE,
deviceMake: args.device_make,
deviceModel: args.device_model,
browserName: args.browser_name,
browserVersion: args.browser_version,
utcOffsetMinutes: -Math.floor((new Date()).getTimezoneOffset()),
memoryTotalKbytes: '8000000',
rolloutToken: args.rollout_token,
deviceExperimentId: args.device_experiment_id,
mainAppWebInfo: {
graftUrl: Constants.URLS.YT_BASE,
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
isWebNativeShareAvailable: true
}
},
user: {
enableSafetyMode: args.enable_safety_mode,
lockedSafetyMode: false
},
request: {
useSsl: true,
internalExperimentFlags: []
}
};
if (args.app_install_data)
context.client.configInfo = { appInstallData: args.app_install_data };
if (args.on_behalf_of_user)
context.user.onBehalfOfUser = args.on_behalf_of_user;
return context;
}, _Session_getVisitorID = function _Session_getVisitorID(visitor_data) {
const decoded_visitor_data = ProtoUtils.decodeVisitorData(visitor_data);
return decoded_visitor_data.id;
};
export default Session;
//# sourceMappingURL=Session.js.map