Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for generating sessions locally #277

Merged
merged 4 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 138 additions & 24 deletions src/core/Session.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import UniversalCache from '../utils/Cache';
import Constants from '../utils/Constants';
import Constants, { CLIENTS } from '../utils/Constants';
import EventEmitterLike from '../utils/EventEmitterLike';
import Actions from './Actions';
import Player from './Player';

import HTTPClient, { FetchFunction } from '../utils/HTTPClient';
import { DeviceCategory, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils';
import { DeviceCategory, generateRandomString, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils';
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth';
import Proto from '../proto';

export enum ClientType {
WEB = 'WEB',
Expand All @@ -21,7 +22,7 @@ export interface Context {
client: {
hl: string;
gl: string;
remoteHost: string;
remoteHost?: string;
screenDensityFloat: number;
screenHeightPoints: number;
screenPixelDensity: number;
Expand All @@ -38,8 +39,8 @@ export interface Context {
clientFormFactor: string;
userInterfaceTheme: string;
timeZone: string;
browserName: string;
browserVersion: string;
browserName?: string;
browserVersion?: string;
originalUrl: string;
deviceMake: string;
deviceModel: string;
Expand All @@ -58,25 +59,72 @@ export interface Context {
}

export interface SessionOptions {
/**
* Language.
*/
lang?: string;
/**
* Geolocation.
*/
location?: string;
/**
* The account index to use. This is useful if you have multiple accounts logged in.
* **NOTE:**
* Only works if you are signed in with cookies.
*/
account_index?: number;
/**
* Specifies whether to retrieve the JS player. Disabling this will make session creation faster.
* **NOTE:** Deciphering formats is not possible without the JS player.
*/
retrieve_player?: boolean;
/**
* Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content.
*/
enable_safety_mode?: boolean;
/**
* Specifies whether to generate the session data locally or retrieve it from YouTube.
* This can be useful if you need more performance.
*/
generate_session_locally?: boolean;
/**
* Platform to use for the session.
*/
device_category?: DeviceCategory;
/**
* InnerTube client type.
*/
client_type?: ClientType;
/**
* The time zone.
*/
timezone?: string;
/**
* Used to cache the deciphering functions from the JS player.
*/
cache?: UniversalCache;
/**
* YouTube cookies.
*/
cookie?: string;
/**
* Fetch function to use.
*/
fetch?: FetchFunction;
}

export interface SessionData {
context: Context;
api_key: string;
api_version: string;
}

export default class Session extends EventEmitterLike {
#api_version;
#key;
#context;
#account_index;
#player;
#api_version: string;
#key: string;
#context: Context;
#account_index: number;
#player?: Player;

oauth: OAuth;
http: HTTPClient;
Expand Down Expand Up @@ -121,6 +169,7 @@ export default class Session extends EventEmitterLike {
options.location,
options.account_index,
options.enable_safety_mode,
options.generate_session_locally,
options.device_category,
options.client_type,
options.timezone,
Expand All @@ -135,30 +184,49 @@ export default class Session extends EventEmitterLike {
}

static async getSessionData(
lang = 'en-US',
lang = '',
location = '',
account_index = 0,
enable_safety_mode = false,
generate_session_locally = false,
device_category: DeviceCategory = 'desktop',
client_name: ClientType = ClientType.WEB,
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
fetch: FetchFunction = globalThis.fetch
) {
let session_data: SessionData;

if (generate_session_locally) {
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode });
} else {
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode }, fetch);
}

return { ...session_data, account_index };
}

static async #retrieveSessionData(options: {
lang: string;
location: string;
time_zone: string;
device_category: string;
client_name: string;
enable_safety_mode: boolean;
}, fetch: FetchFunction = globalThis.fetch): Promise<SessionData> {
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);

const res = await fetch(url, {
headers: {
'accept-language': lang,
'accept-language': options.lang || 'en-US',
'user-agent': getRandomUserAgent('desktop'),
'accept': '*/*',
'referer': 'https://www.youtube.com/sw.js',
'cookie': `PREF=tz=${tz.replace('/', '.')}`
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')}`
}
});

if (!res.ok) {
throw new SessionError(`Failed to get session data: ${res.status}`);
}
if (!res.ok)
throw new SessionError(`Failed to retrieve session data: ${res.status}`);

const text = await res.text();
const data = JSON.parse(text.replace(/^\)\]\}'/, ''));
Expand All @@ -172,22 +240,22 @@ export default class Session extends EventEmitterLike {
const context: Context = {
client: {
hl: device_info[0],
gl: location || device_info[2],
gl: options.location || device_info[2],
remoteHost: device_info[3],
screenDensityFloat: 1,
screenHeightPoints: 720,
screenHeightPoints: 1080,
screenPixelDensity: 1,
screenWidthPoints: 1280,
screenWidthPoints: 1920,
visitorData: device_info[13],
userAgent: device_info[14],
clientName: client_name,
clientName: options.client_name,
clientVersion: device_info[16],
osName: device_info[17],
osVersion: device_info[18],
platform: device_category.toUpperCase(),
platform: options.device_category.toUpperCase(),
clientFormFactor: 'UNKNOWN_FORM_FACTOR',
userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',
timeZone: device_info[79],
timeZone: device_info[79] || options.time_zone,
browserName: device_info[86],
browserVersion: device_info[87],
originalUrl: Constants.URLS.YT_BASE,
Expand All @@ -196,15 +264,61 @@ export default class Session extends EventEmitterLike {
utcOffsetMinutes: new Date().getTimezoneOffset()
},
user: {
enableSafetyMode: enable_safety_mode,
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
},
request: {
useSsl: true
}
};

return { context, api_key, api_version };
}

static #generateSessionData(options: {
lang: string;
location: string;
time_zone: string;
device_category: DeviceCategory;
client_name: string;
enable_safety_mode: boolean
}): SessionData {
const id = generateRandomString(11);
const timestamp = Math.floor(Date.now() / 1000);

const context: Context = {
client: {
hl: options.lang || 'en',
gl: options.location || 'US',
screenDensityFloat: 1,
screenHeightPoints: 1080,
screenPixelDensity: 1,
screenWidthPoints: 1920,
visitorData: Proto.encodeVisitorData(id, timestamp),
userAgent: getRandomUserAgent('desktop'),
clientName: options.client_name,
clientVersion: CLIENTS.WEB.VERSION,
osName: 'Windows',
osVersion: '10.0',
platform: options.device_category.toUpperCase(),
clientFormFactor: 'UNKNOWN_FORM_FACTOR',
userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',
timeZone: options.time_zone,
originalUrl: Constants.URLS.YT_BASE,
deviceMake: '',
deviceModel: '',
utcOffsetMinutes: new Date().getTimezoneOffset()
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
},
request: {
useSsl: true
}
};

return { context, api_key, api_version, account_index };
return { context, api_key: CLIENTS.WEB.API_KEY, api_version: CLIENTS.WEB.API_VERSION };
}

async signIn(credentials?: Credentials): Promise<void> {
Expand Down
7 changes: 6 additions & 1 deletion src/proto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { CLIENTS } from '../utils/Constants';
import { u8ToBase64 } from '../utils/Utils';
import { VideoMetadata } from '../core/Studio';

import { ChannelAnalytics, CreateCommentParams, GetCommentsSectionParams, InnertubePayload, LiveMessageParams, MusicSearchFilter, NotificationPreferences, PeformCommentActionParams, SearchFilter, SearchFilter_Filters } from './youtube';
import { ChannelAnalytics, CreateCommentParams, GetCommentsSectionParams, InnertubePayload, LiveMessageParams, MusicSearchFilter, NotificationPreferences, PeformCommentActionParams, SearchFilter, SearchFilter_Filters, VisitorData } from './youtube';

class Proto {
static encodeVisitorData(id: string, timestamp: number): string {
const buf = VisitorData.toBinary({ id, timestamp });
return encodeURIComponent(u8ToBase64(buf).replace(/\+/g, '-').replace(/\//g, '_'));
}

static encodeChannelAnalyticsParams(channel_id: string): string {
const buf = ChannelAnalytics.toBinary({
params: {
Expand Down
17 changes: 11 additions & 6 deletions src/proto/youtube.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
syntax = "proto2";
package youtube;

message ChannelAnalytics {
message Params {
required string channel_id = 1001;
}

required Params params = 32;
message VisitorData {
required string id = 1;
required int32 timestamp = 5;
}

message InnertubePayload {
Expand Down Expand Up @@ -91,6 +88,14 @@ message InnertubePayload {
optional VideoThumbnail video_thumbnail = 20;
}

message ChannelAnalytics {
message Params {
required string channel_id = 1001;
}

required Params params = 32;
}

message SoundInfoParams {
message Sound {
message Params {
Expand Down
Loading