Files
YouTube.js/src/utils/HTTPClient.ts
LuanRT cac762569a feat(Session): allow overriding geolocation (#260)
* Allow overriding geolocation

* Fix some inconsistencies (unrelated)
2022-12-19 03:55:38 -03:00

160 lines
5.3 KiB
TypeScript

import Session, { Context } from '../core/Session';
import Constants from './Constants';
import { generateSidAuth, getRandomUserAgent, getStringBetweenStrings, InnertubeError, isServer } from './Utils';
export type FetchFunction = typeof fetch;
export interface HTTPClientInit {
baseURL?: string;
}
export default class HTTPClient {
#session: Session;
#cookie?: string;
#fetch: FetchFunction;
constructor(session: Session, cookie?: string, fetch?: FetchFunction) {
this.#session = session;
this.#cookie = cookie;
this.#fetch = fetch || globalThis.fetch;
}
get fetch_function() {
return this.#fetch;
}
async fetch(
input: URL | Request | string,
init?: RequestInit & HTTPClientInit
) {
const innertube_url = Constants.URLS.API.PRODUCTION_1 + this.#session.api_version;
const baseURL = init?.baseURL || innertube_url;
const request_url =
typeof input === 'string' ?
(!baseURL.endsWith('/') && !input.startsWith('/')) ?
new URL(`${baseURL}/${input}`) :
new URL(baseURL + input) :
input instanceof URL ?
input : new URL(input.url, baseURL);
const headers =
init?.headers ||
(input instanceof Request ? input.headers : new Headers()) ||
new Headers();
const body = init?.body || (input instanceof Request ? input.body : undefined);
const request_headers = new Headers(headers);
request_headers.set('Accept', '*/*');
request_headers.set('Accept-Language', `${this.#session.context.client.hl}-${this.#session.context.client.gl}`);
request_headers.set('x-goog-visitor-id', this.#session.context.client.visitorData || '');
request_headers.set('x-origin', request_url.origin);
request_headers.set('x-youtube-client-version', this.#session.context.client.clientVersion || '');
if (isServer()) {
request_headers.set('User-Agent', getRandomUserAgent('desktop'));
request_headers.set('origin', request_url.origin);
}
request_url.searchParams.set('key', this.#session.key);
request_url.searchParams.set('prettyPrint', 'false');
request_url.searchParams.set('alt', 'json');
const content_type = request_headers.get('Content-Type');
let request_body = body;
const is_innertube_req =
baseURL === innertube_url ||
baseURL === Constants.URLS.YT_UPLOAD;
// Copy context into payload when possible
if (content_type === 'application/json' && is_innertube_req && (typeof body === 'string')) {
const json = JSON.parse(body);
const n_body = {
...json,
// Deep copy since we're gonna be modifying it
context: JSON.parse(JSON.stringify(this.#session.context))
};
this.#adjustContext(n_body.context, n_body.client);
request_headers.set('x-youtube-client-version', n_body.context.client.clientVersion);
delete n_body.client;
request_body = JSON.stringify(n_body);
}
// Authenticate
if (this.#session.logged_in && is_innertube_req) {
const oauth = this.#session.oauth;
if (oauth.validateCredentials()) {
await oauth.refreshIfRequired();
request_headers.set('authorization', `Bearer ${oauth.credentials.access_token}`);
// Remove API key as it is not required when using oauth.
request_url.searchParams.delete('key');
}
if (this.#cookie) {
const papisid = getStringBetweenStrings(this.#cookie, 'PAPISID=', ';');
if (papisid) {
request_headers.set('authorization', await generateSidAuth(papisid));
request_headers.set('x-goog-authuser', this.#session.account_index.toString());
}
request_headers.set('cookie', this.#cookie);
}
}
const request = new Request(request_url, input instanceof Request ? input : init);
const response = await this.#fetch(request, {
body: request_body,
headers: request_headers,
credentials: 'include',
redirect: input instanceof Request ? input.redirect : init?.redirect || 'follow'
});
// Check if 2xx
if (response.ok) {
return response;
} throw new InnertubeError(`Request to ${response.url} failed with status ${response.status}`, await response.text());
}
#adjustContext(ctx: Context, client: string) {
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;
ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION;
break;
case 'YTMUSIC_ANDROID':
ctx.client.clientVersion = Constants.CLIENTS.YTMUSIC_ANDROID.VERSION;
ctx.client.clientFormFactor = 'SMALL_FORM_FACTOR';
ctx.client.clientName = Constants.CLIENTS.YTMUSIC_ANDROID.NAME;
ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION;
break;
case 'TV_EMBEDDED':
ctx.client.clientVersion = Constants.CLIENTS.TV_EMBEDDED.VERSION;
ctx.client.clientName = Constants.CLIENTS.TV_EMBEDDED.NAME;
ctx.client.clientScreen = 'EMBED';
ctx.thirdParty = { embedUrl: Constants.URLS.YT_BASE };
break;
default:
break;
}
}
}