Compare commits

...

17 Commits

Author SHA1 Message Date
github-actions[bot]
3ec1609974 chore(main): release 12.2.0 (#833)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-12 10:32:02 -03:00
Luan
25fd979363 chore(deps): Update jintr 2024-12-12 10:30:45 -03:00
jonz94
0c319aacde feat(parser): Update LiveChatMembershipItem (#836) 2024-12-12 09:39:25 -03:00
jonz94
69d42b291c feat(parser): Update LiveChatBanner (#840)
* Mark `viewerIsCreator` property as optional
* Mark `background_type` property as optional
* Add `banner_type` property
* Add `banner_properties_is_ephemeral` property
* Add `banner_properties_auto_collapse_delay_seconds` property
2024-12-12 09:38:44 -03:00
jonz94
9a9bb76a92 feat: add VideoMetadataCarouselView (#839) 2024-12-12 09:34:07 -03:00
jonz94
0b2b0da957 feat(parser): Add ClientSideToggleMenuItem (#835)
* feat(parser): Add `ClientSideToggleMenuItem`

* feat: parse the command

* feat: handle different type of `ClientSideToggleMenuItem`
2024-12-12 09:31:59 -03:00
jonz94
eeaae6209f feat(parser): Add ButtonCardView (#834) 2024-12-12 09:31:14 -03:00
absidue
13e796123b fix(Player): Fix signature algorithm extraction again (#837) 2024-12-12 09:30:50 -03:00
Luan
5f233ae34e feat(parser): Add ActiveAccountHeader
Found on the 'You' (a.k.a, 'Library') page.
2024-12-10 18:32:31 -03:00
Luan
c1de097ce1 chore(Parser): Don't ignore BackgroundPromo
It turns out this is not an ad at all.
2024-12-10 16:30:23 -03:00
Luan
67f13fffac feat(Actions): Allow auth check to be skipped 2024-12-10 16:12:06 -03:00
Luan
c8173c88e0 feat(parser): Add PlaylistThumbnailOverlay 2024-12-10 13:50:20 -03:00
github-actions[bot]
8e97bbc058 chore(main): release 12.1.0 (#830)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-10 10:10:40 -03:00
Luan
283172f220 fix(Player): Bump cache version
This should fix bad (n)sig caches.
2024-12-10 10:08:05 -03:00
absidue
ce4996cea7 fix(Player): Fix signature algorithm extraction (#832) 2024-12-10 09:28:08 -03:00
Luan
ad2ae51b97 feat(parser): Add mobile guide nodes 2024-12-08 17:11:57 -03:00
Luan
4bf125b6a5 feat: Add MWEB client 2024-12-08 17:11:57 -03:00
31 changed files with 506 additions and 45 deletions

View File

@@ -1,5 +1,38 @@
# Changelog
## [12.2.0](https://github.com/LuanRT/YouTube.js/compare/v12.1.0...v12.2.0) (2024-12-12)
### Features
* **Actions:** Allow auth check to be skipped ([67f13ff](https://github.com/LuanRT/YouTube.js/commit/67f13fffacec2c655a03d66c6d8016620d9abcf9))
* add `VideoMetadataCarouselView` ([#839](https://github.com/LuanRT/YouTube.js/issues/839)) ([9a9bb76](https://github.com/LuanRT/YouTube.js/commit/9a9bb76a928594c5c5f3e828c86081bf79c2562d))
* **parser:** Add `ActiveAccountHeader` ([5f233ae](https://github.com/LuanRT/YouTube.js/commit/5f233ae34e278e7f7a0c48e4ba762d9bac9e312f))
* **parser:** Add `ButtonCardView` ([#834](https://github.com/LuanRT/YouTube.js/issues/834)) ([eeaae62](https://github.com/LuanRT/YouTube.js/commit/eeaae6209f238b838b9b7fdd9bbef89f4f858fa3))
* **parser:** Add `ClientSideToggleMenuItem` ([#835](https://github.com/LuanRT/YouTube.js/issues/835)) ([0b2b0da](https://github.com/LuanRT/YouTube.js/commit/0b2b0da9577f8d6ad19393700071ea9f26d4da10))
* **parser:** Add `PlaylistThumbnailOverlay` ([c8173c8](https://github.com/LuanRT/YouTube.js/commit/c8173c88e0e17ec4bb4e93af1867c55d07611cc0))
* **parser:** Update `LiveChatBanner` ([#840](https://github.com/LuanRT/YouTube.js/issues/840)) ([69d42b2](https://github.com/LuanRT/YouTube.js/commit/69d42b291c927abb9d84f97ed03518c4ddd4506e))
* **parser:** Update `LiveChatMembershipItem` ([#836](https://github.com/LuanRT/YouTube.js/issues/836)) ([0c319aa](https://github.com/LuanRT/YouTube.js/commit/0c319aacdeba106d84b7f44505a7a296a154f97a))
### Bug Fixes
* **Player:** Fix signature algorithm extraction again ([#837](https://github.com/LuanRT/YouTube.js/issues/837)) ([13e7961](https://github.com/LuanRT/YouTube.js/commit/13e796123b87136f2d5d3b3c9b3ed079a014bf46))
## [12.1.0](https://github.com/LuanRT/YouTube.js/compare/v12.0.0...v12.1.0) (2024-12-10)
### Features
* Add `MWEB` client ([4bf125b](https://github.com/LuanRT/YouTube.js/commit/4bf125b6a53460f631410e1ab949a16cc0c7d095))
* **parser:** Add mobile guide nodes ([ad2ae51](https://github.com/LuanRT/YouTube.js/commit/ad2ae51b97d84dcb9d2547b4cfef7402f2410404))
### Bug Fixes
* **Player:** Bump cache version ([283172f](https://github.com/LuanRT/YouTube.js/commit/283172f22032d60b407cb0159a391a147b569432))
* **Player:** Fix signature algorithm extraction ([#832](https://github.com/LuanRT/YouTube.js/issues/832)) ([ce4996c](https://github.com/LuanRT/YouTube.js/commit/ce4996cea7b0607cb7f9aca7ae9c9e439d964a5a))
## [12.0.0](https://github.com/LuanRT/YouTube.js/compare/v11.0.1...v12.0.0) (2024-12-05)

View File

@@ -46,8 +46,10 @@ app.get('/', async (_req, res) => {
console.info('Innertube instance is logged in.');
const userInfo = await innertube.account.getInfo();
console.log(await innertube.getBasicInfo('R8vgwMYSQi8', 'ANDROID'));
return res.send({ userInfo });
return res.send({ userInfo });
}
if (!oAuth2Client) {

12
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "youtubei.js",
"version": "12.0.0",
"version": "12.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "youtubei.js",
"version": "12.0.0",
"version": "12.2.0",
"funding": [
"https://github.com/sponsors/LuanRT"
],
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"jintr": "^3.1.0",
"jintr": "^3.2.0",
"tslib": "^2.5.0",
"undici": "^5.19.1"
},
@@ -6155,9 +6155,9 @@
}
},
"node_modules/jintr": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jintr/-/jintr-3.1.0.tgz",
"integrity": "sha512-azhCHApkRfBH8INpiUCwKBYaNCdB5G+x3NApsI2MxQXSlgFAx7rap3YwE3JAkN08GO8f3ilZsGB0Yvc+412ntQ==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jintr/-/jintr-3.2.0.tgz",
"integrity": "sha512-psD1yf05kMKDNsUdW1l5YhO59pHScQ6OIHHb8W5SKSM2dCOFPsqolmIuSHgVA8+3Dc47NJR181CXZ4alCAPTkA==",
"funding": [
"https://github.com/sponsors/LuanRT"
],

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "12.0.0",
"version": "12.2.0",
"description": "A JavaScript client for YouTube's private API, known as InnerTube.",
"type": "module",
"types": "./dist/src/platform/lib.d.ts",
@@ -107,7 +107,7 @@
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"jintr": "^3.1.0",
"jintr": "^3.2.0",
"tslib": "^2.5.0",
"undici": "^5.19.1"
},

View File

@@ -1,9 +1,14 @@
import type {
IBrowseResponse, IGetNotificationsMenuResponse, INextResponse,
IParsedResponse, IPlayerResponse, IRawResponse,
IResolveURLResponse, ISearchResponse, IUpdatedMetadataResponse
IBrowseResponse,
IGetNotificationsMenuResponse,
INextResponse,
IParsedResponse,
IPlayerResponse,
IRawResponse,
IResolveURLResponse,
ISearchResponse,
IUpdatedMetadataResponse
} from '../parser/index.js';
import { NavigateAction, Parser } from '../parser/index.js';
import { InnertubeError } from '../utils/Utils.js';
@@ -15,17 +20,25 @@ export interface ApiResponse {
data: IRawResponse;
}
export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/reel' | '/updated_metadata' | '/notification/get_notification_menu' | string;
export type InnertubeEndpoint =
'/player'
| '/search'
| '/browse'
| '/next'
| '/reel'
| '/updated_metadata'
| '/notification/get_notification_menu'
| string;
export type ParsedResponse<T> =
T extends '/player' ? IPlayerResponse :
T extends '/search' ? ISearchResponse :
T extends '/browse' ? IBrowseResponse :
T extends '/next' ? INextResponse :
T extends '/updated_metadata' ? IUpdatedMetadataResponse :
T extends '/navigation/resolve_url' ? IResolveURLResponse :
T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse :
IParsedResponse;
T extends '/search' ? ISearchResponse :
T extends '/browse' ? IBrowseResponse :
T extends '/next' ? INextResponse :
T extends '/updated_metadata' ? IUpdatedMetadataResponse :
T extends '/navigation/resolve_url' ? IResolveURLResponse :
T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse :
IParsedResponse;
export default class Actions {
public session: Session;
@@ -52,7 +65,9 @@ export default class Actions {
* @param client - The client to use.
* @param params - Call parameters.
*/
async stats(url: string, client: { client_name: string; client_version: string }, params: { [key: string]: any }): Promise<Response> {
async stats(url: string, client: { client_name: string; client_version: string }, params: {
[key: string]: any
}): Promise<Response> {
const s_url = new URL(url);
s_url.searchParams.set('ver', '2');
@@ -72,18 +87,39 @@ export default class Actions {
* @param endpoint - The endpoint to call.
* @param args - Call arguments
*/
async execute<T extends InnertubeEndpoint>(endpoint: T, args: { [key: string]: any; parse: true; protobuf?: false; serialized_data?: any }): Promise<ParsedResponse<T>>;
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: { [key: string]: any; parse?: false; protobuf?: true; serialized_data?: any }): Promise<ApiResponse>;
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: { [key: string]: any; parse?: boolean; protobuf?: boolean; serialized_data?: any }): Promise<ParsedResponse<T> | ApiResponse> {
async execute<T extends InnertubeEndpoint>(endpoint: T, args: {
[key: string]: any;
parse: true;
protobuf?: false;
serialized_data?: any;
skip_auth_check?: boolean
}): Promise<ParsedResponse<T>>;
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: {
[key: string]: any;
parse?: false;
protobuf?: true;
serialized_data?: any;
skip_auth_check?: boolean
}): Promise<ApiResponse>;
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: {
[key: string]: any;
parse?: boolean;
protobuf?: boolean;
serialized_data?: any;
skip_auth_check?: boolean
}): Promise<ParsedResponse<T> | ApiResponse> {
let data;
if (args && !args.protobuf) {
data = { ...args };
if (Reflect.has(data, 'browseId')) {
if (Reflect.has(data, 'browseId') && !args.skip_auth_check) {
if (this.#needsLogin(data.browseId) && !this.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
}
if (Reflect.has(data, 'skip_auth_check'))
delete data.skip_auth_check;
if (Reflect.has(data, 'override_endpoint'))
delete data.override_endpoint;

View File

@@ -140,6 +140,9 @@ export default class Player {
case 'WEB':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB.VERSION);
break;
case 'MWEB':
url_components.searchParams.set('cver', Constants.CLIENTS.MWEB.VERSION);
break;
case 'WEB_REMIX':
url_components.searchParams.set('cver', Constants.CLIENTS.YTMUSIC.VERSION);
break;
@@ -220,15 +223,23 @@ export default class Player {
return parseInt(getStringBetweenStrings(data, 'signatureTimestamp:', ',') || '0');
}
static extractSigSourceCode(data: string): string {
const calls = getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
const obj_name = calls?.split(/\.|\[/)?.[0]?.replace(';', '')?.trim();
static extractSigSourceCode(data: string): string | undefined {
const match = data.match(/function\(([A-Za-z_0-9]+)\)\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\.split\(""\)(.+?)\.join\(""\))\}/);
if (!match) {
Log.warn(TAG, 'Failed to extract signature decipher algorithm.');
return;
}
const var_name = match[1];
const obj_name = match[3].split(/\.|\[/)[0]?.replace(';', '').trim();
const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};');
if (!functions || !calls)
if (!functions || !var_name)
Log.warn(TAG, 'Failed to extract signature decipher algorithm.');
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`;
return `function descramble_sig(${var_name}) { let ${obj_name}={${functions}}; ${match[2]} } descramble_sig(sig);`;
}
static extractNSigSourceCode(data: string): string | undefined {
@@ -252,6 +263,6 @@ export default class Player {
}
static get LIBRARY_VERSION(): number {
return 11;
return 13;
}
}

View File

@@ -15,6 +15,7 @@ import type { OAuth2Tokens, OAuth2AuthErrorEventHandler, OAuth2AuthPendingEventH
export enum ClientType {
WEB = 'WEB',
MWEB = 'MWEB',
KIDS = 'WEB_KIDS',
MUSIC = 'WEB_REMIX',
IOS = 'iOS',

View File

@@ -0,0 +1,24 @@
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class ActiveAccountHeader extends YTNode {
static type = 'ActiveAccountHeader';
public account_name: Text;
public account_photo: Thumbnail[];
public endpoint: NavigationEndpoint;
public manage_account_title: Text;
public channel_handle: Text;
constructor(data: RawNode) {
super();
this.account_name = new Text(data.accountName);
this.account_photo = Thumbnail.fromResponse(data.accountPhoto);
this.endpoint = new NavigationEndpoint(data.serviceEndpoint);
this.manage_account_title = new Text(data.manageAccountTitle);
this.channel_handle = new Text(data.channelHandle);
}
}

View File

@@ -0,0 +1,25 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import Text from './misc/Text.js';
import Button from './Button.js';
import ButtonView from './ButtonView.js';
export default class BackgroundPromo extends YTNode {
static type = 'BackgroundPromo';
public body_text?: Text;
public cta_button?: Button | ButtonView | null;
public icon_type?: string;
public title?: Text;
constructor(data: RawNode) {
super();
this.body_text = new Text(data.bodyText);
this.cta_button = Parser.parseItem(data.ctaButton, [ Button, ButtonView ]);
if (Reflect.has(data, 'icon'))
this.icon_type = data.icon.iconType;
this.title = new Text(data.title);
}
}

View File

@@ -0,0 +1,18 @@
import NavigationEndpoint from './NavigationEndpoint.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
export default class ButtonCardView extends YTNode {
static type = 'ButtonCardView';
title: string;
icon_name: string;
on_tap_endpoint: NavigationEndpoint;
constructor(data: RawNode) {
super();
this.title = data.title;
this.icon_name = data.icon.sources[0].clientResource.imageName;
this.on_tap_endpoint = new NavigationEndpoint(data.rendererContext.commandContext.onTap);
}
}

View File

@@ -0,0 +1,16 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import TextCarouselItemView from './TextCarouselItemView.js';
export default class CarouselItemView extends YTNode {
static type = 'CarouselItemView';
item_type: string;
carousel_item: TextCarouselItemView | null;
constructor(data: RawNode) {
super();
this.item_type = data.itemType;
this.carousel_item = Parser.parseItem(data.carouselItem, TextCarouselItemView);
}
}

View File

@@ -0,0 +1,18 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import ButtonView from './ButtonView.js';
export default class CarouselTitleView extends YTNode {
static type = 'CarouselTitleView';
title: string;
previous_button: ButtonView | null;
next_button: ButtonView | null;
constructor(data: RawNode) {
super();
this.title = data.title;
this.previous_button = Parser.parseItem(data.previousButton, ButtonView);
this.next_button = Parser.parseItem(data.nextButton, ButtonView);
}
}

View File

@@ -0,0 +1,46 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
export default class ClientSideToggleMenuItem extends YTNode {
static type = 'ClientSideToggleMenuItem';
text: Text;
icon_type: string;
toggled_text: Text;
toggled_icon_type: string;
is_toggled?: boolean;
menu_item_identifier: string;
endpoint: NavigationEndpoint;
logging_directives?: {
visibility: {
types: string;
},
enable_displaylogger_experiment: boolean;
};
constructor(data: RawNode) {
super();
this.text = new Text(data.defaultText);
this.icon_type = data.defaultIcon.iconType;
this.toggled_text = new Text(data.toggledText);
this.toggled_icon_type = data.toggledIcon.iconType;
if (Reflect.has(data, 'isToggled')) {
this.is_toggled = data.isToggled;
}
this.menu_item_identifier = data.menuItemIdentifier;
this.endpoint = new NavigationEndpoint(data.command);
if (Reflect.has(data, 'loggingDirectives')) {
this.logging_directives = {
visibility: {
types: data.loggingDirectives.visibility.types
},
enable_displaylogger_experiment: data.loggingDirectives.enableDisplayloggerExperiment
};
}
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import Text from './misc/Text.js';
export default class PlaylistThumbnailOverlay extends YTNode {
static type = 'PlaylistThumbnailOverlay';
public icon_type?: string;
public text: Text;
constructor(data: RawNode) {
super();
if (Reflect.has(data, 'icon'))
this.icon_type = data.icon.iconType;
this.text = new Text(data.text);
}
}

View File

@@ -0,0 +1,22 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import { Text } from '../misc.js';
import ButtonView from './ButtonView.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class TextCarouselItemView extends YTNode {
static type = 'TextCarouselItemView';
icon_name: string;
text: Text;
on_tap_endpoint: NavigationEndpoint;
button: ButtonView | null;
constructor(data: RawNode) {
super();
this.icon_name = data.iconName;
this.text = Text.fromAttributed(data.text);
this.on_tap_endpoint = new NavigationEndpoint(data.onTap);
this.button = Parser.parseItem(data.button, ButtonView);
}
}

View File

@@ -38,6 +38,7 @@ export default class Video extends YTNode {
show_action_menu: boolean;
is_watched: boolean;
menu: Menu | null;
byline_text?: Text;
search_video_result_entity_key?: string;
constructor(data: RawNode) {
@@ -91,6 +92,10 @@ export default class Video extends YTNode {
if (Reflect.has(data, 'searchVideoResultEntityKey')) {
this.search_video_result_entity_key = data.searchVideoResultEntityKey;
}
if (Reflect.has(data, 'bylineText')) {
this.byline_text = new Text(data.bylineText);
}
}
get description(): string {

View File

@@ -5,16 +5,20 @@ import Video from './Video.js';
export default class VideoCard extends Video {
static type = 'VideoCard';
public metadata_text?: Text;
public byline_text?: Text;
constructor(data: RawNode) {
super(data);
if (Reflect.has(data, 'metadataText')) {
const metadata = new Text(data.metadataText);
if (metadata.text) {
this.short_view_count = new Text({ simpleText: metadata.text.split('·')[0].trim() } as RawNode);
this.published = new Text({ simpleText: metadata.text.split('·')[1].trim() } as RawNode);
this.metadata_text = new Text(data.metadataText);
if (this.metadata_text.text) {
this.short_view_count = new Text({ simpleText: this.metadata_text.text.split('·')[0]?.trim() } as RawNode);
this.published = new Text({ simpleText: this.metadata_text.text.split('·')[1]?.trim() } as RawNode);
}
}
if (Reflect.has(data, 'bylineText')) {
this.author = new Author(data.bylineText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail);
}

View File

@@ -0,0 +1,17 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import CarouselItemView from './CarouselItemView.js';
import CarouselTitleView from './CarouselTitleView.js';
export default class VideoMetadataCarouselView extends YTNode {
static type = 'VideoMetadataCarouselView';
carousel_titles: ObservedArray<CarouselTitleView> | null;
carousel_items: ObservedArray<CarouselItemView> | null;
constructor(data: RawNode) {
super();
this.carousel_titles = Parser.parse(data.carouselTitles, true, CarouselTitleView);
this.carousel_items = Parser.parse(data.carouselItems, true, CarouselItemView);
}
}

View File

@@ -9,19 +9,46 @@ export default class LiveChatBanner extends YTNode {
header: LiveChatBannerHeader | null;
contents: YTNode;
action_id: string;
viewer_is_creator: boolean;
viewer_is_creator?: boolean;
target_id: string;
is_stackable: boolean;
background_type: string;
background_type?: string;
banner_type: string;
banner_properties_is_ephemeral?: boolean;
banner_properties_auto_collapse_delay_seconds?: string;
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header, LiveChatBannerHeader);
this.contents = Parser.parseItem(data.contents);
this.action_id = data.actionId;
this.viewer_is_creator = data.viewerIsCreator;
if (Reflect.has(data, 'viewerIsCreator')) {
this.viewer_is_creator = data.viewerIsCreator;
}
this.target_id = data.targetId;
this.is_stackable = data.isStackable;
this.background_type = data.backgroundType;
if (Reflect.has(data, 'backgroundType')) {
this.background_type = data.backgroundType;
}
this.banner_type = data.bannerType;
if (
Reflect.has(data, 'bannerProperties') &&
Reflect.has(data.bannerProperties, 'isEphemeral')
) {
this.banner_properties_is_ephemeral = Boolean(data.bannerProperties.isEphemeral);
}
if (
Reflect.has(data, 'bannerProperties') &&
Reflect.has(data.bannerProperties, 'autoCollapseDelay') &&
Reflect.has(data.bannerProperties.autoCollapseDelay, 'seconds')
) {
this.banner_properties_auto_collapse_delay_seconds = data.bannerProperties.autoCollapseDelay.seconds;
}
}
}

View File

@@ -9,16 +9,37 @@ export default class LiveChatMembershipItem extends YTNode {
id: string;
timestamp: number;
timestamp_usec: string;
timestamp_text?: Text;
header_primary_text?: Text;
header_subtext: Text;
message?: Text;
author: Author;
menu_endpoint: NavigationEndpoint;
context_menu_accessibility_label: string;
constructor(data: RawNode) {
super();
this.id = data.id;
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);
this.timestamp_usec = data.timestampUsec;
if (Reflect.has(data, 'timestampText')) {
this.timestamp_text = new Text(data.timestampText);
}
if (Reflect.has(data, 'headerPrimaryText')) {
this.header_primary_text = new Text(data.headerPrimaryText);
}
this.header_subtext = new Text(data.headerSubtext);
if (Reflect.has(data, 'message')) {
this.message = new Text(data.message);
}
this.author = new Author(data.authorName, data.authorBadges, data.authorPhoto, data.authorExternalChannelId);
this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint);
this.context_menu_accessibility_label = data.contextMenuAccessibility.accessibilityData.label;
}
}

View File

@@ -8,20 +8,21 @@ import SegmentedLikeDislikeButtonView from '../SegmentedLikeDislikeButtonView.js
import MenuFlexibleItem from './MenuFlexibleItem.js';
import LikeButton from '../LikeButton.js';
import ToggleButton from '../ToggleButton.js';
import FlexibleActionsView from '../FlexibleActionsView.js';
export default class Menu extends YTNode {
static type = 'Menu';
public items: ObservedArray<YTNode>;
public flexible_items: ObservedArray<MenuFlexibleItem>;
public top_level_buttons: ObservedArray<ToggleButton | LikeButton | Button |ButtonView | SegmentedLikeDislikeButtonView>;
public top_level_buttons: ObservedArray<ToggleButton | LikeButton | Button |ButtonView | SegmentedLikeDislikeButtonView | FlexibleActionsView>;
public label?: string;
constructor(data: RawNode) {
super();
this.items = Parser.parseArray(data.items);
this.flexible_items = Parser.parseArray(data.flexibleItems, MenuFlexibleItem);
this.top_level_buttons = Parser.parseArray(data.topLevelButtons, [ ToggleButton, LikeButton, Button, ButtonView, SegmentedLikeDislikeButtonView ]);
this.top_level_buttons = Parser.parseArray(data.topLevelButtons, [ ToggleButton, LikeButton, Button, ButtonView, SegmentedLikeDislikeButtonView, FlexibleActionsView ]);
if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'accessibilityData')) {
this.label = data.accessibility.accessibilityData.label;

View File

@@ -0,0 +1,20 @@
import { YTNode } from '../../helpers.js';
import Text from '../misc/Text.js';
import { Parser, type RawNode } from '../../index.js';
export default class MobileTopbar extends YTNode {
static type = 'MobileTopbar';
public placeholder_text: Text;
public buttons;
public logo_type?: string;
constructor(data: RawNode) {
super();
this.placeholder_text = new Text(data.placeholderText);
this.buttons = Parser.parseArray(data.buttons);
if (Reflect.has(data, 'logo') && Reflect.has(data.logo, 'iconType'))
this.logo_type = data.logo.iconType;
}
}

View File

@@ -0,0 +1,14 @@
import type { ObservedArray } from '../../helpers.js';
import { YTNode } from '../../helpers.js';
import { Parser, type RawNode } from '../../index.js';
export default class MultiPageMenuSection extends YTNode {
static type = 'MultiPageMenuSection';
public items: ObservedArray<YTNode> | null;
constructor(data: RawNode) {
super();
this.items = Parser.parseArray(data.items);
}
}

View File

@@ -0,0 +1,13 @@
import { YTNode } from '../../helpers.js';
import { Parser, type RawNode } from '../../index.js';
export default class PivotBar extends YTNode {
static type = 'PivotBar';
public items;
constructor(data: RawNode) {
super();
this.items = Parser.parseArray(data.items);
}
}

View File

@@ -0,0 +1,27 @@
import { YTNode } from '../../helpers.js';
import { type RawNode } from '../../index.js';
import Text from '../misc/Text.js';
import NavigationEndpoint from '../NavigationEndpoint.js';
export default class PivotBarItem extends YTNode {
static type = 'PivotBarItem';
public pivot_identifier: string;
public endpoint: NavigationEndpoint;
public title: Text;
public accessibility_label?: string;
public icon_type?: string;
constructor(data: RawNode) {
super();
this.pivot_identifier = data.pivotIdentifier;
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.title = new Text(data.title);
if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'accessibilityData'))
this.accessibility_label = data.accessibility.accessibilityData.label;
if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType'))
this.icon_type = data.icon.iconType;
}
}

View File

@@ -0,0 +1,18 @@
import { YTNode } from '../../helpers.js';
import { Parser, type RawNode } from '../../index.js';
export default class TopbarMenuButton extends YTNode {
static type = 'TopbarMenuButton';
public icon_type?: string;
public menu_renderer: YTNode | null;
public target_id: string;
constructor(data: RawNode) {
super();
if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType'))
this.icon_type = data.icon.iconType;
this.menu_renderer = Parser.parseItem(data.menuRenderer);
this.target_id = data.targetId;
}
}

View File

@@ -17,6 +17,7 @@ export { default as SignalAction } from './classes/actions/SignalAction.js';
export { default as UpdateChannelSwitcherPageAction } from './classes/actions/UpdateChannelSwitcherPageAction.js';
export { default as UpdateEngagementPanelAction } from './classes/actions/UpdateEngagementPanelAction.js';
export { default as UpdateSubscribeButtonAction } from './classes/actions/UpdateSubscribeButtonAction.js';
export { default as ActiveAccountHeader } from './classes/ActiveAccountHeader.js';
export { default as AddToPlaylist } from './classes/AddToPlaylist.js';
export { default as Alert } from './classes/Alert.js';
export { default as AlertWithButton } from './classes/AlertWithButton.js';
@@ -24,6 +25,7 @@ export { default as AttributionView } from './classes/AttributionView.js';
export { default as AudioOnlyPlayability } from './classes/AudioOnlyPlayability.js';
export { default as AutomixPreviewVideo } from './classes/AutomixPreviewVideo.js';
export { default as AvatarView } from './classes/AvatarView.js';
export { default as BackgroundPromo } from './classes/BackgroundPromo.js';
export { default as BackstageImage } from './classes/BackstageImage.js';
export { default as BackstagePost } from './classes/BackstagePost.js';
export { default as BackstagePostThread } from './classes/BackstagePostThread.js';
@@ -31,6 +33,7 @@ export { default as BadgeView } from './classes/BadgeView.js';
export { default as BrowseFeedActions } from './classes/BrowseFeedActions.js';
export { default as BrowserMediaSession } from './classes/BrowserMediaSession.js';
export { default as Button } from './classes/Button.js';
export { default as ButtonCardView } from './classes/ButtonCardView.js';
export { default as ButtonView } from './classes/ButtonView.js';
export { default as C4TabbedHeader } from './classes/C4TabbedHeader.js';
export { default as CallToActionButton } from './classes/CallToActionButton.js';
@@ -38,7 +41,9 @@ export { default as Card } from './classes/Card.js';
export { default as CardCollection } from './classes/CardCollection.js';
export { default as CarouselHeader } from './classes/CarouselHeader.js';
export { default as CarouselItem } from './classes/CarouselItem.js';
export { default as CarouselItemView } from './classes/CarouselItemView.js';
export { default as CarouselLockup } from './classes/CarouselLockup.js';
export { default as CarouselTitleView } from './classes/CarouselTitleView.js';
export { default as Channel } from './classes/Channel.js';
export { default as ChannelAboutFullMetadata } from './classes/ChannelAboutFullMetadata.js';
export { default as ChannelAgeGate } from './classes/ChannelAgeGate.js';
@@ -62,6 +67,7 @@ export { default as ChipBarView } from './classes/ChipBarView.js';
export { default as ChipCloud } from './classes/ChipCloud.js';
export { default as ChipCloudChip } from './classes/ChipCloudChip.js';
export { default as ChipView } from './classes/ChipView.js';
export { default as ClientSideToggleMenuItem } from './classes/ClientSideToggleMenuItem.js';
export { default as ClipAdState } from './classes/ClipAdState.js';
export { default as ClipCreation } from './classes/ClipCreation.js';
export { default as ClipCreationScrubber } from './classes/ClipCreationScrubber.js';
@@ -323,6 +329,11 @@ export { default as MusicTastebuilderShelfThumbnail } from './classes/MusicTaste
export { default as MusicThumbnail } from './classes/MusicThumbnail.js';
export { default as MusicTwoRowItem } from './classes/MusicTwoRowItem.js';
export { default as MusicVisualHeader } from './classes/MusicVisualHeader.js';
export { default as MobileTopbar } from './classes/mweb/MobileTopbar.js';
export { default as MultiPageMenuSection } from './classes/mweb/MultiPageMenuSection.js';
export { default as PivotBar } from './classes/mweb/PivotBar.js';
export { default as PivotBarItem } from './classes/mweb/PivotBarItem.js';
export { default as TopbarMenuButton } from './classes/mweb/TopbarMenuButton.js';
export { default as NavigationEndpoint } from './classes/NavigationEndpoint.js';
export { default as Notification } from './classes/Notification.js';
export { default as NotificationAction } from './classes/NotificationAction.js';
@@ -356,6 +367,7 @@ export { default as PlaylistPanelVideoWrapper } from './classes/PlaylistPanelVid
export { default as PlaylistSidebar } from './classes/PlaylistSidebar.js';
export { default as PlaylistSidebarPrimaryInfo } from './classes/PlaylistSidebarPrimaryInfo.js';
export { default as PlaylistSidebarSecondaryInfo } from './classes/PlaylistSidebarSecondaryInfo.js';
export { default as PlaylistThumbnailOverlay } from './classes/PlaylistThumbnailOverlay.js';
export { default as PlaylistVideo } from './classes/PlaylistVideo.js';
export { default as PlaylistVideoList } from './classes/PlaylistVideoList.js';
export { default as PlaylistVideoThumbnail } from './classes/PlaylistVideoThumbnail.js';
@@ -431,6 +443,7 @@ export { default as SubscriptionNotificationToggleButton } from './classes/Subsc
export { default as Tab } from './classes/Tab.js';
export { default as Tabbed } from './classes/Tabbed.js';
export { default as TabbedSearchResults } from './classes/TabbedSearchResults.js';
export { default as TextCarouselItemView } from './classes/TextCarouselItemView.js';
export { default as TextFieldView } from './classes/TextFieldView.js';
export { default as TextHeader } from './classes/TextHeader.js';
export { default as ThirdPartyShareTargetSection } from './classes/ThirdPartyShareTargetSection.js';
@@ -484,6 +497,7 @@ export { default as VideoDescriptionInfocardsSection } from './classes/VideoDesc
export { default as VideoDescriptionMusicSection } from './classes/VideoDescriptionMusicSection.js';
export { default as VideoDescriptionTranscriptSection } from './classes/VideoDescriptionTranscriptSection.js';
export { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.js';
export { default as VideoMetadataCarouselView } from './classes/VideoMetadataCarouselView.js';
export { default as VideoOwner } from './classes/VideoOwner.js';
export { default as VideoPrimaryInfo } from './classes/VideoPrimaryInfo.js';
export { default as VideoSecondaryInfo } from './classes/VideoSecondaryInfo.js';

View File

@@ -76,7 +76,6 @@ const IGNORED_LIST = new Set([
'SearchPyv',
'MealbarPromo',
'PrimetimePromo',
'BackgroundPromo',
'PromotedSparklesWeb',
'CompactPromotedVideo',
'BrandVideoShelf',

View File

@@ -1,7 +1,7 @@
import type { SessionOptions } from '../core/index.js';
export type InnerTubeConfig = SessionOptions;
export type InnerTubeClient = 'IOS' | 'WEB' | 'ANDROID' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'YTSTUDIO_ANDROID' | 'TV' | 'TV_EMBEDDED' | 'YTKIDS' | 'WEB_EMBEDDED' | 'WEB_CREATOR';
export type InnerTubeClient = 'IOS' | 'WEB' | 'MWEB' | 'ANDROID' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'YTSTUDIO_ANDROID' | 'TV' | 'TV_EMBEDDED' | 'YTKIDS' | 'WEB_EMBEDDED' | 'WEB_CREATOR';
export type UploadDate = 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';
export type SearchType = 'all' | 'video' | 'channel' | 'playlist' | 'movie';

View File

@@ -38,6 +38,12 @@ export const CLIENTS = Object.freeze({
API_VERSION: 'v1',
STATIC_VISITOR_ID: '6zpwvWUNAco'
},
MWEB: {
NAME_ID: '2',
NAME: 'MWEB',
VERSION: '2.20241205.01.00',
API_VERSION: 'v1'
},
WEB_KIDS: {
NAME_ID: '76',
NAME: 'WEB_KIDS',
@@ -105,4 +111,4 @@ export const INNERTUBE_HEADERS_BASE = Object.freeze({
'content-type': 'application/json'
});
export const SUPPORTED_CLIENTS = [ 'IOS', 'WEB', 'YTKIDS', 'YTMUSIC', 'ANDROID', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDROID', 'TV', 'TV_EMBEDDED', 'WEB_EMBEDDED', 'WEB_CREATOR' ];
export const SUPPORTED_CLIENTS = [ 'IOS', 'WEB', 'MWEB', 'YTKIDS', 'YTMUSIC', 'ANDROID', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDROID', 'TV', 'TV_EMBEDDED', 'WEB_EMBEDDED', 'WEB_CREATOR' ];

View File

@@ -190,6 +190,12 @@ export default class HTTPClient {
}
switch (client.toUpperCase()) {
case 'MWEB':
ctx.client.clientVersion = Constants.CLIENTS.MWEB.VERSION;
ctx.client.clientName = Constants.CLIENTS.MWEB.NAME;
ctx.client.clientFormFactor = 'SMALL_FORM_FACTOR';
ctx.client.platform = 'MOBILE';
break;
case 'IOS':
ctx.client.deviceMake = 'Apple';
ctx.client.deviceModel = Constants.CLIENTS.IOS.DEVICE_MODEL;