Files
YouTube.js/lib/utils/Request.js
LuanRT 84d5edb6f0 refactor: rewrite OAuth and Requester (#90)
* chore: update type declarations

* dev: refactor oauth & requester

* chore: tidy things up

* chore: remove unneeded check
2022-07-04 16:34:02 -03:00

154 lines
4.7 KiB
JavaScript

'use strict';
const Axios = require('axios');
const Constants = require('./Constants');
const Utils = require('./Utils');
class Request {
#instance;
#session;
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.#setupRequestInterceptor();
this.#setupResponseInterceptor();
}
#setupRequestInterceptor() {
this.#instance.interceptors.request.use(async (config) => {
if (this.#session) {
const innertube_url = `${Constants.URLS.API.PRODUCTION}${this.#session.version}`;
config.baseURL = config.baseURL || innertube_url;
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;
config.params.key = this.#session.key;
const is_innertube_req = config.baseURL == innertube_url;
// 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.config.debug) {
const url = `${(config.baseURL ? `${config.baseURL}` : '')}${config.url}`;
console.info('\n', `[${config.method.toUpperCase()}] > ${url}`, '\n', config?.data || 'N/A', '\n');
}
return config;
}, (error) => {
throw new Utils.InnertubeError(error.message, error);
});
}
#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}`, 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);
});
}
/**
* Adjusts the context object according to the given client.
* @param {object} ctx
* @param {string} client
*/
#adjustContext(ctx, client) {
switch (client) {
case 'YTMUSIC':
ctx.client.clientVersion = Constants.CLIENTS.YTMUSIC.VERSION;
ctx.client.clientName = Constants.CLIENTS.YTMUSIC.NAME;
break;
case 'ANDROID':
ctx.client.clientVersion = Constants.CLIENTS.ANDROID.VERSION;
ctx.client.clientFormFactor = 'SMALL_FORM_FACTOR';
ctx.client.clientName = Constants.CLIENTS.ANDROID.NAME;
break;
default:
break;
}
}
setSession(session) {
this.#session = session;
}
/**
* Returns the axios instance.
* @returns {Axios.AxiosInstance}
*/
get instance() {
return this.#instance;
}
}
module.exports = Request;