mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-07-02 21:52:48 +00:00
refactor: rewrite OAuth and Requester (#90)
* chore: update type declarations * dev: refactor oauth & requester * chore: tidy things up * chore: remove unneeded check
This commit is contained in:
@@ -20,18 +20,16 @@ module.exports = {
|
||||
GRANT_TYPE: 'http://oauth.net/grant_type/device/1.0',
|
||||
MODEL_NAME: 'ytlr::',
|
||||
HEADERS: {
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'origin': 'https://www.youtube.com',
|
||||
'user-agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
||||
'content-type': 'application/json',
|
||||
'referer': 'https://www.youtube.com/tv',
|
||||
'accept-language': 'en-US'
|
||||
}
|
||||
'accept': '*/*',
|
||||
'origin': 'https://www.youtube.com',
|
||||
'user-agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
||||
'content-type': 'application/json',
|
||||
'referer': 'https://www.youtube.com/tv',
|
||||
'accept-language': 'en-US'
|
||||
},
|
||||
REGEX: {
|
||||
AUTH_SCRIPT: /<script id="base-js" src="(.*?)" nonce=".*?"><\/script>/,
|
||||
CLIENT_IDENTITY: /.+?={};var .+?={clientId:"(?<id>.+?)",.+?:"(?<secret>.+?)"},/
|
||||
CLIENT_IDENTITY: /.+?={};var .+?={clientId:"(?<client_id>.+?)",.+?:"(?<client_secret>.+?)"},/
|
||||
}
|
||||
},
|
||||
CLIENTS: {
|
||||
|
||||
@@ -1,105 +1,126 @@
|
||||
'use strict';
|
||||
|
||||
const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
const Utils = require('./Utils');
|
||||
|
||||
/** @namespace */
|
||||
class Request {
|
||||
/**
|
||||
* @param {import('../Innertube')} session
|
||||
*/
|
||||
constructor(session) {
|
||||
this.session = session;
|
||||
#instance;
|
||||
#session;
|
||||
|
||||
this.instance = Axios.create({
|
||||
...session.axios.defaults,
|
||||
baseURL: Constants.URLS.API.PRODUCTION + session.version,
|
||||
headers: Constants.INNERTUBE_HEADERS_BASE,
|
||||
params: { key: session.key, prettyPrint: false },
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
|
||||
/** @type {Axios.AxiosInstance} */
|
||||
this.#instance = Axios.create({
|
||||
proxy: config.proxy,
|
||||
httpAgent: config.http_agent,
|
||||
httpsAgent: config.https_agent,
|
||||
params: { prettyPrint: false },
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'accept-encoding': 'gzip, deflate',
|
||||
'content-type': 'application/json',
|
||||
'user-agent': Utils.getRandomUserAgent('desktop').userAgent
|
||||
},
|
||||
validateStatus: () => true,
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
this.#setupInterceptor();
|
||||
|
||||
return this.instance;
|
||||
this.#setupRequestInterceptor();
|
||||
this.#setupResponseInterceptor();
|
||||
}
|
||||
|
||||
#setupInterceptor() {
|
||||
this.instance.interceptors.request.use((config) => {
|
||||
const is_json_payload = typeof config.data == 'object';
|
||||
#setupRequestInterceptor() {
|
||||
this.#instance.interceptors.request.use(async (config) => {
|
||||
if (this.#session) {
|
||||
const innertube_url = `${Constants.URLS.API.PRODUCTION}${this.#session.version}`;
|
||||
|
||||
config.headers['user-agent'] = Utils.getRandomUserAgent('desktop').userAgent;
|
||||
config.headers['accept-language'] = `en-${this.session.config.gl || 'US'}`;
|
||||
config.headers['x-goog-visitor-id'] = this.session.context.client.visitorData || '';
|
||||
config.headers['x-youtube-client-version'] = this.session.context.client.clientVersion;
|
||||
config.baseURL = config.baseURL || innertube_url;
|
||||
|
||||
if (is_json_payload) {
|
||||
config.data = {
|
||||
context: JSON.parse(JSON.stringify(this.session.context)), // Deep copies the context object
|
||||
...config.data
|
||||
};
|
||||
config.headers['accept-language'] = `en-${this.#session.config.gl || 'US'}`;
|
||||
config.headers['x-goog-visitor-id'] = this.#session.context.client.visitorData || '';
|
||||
config.headers['x-youtube-client-version'] = this.#session.context.client.clientVersion;
|
||||
config.headers['x-origin'] = new URL(config.baseURL).origin;
|
||||
config.headers['origin'] = new URL(config.baseURL).origin;
|
||||
|
||||
this.#adjustContext(config.data.context, config.data.client);
|
||||
config.params.key = this.#session.key;
|
||||
|
||||
config.headers['x-youtube-client-version'] = config.data.context.client.clientVersion;
|
||||
config.headers['x-origin'] = config.data.context.client.originalUrl;
|
||||
config.headers['origin'] = config.data.context.client.originalUrl;
|
||||
const is_innertube_req = config.baseURL == innertube_url;
|
||||
|
||||
delete config.data.client;
|
||||
// Copy the context into the payload.
|
||||
if (is_innertube_req && typeof config.data === 'object') {
|
||||
config.data = {
|
||||
context: JSON.parse(JSON.stringify(this.#session.context)), // Deep copies the context object
|
||||
...config.data
|
||||
};
|
||||
|
||||
this.#adjustContext(config.data.context, config.data.client);
|
||||
|
||||
config.headers['x-youtube-client-version'] = config.data.context.client.clientVersion;
|
||||
|
||||
delete config.data.client;
|
||||
}
|
||||
|
||||
// Check if authentication is possible.
|
||||
if (this.#session.logged_in && is_innertube_req) {
|
||||
const oauth = this.#session.oauth;
|
||||
|
||||
if (oauth.validateCredentials()) {
|
||||
// Check if the access token is valid to avoid authorization errors.
|
||||
await oauth.checkAccessTokenValidity();
|
||||
|
||||
config.headers.authorization = `Bearer ${oauth.credentials.access_token}`;
|
||||
|
||||
// Remove API key as it is not required when using oauth.
|
||||
delete config.params.key;
|
||||
}
|
||||
|
||||
if (this.config.cookie) {
|
||||
const papisid = Utils.getStringBetweenStrings(this.config.cookie, 'PAPISID=', ';');
|
||||
config.headers.authorization = Utils.generateSidAuth(papisid);
|
||||
config.headers.cookie = this.config.cookie;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.session.logged_in) {
|
||||
const cookie = this.session.config.cookie;
|
||||
|
||||
const token = cookie &&
|
||||
this.session.auth_apisid ||
|
||||
this.session.access_token;
|
||||
|
||||
config.headers.cookie = cookie || '';
|
||||
config.headers.authorization = cookie ? token : `Bearer ${token}`;
|
||||
|
||||
!cookie && (delete config.params.key);
|
||||
if (this.config.debug) {
|
||||
const url = `${(config.baseURL ? `${config.baseURL}` : '')}${config.url}`;
|
||||
console.info('\n', `[${config.method.toUpperCase()}] > ${url}`, '\n', config?.data || 'N/A', '\n');
|
||||
}
|
||||
|
||||
this.session.config.debug &&
|
||||
console.info('\n', `[${config.method.toUpperCase()}]`, '>', config.baseURL + config.url, '\n', config?.data, '\n');
|
||||
|
||||
return config;
|
||||
}, (error) => {
|
||||
throw new Utils.InnertubeError(error.message, error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardizes the API response and catches all errors.
|
||||
*/
|
||||
this.instance.interceptors.response.use((res) => {
|
||||
const response = {
|
||||
success: res.status === 200,
|
||||
status_code: res.status,
|
||||
data: res.data
|
||||
};
|
||||
#setupResponseInterceptor() {
|
||||
this.#instance.interceptors.response.use(
|
||||
(res) => {
|
||||
const response = {
|
||||
success: res.status === 200,
|
||||
status_code: res.status,
|
||||
data: res.data
|
||||
};
|
||||
|
||||
if (res.status !== 200)
|
||||
throw new Utils.InnertubeError(`Request to ${res.config.url} failed with status code ${res.status} ${res.statusText}`, response);
|
||||
if (res.status !== 200)
|
||||
throw new Utils.InnertubeError(`Request to ${res.config.url} failed with status code ${res.status}`, response);
|
||||
|
||||
return response;
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
this.instance.interceptors.response.use(undefined, (error) => {
|
||||
if (error.info) return Promise.reject(error);
|
||||
throw new Utils.InnertubeError('Could not complete this operation', error.message);
|
||||
});
|
||||
this.#instance.interceptors.response.use(undefined,
|
||||
(error) => {
|
||||
if (error.info) return Promise.reject(error);
|
||||
throw new Utils.InnertubeError('Could not complete this operation', error.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the context according to the given client.
|
||||
*
|
||||
* Adjusts the context object according to the given client.
|
||||
* @param {object} ctx
|
||||
* @param {string} client
|
||||
* @todo refactor this?
|
||||
* @returns {void}
|
||||
*/
|
||||
#adjustContext(ctx, client) {
|
||||
switch (client) {
|
||||
@@ -116,6 +137,18 @@ class Request {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setSession(session) {
|
||||
this.#session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the axios instance.
|
||||
* @returns {Axios.AxiosInstance}
|
||||
*/
|
||||
get instance() {
|
||||
return this.#instance;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Request;
|
||||
@@ -22,6 +22,7 @@ class DownloadError extends InnertubeError {}
|
||||
class MissingParamError extends InnertubeError {}
|
||||
class UnavailableContentError extends InnertubeError {}
|
||||
class NoStreamingDataError extends InnertubeError {}
|
||||
class OAuthError extends InnertubeError {}
|
||||
|
||||
/**
|
||||
* Utility to help access deep properties of an object.
|
||||
@@ -274,7 +275,7 @@ function refineNTokenData(data) {
|
||||
.replace(/""/g, '').replace(/length]\)}"/g, 'length])}');
|
||||
}
|
||||
|
||||
const errors = { InnertubeError, UnavailableContentError, ParsingError, DownloadError, MissingParamError, NoStreamingDataError };
|
||||
const errors = { InnertubeError, UnavailableContentError, ParsingError, DownloadError, MissingParamError, NoStreamingDataError, OAuthError };
|
||||
|
||||
const functions = {
|
||||
findNode, observe, getRandomUserAgent, generateSidAuth, generateRandomString, getStringBetweenStrings,
|
||||
|
||||
Reference in New Issue
Block a user