diff --git a/lib/Innertube.js b/lib/Innertube.js index b9ee1fe0..02514763 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -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) => { diff --git a/lib/core/OAuth.js b/lib/core/OAuth.js index f42e743e..7e29c900 100644 --- a/lib/core/OAuth.js +++ b/lib/core/OAuth.js @@ -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); diff --git a/lib/core/SessionBuilder.js b/lib/core/SessionBuilder.js new file mode 100644 index 00000000..0f2103a2 --- /dev/null +++ b/lib/core/SessionBuilder.js @@ -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; \ No newline at end of file diff --git a/lib/utils/Request.js b/lib/utils/Request.js index 9187e0a8..5a9b3664 100644 --- a/lib/utils/Request.js +++ b/lib/utils/Request.js @@ -77,4 +77,4 @@ class Request { } } -module.exports = Request; +module.exports = Request; \ No newline at end of file diff --git a/lib/utils/Utils.js b/lib/utils/Utils.js index 9136bde5..2ef308bf 100644 --- a/lib/utils/Utils.js +++ b/lib/utils/Utils.js @@ -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 }; \ No newline at end of file