Skip to content

Commit

Permalink
feat(Session): allow setting a custom visitor data token (#371)
Browse files Browse the repository at this point in the history
* feat(Session): allow setting a custom visitor data token

* docs: update init options

* chore: lint
  • Loading branch information
LuanRT authored Mar 24, 2023
1 parent cb8fafe commit 13ebf0a
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 17 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const youtube = await Innertube.create(/* options */);
| `lang` | `string` | Language. | `en` |
| `location` | `string` | Geolocation. | `US` |
| `account_index` | `number` | 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. | `0` |
| `visitor_data` | `string` | Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in. A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint. | `undefined` |
| `retrieve_player` | `boolean` | Specifies whether to retrieve the JS player. Disabling this will make session creation faster. **NOTE:** Deciphering formats is not possible without the JS player. | `true` |
| `enable_safety_mode` | `boolean` | Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content. | `false` |
| `generate_session_locally` | `boolean` | Specifies whether to generate the session data locally or retrieve it from YouTube. This can be useful if you need more performance. | `false` |
Expand Down
40 changes: 30 additions & 10 deletions src/core/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import EventEmitterLike from '../utils/EventEmitterLike.js';
import Actions from './Actions.js';
import Player from './Player.js';

import HTTPClient from '../utils/HTTPClient.js';
import { Platform, DeviceCategory, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils.js';
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
import Proto from '../proto/index.js';
import { ICache } from '../types/Cache.js';
import { FetchFunction } from '../types/PlatformShim.js';
import HTTPClient from '../utils/HTTPClient.js';
import { DeviceCategory, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.js';
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';

export enum ClientType {
WEB = 'WEB',
Expand Down Expand Up @@ -118,6 +118,11 @@ export interface SessionOptions {
* YouTube cookies.
*/
cookie?: string;
/**
* Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in.
* A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint.
*/
visitor_data?: string;
/**
* Fetch function to use.
*/
Expand Down Expand Up @@ -179,6 +184,7 @@ export default class Session extends EventEmitterLike {
options.lang,
options.location,
options.account_index,
options.visitor_data,
options.enable_safety_mode,
options.generate_session_locally,
options.device_category,
Expand All @@ -198,6 +204,7 @@ export default class Session extends EventEmitterLike {
lang = '',
location = '',
account_index = 0,
visitor_data = '',
enable_safety_mode = false,
generate_session_locally = false,
device_category: DeviceCategory = 'desktop',
Expand All @@ -208,9 +215,9 @@ export default class Session extends EventEmitterLike {
let session_data: SessionData;

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

return { ...session_data, account_index };
Expand All @@ -223,16 +230,24 @@ export default class Session extends EventEmitterLike {
device_category: string;
client_name: string;
enable_safety_mode: boolean;
visitor_data: string;
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);

let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;

if (options.visitor_data) {
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
visitor_id = decoded_visitor_data.id;
}

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

Expand Down Expand Up @@ -292,10 +307,15 @@ export default class Session extends EventEmitterLike {
time_zone: string;
device_category: DeviceCategory;
client_name: string;
enable_safety_mode: boolean
enable_safety_mode: boolean;
visitor_data: string;
}): SessionData {
const id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
const timestamp = Math.floor(Date.now() / 1000);
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;

if (options.visitor_data) {
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
visitor_id = decoded_visitor_data.id;
}

const context: Context = {
client: {
Expand All @@ -305,7 +325,7 @@ export default class Session extends EventEmitterLike {
screenHeightPoints: 1080,
screenPixelDensity: 1,
screenWidthPoints: 1920,
visitorData: Proto.encodeVisitorData(id, timestamp),
visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)),
userAgent: getRandomUserAgent('desktop'),
clientName: options.client_name,
clientVersion: CLIENTS.WEB.VERSION,
Expand Down
7 changes: 6 additions & 1 deletion src/proto/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CLIENTS } from '../utils/Constants.js';
import { u8ToBase64 } from '../utils/Utils.js';
import { base64ToU8, u8ToBase64 } from '../utils/Utils.js';
import { VideoMetadata } from '../core/Studio.js';

import * as VisitorData from './generated/messages/youtube/VisitorData.js';
Expand All @@ -21,6 +21,11 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf).replace(/\+/g, '-').replace(/\//g, '_'));
}

static decodeVisitorData(visitor_data: string): VisitorData.Type {
const data = VisitorData.decodeBinary(base64ToU8(decodeURIComponent(visitor_data)));
return data;
}

static encodeChannelAnalyticsParams(channel_id: string): string {
const buf = ChannelAnalytics.encodeBinary({
params: {
Expand Down
6 changes: 5 additions & 1 deletion src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function getRandomUserAgent(type: DeviceCategory): string {
}

/**
* Generates an authentication token from a cookies' sid..js
* Generates an authentication token from a cookies' sid.
* @param sid - Sid extracted from cookies
*/
export async function generateSidAuth(sid: string): Promise<string> {
Expand Down Expand Up @@ -217,4 +217,8 @@ export const debugFetch: FetchFunction = (input, init) => {

export function u8ToBase64(u8: Uint8Array): string {
return btoa(String.fromCharCode.apply(null, Array.from(u8)));
}

export function base64ToU8(base64: string): Uint8Array {
return new Uint8Array(atob(base64).split('').map((char) => char.charCodeAt(0)));
}
7 changes: 2 additions & 5 deletions test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe('YouTube.js Tests', () => {
it('should retrieve the "Related" tab', async () => {
const info = await yt.music.getInfo(VIDEOS[1].ID);
const related = await info.getRelated();
expect((related as any).length).toBeGreaterThan(3);
expect((related as any).length).toBeGreaterThan(0);
});

it('should retrieve albums', async () => {
Expand Down Expand Up @@ -278,15 +278,12 @@ describe('YouTube.js Tests', () => {
});

async function download(id: string, yt: Innertube): Promise<boolean> {
// TODO: add back info
// let got_video_info = false;

const stream = await yt.download(id, { type: 'video+audio' });
const file = fs.createWriteStream(`./${id}.mp4`);

for await (const chunk of Utils.streamToIterable(stream)) {
file.write(chunk);
}

return fs.existsSync(`./${id}.mp4`); // && got_video_info;
return fs.existsSync(`./${id}.mp4`);
}

0 comments on commit 13ebf0a

Please sign in to comment.