Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
336e04a
feat(YouTube Studio Web): Integrate YouTube Studio Web around as a ne…
Illusion137 Aug 12, 2026
88a56b1
feat(YouTube Studio Web): Add support for scotty chunk based file upl…
Illusion137 Aug 12, 2026
1a6eddb
feat(YouTube Studio Web): Add in the `DelegationContext` proto conten…
Illusion137 Aug 12, 2026
7793af5
chore(YouTube Studio Web Examples): remove unecessary package-lock.js…
Illusion137 Aug 12, 2026
a438f64
feat(YouTube Studio Web): new access interface that requires channel …
Illusion137 Aug 12, 2026
745db99
chore(YouTube Studio Web): I forgot to commit the updated types from …
Illusion137 Aug 12, 2026
8da2107
fix(YouTube Studio Web): make `uploadVideo` use the new single channe…
Illusion137 Aug 12, 2026
d348e96
fix(YouTube Studio Web): `uploadSubtitles` was broken because of an a…
Illusion137 Aug 12, 2026
0e4d89f
feat(YouTube Studio Web Examples): two working good examples of usage :3
Illusion137 Aug 12, 2026
072674d
Merge branch 'main' of https://github.com/Illusion137/YouTube.js into…
Illusion137 Aug 12, 2026
81006db
refactor(YouTube Studio Web): put initial EATS in constants
Illusion137 Aug 12, 2026
c054521
chore(YouTube Studio Web): clean unecessary comments from porting
Illusion137 Aug 12, 2026
15c3964
chore(Utils): remove unecessary arg in `wait` `setTimeout`
Illusion137 Aug 12, 2026
66b2fd9
fix(Parser): fix potentaily bug from checking wheather a boolean is a…
Illusion137 Aug 12, 2026
81e91f4
chore(Types): remove duplicate `FileNamedBufferReader `
Illusion137 Aug 12, 2026
45db0bb
fix(Actions): fix edge case regarding not cleaning up Studio Web Cont…
Illusion137 Aug 12, 2026
7a53334
fix(YouTube Studio Web): `managedExecute` fix the attestation_placement
Illusion137 Aug 12, 2026
d1d1263
Merge branch 'main' of https://github.com/Illusion137/YouTube.js into…
Illusion137 Aug 13, 2026
7e4ca0f
feat(YouTube Studio Web/Parser): updating the parser to have many of …
Illusion137 Aug 14, 2026
d27effc
Merge branch 'main' of https://github.com/Illusion137/YouTube.js into…
Illusion137 Aug 14, 2026
5954f25
refactor(YouTube Studio Web Examples): just simply fetch `CHANNEL_ID`…
Illusion137 Aug 14, 2026
99a9331
refactor(YouTube Studio Web): move the constant context into HTTPClie…
Illusion137 Aug 15, 2026
f6df8f7
fix(YouTube Studio Web Examples): use new system that uses parser cla…
Illusion137 Aug 15, 2026
3f1cd88
fix(YouTube Studio Web): metadata endpoint doesn't return videos, but…
Illusion137 Aug 15, 2026
99b2e0a
refactor(YouTube Studio Web): make `update_metadata_response` and `up…
Illusion137 Aug 15, 2026
449d968
fix(YouTube Studio Web Example): should multiply by 100 to show actua…
Illusion137 Aug 15, 2026
bfc6b12
refactor(YouTube Studio Web / Parser): rework the new Studio part of …
Illusion137 Aug 15, 2026
f7f41ff
refactor(YouTube Studio Web): make parser classes self-parsing and st…
Illusion137 Aug 15, 2026
4ff7b6c
feat(YouTube Studio Web): update VIDEO_READ_MASK to include everythin…
Illusion137 Aug 15, 2026
ef25cc0
feat(YouTube Studio Web Example): use `cookie-eater` for easier testing
Illusion137 Aug 30, 2026
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
17 changes: 17 additions & 0 deletions examples/youtube-studio-web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "youtube-studio-web",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {},
"devDependencies": {
"@types/jsdom": "^27.0.0",
"typescript": "^5.0.4"
},
"dependencies": {
"bgutils-js": "^4.0.3",
"cookie-eater": "^1.0.0",
"jsdom": "^28.1.0",
"youtubei.js": "file:../../"
}
}
16 changes: 16 additions & 0 deletions examples/youtube-studio-web/src/GetSessionToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Innertube, { UniversalCache } from 'youtubei.js';
import { botguard_solver, get_channel_id, youtube_cookies } from './utils.ts';


(async () => {
const COOKIES = await youtube_cookies();

const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES });

const CHANNEL_ID = await get_channel_id(yt);

const yt_studio_web = yt.studioWeb(CHANNEL_ID);
yt_studio_web.setBotGuardSolver(botguard_solver);
const session_token = await yt_studio_web.getSessionToken();
console.log(session_token);
})();
40 changes: 40 additions & 0 deletions examples/youtube-studio-web/src/UploadVideo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Innertube, { UniversalCache } from 'youtubei.js';
import { botguard_solver, get_channel_id, get_file_named_buffer_base64, get_file_named_buffer_reader } from "./utils.ts";

const COOKIES = ""; // ?? Place your YouTube cookies here
const VIDEO_FILE_PATH = ""; // ?? Place your video file path here
const THUMBNAIL_FILE_PATH = ""; // ?? Place your thumbnail file path here
const SRT_FILE_PATH = ""; // ?? Place your srt file path here

(async () => {
const yt = await Innertube.create({ cache: new UniversalCache(false), cookie: COOKIES });

const CHANNEL_ID = await get_channel_id(yt);

const yt_studio_web = yt.studioWeb(CHANNEL_ID);
yt_studio_web.setBotGuardSolver(botguard_solver);

console.log('creating...');

const created_response = await yt_studio_web.uploadVideo(await get_file_named_buffer_reader(VIDEO_FILE_PATH), {
title: "This is a test upload",
thumbnail: await get_file_named_buffer_reader(THUMBNAIL_FILE_PATH),
subtitles: {
synced: true,
data: await get_file_named_buffer_base64(SRT_FILE_PATH)
},
description: "This is a test description",
visibility: "PRIVATE"
}, (written_bytes, total_bytes) => {
// NOTE this callback only fires around every 100mb
console.log(`${(written_bytes / total_bytes) * 100}%`);
}, async (full_created) => {
// NOTE this cycle fires every 2s-ish
await yt_studio_web.uploadFeedbackCycle([full_created.feedback_token], (content) => {
if(content[0].transfer_progress_bar?.progress_message) console.log(content[0].transfer_progress_bar?.progress_message);
if(content[0].is_processing) console.log("processing...");
return true;
});
});
console.log(created_response);
})();
85 changes: 85 additions & 0 deletions examples/youtube-studio-web/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { BotGuardClient } from "bgutils-js/botguard";
import { USER_AGENT } from "bgutils-js/utils";
import { JSDOM, VirtualConsole } from "jsdom";
import Innertube, { type Types } from 'youtubei.js';
import path from "path";
import fs from 'fs/promises';
import { get_cookies } from 'cookie-eater';

export const botguard_solver: Types.BotGuardSolver<string> = {
solve: async(botguard_challenge, binding) => {
const virtual_console = new VirtualConsole();
const dom = new JSDOM('<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>', { url: "https://www.youtube.com", referrer: "https://www.youtube.com/", userAgent: USER_AGENT, resources: "usable", runScripts: "dangerously", virtualConsole: virtual_console });

Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location, origin: dom.window.origin });

if (!("navigator" in globalThis)) {
Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator });
}

Object.defineProperty(dom.window.HTMLCanvasElement.prototype, "getContext", { value: () => null, writable: true });

let interpreter_url = botguard_challenge.interpreter_url ?? "";

if (interpreter_url.startsWith("//")) interpreter_url = `https:${interpreter_url}`;

const bg_script_response = await fetch(interpreter_url);
const interpreter_javascript = await bg_script_response.text();

new Function(interpreter_javascript)();

const botguard = await BotGuardClient.create({ program: botguard_challenge.program, globalName: botguard_challenge.global_name, globalObject: globalThis });

const botguard_response = await botguard.snapshot({ contentBinding: { atr_challenge: binding } });
return botguard_response;
}
};

export async function get_channel_id(yt: Innertube, index?: number): Promise<string> {
const account_info = await yt.account.getInfo(true);
const account = account_info[index ?? 0];
if (account === undefined) throw new Error("No accounts found");
const resolve_url = `https://www.youtube.com/${account.channel_handle.text}`;
const resolved = await yt.resolveURL(resolve_url);
const channel_id = resolved.payload.browseId as string;
return channel_id;
}

export async function get_file_named_buffer_reader(file_path: string): Promise<Types.FileNamedBufferReader> {
const stats = await fs.stat(file_path);
return {
file_name: path.basename(file_path),
source: {
total_bytes: stats.size,
read_chunk: async (position: number, length: number) => {
let handle: fs.FileHandle | undefined = undefined;
try {
handle = await fs.open(file_path, "r");
const buffer = Buffer.allocUnsafe(length);
const { bytesRead } = await handle.read(buffer, 0, length, position);
return new Uint8Array(buffer.buffer, buffer.byteOffset, bytesRead);
} finally {
await handle?.close();
}
}
}
};
}

export async function get_file_named_buffer_base64(file_path: string): Promise<Types.FileNamedBufferBase64> {
return {
file_name: path.basename(file_path),
source: {
base64: (await fs.readFile(file_path, { encoding: "base64" })).toString()
}
};
}

export async function youtube_cookies(): Promise<string|undefined> {
const HOST_NAME = '.youtube.com';
const cookies_map = await get_cookies([HOST_NAME]);
const cookies = cookies_map?.[HOST_NAME].toString() ?? undefined;

if(cookies) console.log(`Successfully found ${cookies_map?.[HOST_NAME].getCookies().length} cookies`);
return cookies;
}
20 changes: 20 additions & 0 deletions examples/youtube-studio-web/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"strict": true,
"moduleResolution": "bundler",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true
}
}
76 changes: 74 additions & 2 deletions protos/generated/youtube/api/pfiinnertube/user_info.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion protos/youtube/api/pfiinnertube/user_info.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ message UserInfo {

message KidsParent { }
message DelegatePurchases { }
message DelegationContext { }

message DelegationContext {
optional string external_channel_id = 2;
optional RoleType role_type = 5;

message RoleType {
optional ChannelRoleType channel_role_type = 1;

enum ChannelRoleType {
CHANNEL_ROLE_TYPE_UNKNOWN = 0;
CREATOR_CHANNEL_ROLE_TYPE_OWNER = 8;
}
}
}

message CredentialTransferToken { }
}
20 changes: 14 additions & 6 deletions src/Innertube.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Session from './core/Session.js';

import { Kids, Music, Studio } from './core/clients/index.js';
import { Kids, Music, Studio, StudioWeb } from './core/clients/index.js';
import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js';
import { Feed, TabbedFeed } from './core/mixins/index.js';

Expand Down Expand Up @@ -143,7 +143,7 @@ export default class Innertube {
signatureTimestamp: session.player?.signature_timestamp
}
},
client: options?.client
client: options?.client
};

if (options?.po_token) {
Expand All @@ -155,7 +155,7 @@ export default class Innertube {
poToken: session.po_token
};
}

const watch_response = await watch_endpoint.call(session.actions, extra_payload);

const cpn = generateRandomString(16);
Expand Down Expand Up @@ -296,7 +296,7 @@ export default class Innertube {
'Cookie': session.cookie || ''
}
});

const text = await response.text();

const data = JSON.parse(text.replace('window.google.ac.h(', '').slice(0, -1));
Expand Down Expand Up @@ -560,10 +560,10 @@ export default class Innertube {
const payload: Record<string, any> = {
engagementType: engagement_type
};

if (ids)
payload.ids = ids;

return this.actions.execute('/att/get', { parse: true, ...payload });
}

Expand All @@ -590,6 +590,14 @@ export default class Innertube {
return new Studio(this.#session);
}

/**
* An interface for interacting with YouTube Studio Web.
* @param channel_id - the channel id to interface with
*/
studioWeb(channel_id: string) {
return new StudioWeb(this.#session, channel_id);
}

/**
* An interface for interacting with YouTube Kids.
*/
Expand Down
Loading