refactor: generate sessions manually

Session generation has been moved to `core/SessionBuilder.js`, which retrieves & generates all the required data to create a valid session. This should also decrease initialization time by over 600 milliseconds!
This commit is contained in:
LuanRT
2022-05-05 04:33:24 -03:00
parent 4943685e57
commit b0a861dec8
5 changed files with 160 additions and 56 deletions

View File

@@ -7,9 +7,9 @@ const CancelToken = Axios.CancelToken;
const EventEmitter = require('events');
const OAuth = require('./core/OAuth');
const Player = require('./core/Player');
const Actions = require('./core/Actions');
const Livechat = require('./core/Livechat');
const SessionBuilder = require('./core/SessionBuilder');
const Utils = require('./utils/Utils');
const Request = require('./utils/Request');
@@ -44,56 +44,41 @@ class Innertube {
this.#retry_count = 0;
return this.#init();
}
async #init() {
const response = await Axios.get(Constants.URLS.YT_BASE, Constants.DEFAULT_HEADERS(this.config)).catch((error) => error);
if (response instanceof Error) throw new Utils.InnertubeError('Could not retrieve Innertube session', { message: response.message, status_code: response.status || 0 });
const data = JSON.parse(`{${Utils.getStringBetweenStrings(response.data, 'ytcfg.set({', '});') || ''}}`);
if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY;
this.version = data.INNERTUBE_API_VERSION;
this.context = data.INNERTUBE_CONTEXT;
this.player_url = data.PLAYER_JS_URL;
this.logged_in = data.LOGGED_IN;
this.sts = data.STS;
this.context.client.hl = 'en';
this.context.client.gl = this.config.gl || 'US';
/**
* @event Innertube#auth - fired when signing in to an account.
* @event Innertube#update-credentials - fired when the access token is no longer valid.
* @type {EventEmitter}
*/
this.ev = new EventEmitter();
this.#oauth = new OAuth(this.ev);
this.#player = new Player(this);
await this.#player.init();
if (this.logged_in && this.config.cookie) {
this.auth_apisid = Utils.getStringBetweenStrings(this.config.cookie, 'PAPISID=', ';');
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
}
this.request = new Request(this);
this.actions = new Actions(this);
this.#initMethods();
} else {
this.#retry_count += 1;
if (this.#retry_count >= 10)
throw new Utils.ParsingError('No InnerTubeContext shell provided in ytconfig.', {
data_snippet: response.data.slice(0, 300),
status_code: response.status || 0
});
return this.#init();
const session = await new SessionBuilder(this.config).build();
this.key = session.key;
this.version = session.api_version;
this.context = session.context;
this.logged_in = false;
this.player_url = session.player.url;
this.sts = session.player.sts;
this.#player = session.player;
/**
* @fires Innertube#auth - fired when signing in to an account.
* @fires Innertube#update-credentials - fired when the access token is no longer valid.
* @type {EventEmitter}
*/
this.ev = new EventEmitter();
this.#oauth = new OAuth(this.ev);
if (this.config.cookie) {
this.auth_apisid = Utils.getStringBetweenStrings(this.config.cookie, 'PAPISID=', ';');
this.auth_apisid = Utils.generateSidAuth(this.auth_apisid);
}
this.request = new Request(this);
this.actions = new Actions(this);
this.#initMethods();
return this;
}
#initMethods() {
this.account = {
info: () => this.getAccountInfo(),
@@ -705,7 +690,7 @@ class Innertube {
format.url = format.url || format.signatureCipher || format.cipher;
if (format.signatureCipher || format.cipher) {
format.url = new Signature(format.url, this.#player).decipher();
format.url = new Signature(format.url, this.#player.signature_decipher).decipher();
}
const url_components = new URL(format.url);
@@ -713,7 +698,7 @@ class Innertube {
url_components.searchParams.set('ratebypass', 'yes');
if (url_components.searchParams.get('n')) {
url_components.searchParams.set('n', new NToken(this.#player.ntoken_sc, url_components.searchParams.get('n')).transform());
url_components.searchParams.set('n', new NToken(this.#player.ntoken_decipher, url_components.searchParams.get('n')).transform());
}
format.url = url_components.toString();
@@ -796,7 +781,7 @@ class Innertube {
let cancel;
let cancelled = false;
const cpn = Utils.generateContentPlaybackNonce();
const cpn = Utils.generateRandomString(16);
const stream = new Stream.PassThrough();
this.actions.getVideoInfo(id, cpn).then(async (video_data) => {

View File

@@ -197,7 +197,7 @@ class OAuth {
const url_body = Constants.OAUTH.REGEX.AUTH_SCRIPT.exec(yttv_response.data)[1];
const script_url = `${Constants.URLS.YT_BASE}/${url_body}`;
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS()).catch((error) => error);
const response = await Axios.get(script_url).catch((error) => error);
if (response instanceof Error) throw new Error(`Could not extract client identity: ${response.message}`);
const client_identity = response.data.replace(/\n/g, '').match(Constants.OAUTH.REGEX.CLIENT_IDENTITY);

119
lib/core/SessionBuilder.js Normal file
View File

@@ -0,0 +1,119 @@
'use strict';
const Axios = require('axios');
const Player = require('./Player');
const Proto = require('../proto');
const Utils = require('../utils/Utils');
const Constants = require('../utils/Constants');
const UserAgent = require('user-agents');
class SessionBuilder {
#config;
#key;
#client_name;
#client_version;
#api_version;
#context;
#player;
constructor(config) {
this.#config = config;
}
async build() {
const data = await Promise.all([
this.#getYtConfig(),
this.#getPlayerId()
]);
const ytcfg = data[0];
this.#key = ytcfg.INNERTUBE_API_KEY;
this.#client_name = ytcfg.INNERTUBE_CLIENT_NAME;
this.#client_version = ytcfg.INNERTUBE_CLIENT_VERSION;
this.#api_version = ytcfg.INNERTUBE_API_VERSION;
this.#player = await new Player(data[1]).init();
this.#context = this.#buildContext();
return this;
}
#buildContext() {
const user_agent = new UserAgent({ deviceCategory: 'desktop' });
const id = Utils.generateRandomString(11);
const timestamp = new Date().getTime();
const visitor_data = Proto.encodeVisitorData(id, timestamp);
const context = {
client: {
hl: 'en',
gl: this.#config.gl || 'US',
deviceMake: user_agent.vendor,
deviceModel: user_agent.platform,
visitorData: visitor_data,
userAgent: user_agent.toString(),
clientName: this.#client_name,
clientVersion: this.#client_version,
originalUrl: Constants.URLS.YT_BASE
},
user: { lockedSafetyMode: false },
request: { useSsl: true }
}
return context;
}
async #getYtConfig() {
const response = await Axios.get(`${Constants.URLS.YT_BASE}/sw.js`).catch((err) => err);
if (response instanceof Error)
throw new Utils.InnertubeError('Could not retrieve session data', {
status_code: response?.response?.status || 0,
message: response.message
});
return JSON.parse(Utils.getStringBetweenStrings(response.data, 'ytcfg.set(', ')'));
}
async #getPlayerId() {
const response = await Axios.get(`${Constants.URLS.YT_BASE}/iframe_api`).catch((err) => err);
if (response instanceof Error)
throw new Utils.InnertubeError('Could not retrieve js player id', {
status_code: response?.response?.status || 0,
message: response.message
});
return Utils.getStringBetweenStrings(response.data, 'player\\/', '\\/');
}
get key() {
return this.#key;
}
get context() {
return this.#context;
}
get api_version() {
return this.#api_version;
}
get client_version() {
return this.#client_version;
}
get client_name() {
return this.#client_name;
}
get player() {
return this.#player;
}
}
module.exports = SessionBuilder;

View File

@@ -77,4 +77,4 @@ class Request {
}
}
module.exports = Request;
module.exports = Request;

View File

@@ -86,11 +86,11 @@ function generateSidAuth(sid) {
return ['SAPISIDHASH', [timestamp, gen_hash].join('_')].join(' ');
}
function generateContentPlaybackNonce() {
function generateRandomString(length) {
const result = [];
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
for (let i = 0; i < 16; i++) {
for (let i = 0; i < length; i++) {
result.push(alphabet.charAt(Math.floor(Math.random() * alphabet.length)));
}
@@ -139,6 +139,6 @@ function refineNTokenData(data) {
}
const errors = { UnavailableContentError, ParsingError, DownloadError, InnertubeError, MissingParamError, NoStreamingDataError };
const functions = { findNode, getRandomUserAgent, generateSidAuth, generateContentPlaybackNonce, getStringBetweenStrings, camelToSnake, timeToSeconds, refineNTokenData };
const functions = { findNode, getRandomUserAgent, generateSidAuth, generateRandomString, getStringBetweenStrings, camelToSnake, timeToSeconds, refineNTokenData };
module.exports = { ...functions, ...errors };