Compare commits

..

16 Commits

Author SHA1 Message Date
LuanRT
dc14d3785f chore(release): v2.3.2 2022-10-13 16:58:19 -03:00
LuanRT
088f909515 chore: update proto 2022-10-13 16:52:19 -03:00
LuanRT
2a78d77aa3 refactor: get visitor data from the API [skip ci] 2022-10-13 16:39:56 -03:00
LuanRT
1b2862c00f refactor: improve live chat polling (#220)
* dev: add RemoveChatItemByAuthorAction renderer parser

* dev: improve live chat polling
2022-10-12 16:16:07 -03:00
LuanRT
477c030084 feat(studio): add support for updating video metadata (#219)
* dev: update proto

* dev: add `Studio#updateVideoMetadata`

* feat: add `category` option

* chore(studio): update API ref
2022-10-12 16:08:53 -03:00
Émilien Devos
19d579df13 fix: wrong element name (#217) 2022-10-11 05:03:21 -03:00
LuanRT
5313c57783 chore(docs): fix typos [skip ci] 2022-10-06 05:24:09 -03:00
LuanRT
190f7681be chore: update tests 2022-10-06 05:20:24 -03:00
LuanRT
6e027bcc85 docs(livechat): update API ref 2022-10-06 04:44:49 -03:00
LuanRT
6b531dd0ea chore: lint 2022-10-06 04:38:28 -03:00
LuanRT
92f24076db docs(ytmusic): add library class docs 2022-10-06 04:36:17 -03:00
Akazawa Daisuke
a9eba7ca62 feat: add RemoveChatItemAction and LiveChatTickerStickerItem (#214) 2022-10-03 03:09:40 -03:00
Akazawa Daisuke
2f56c15ecc feat(LiveChat): add support for moderation & more (#202)
* Live Chat - Implement moderation

* Live Chat - Implement class ItemMenu

* fix moderation method

Co-authored-by: LuanRT <luan.lrt4@gmail.com>
2022-10-02 02:00:24 -03:00
LuanRT
95e0479745 docs: add parser ytnode instructions & other minor changes (#206)
* docs: add instructions on implementing ytnodes

* docs(parser): fix grammar & other minor improvements

* docs: update guidelines

* chore: update parser warning messages
2022-09-28 03:08:51 -03:00
LuanRT
556c7cd6e8 docs(parser): rephrase validTypes description [skip ci] 2022-09-23 03:38:11 -03:00
LuanRT
a4a88419ef docs(parser): escape | separators [skip ci] 2022-09-23 03:34:53 -03:00
27 changed files with 1240 additions and 316 deletions

View File

@@ -22,7 +22,7 @@ If you find a problem, search if an issue already exists. If a related issue doe
<a id="issue-2"></a>
#### Solve an issue
Scan through the existing issues to find one that interests you. You can narrow down the search using labels as filters. If you find an issue to work on, you are welcome to open a PR with a fix.
Scan through the existing issues to find one that interests you. You can narrow down the search using labels as filters. If you find an issue to work on, you are welcome to open a PR with a fix. Documentation updates and grammar fixes are also appreciated!
<a id="changes"></a>
## Make changes

View File

@@ -149,9 +149,29 @@ Retrieves “Explore” feed.
Retrieves library.
**Returns:** `Promise.<Library>`
**Returns:** `Library`
<!-- TODO: document Library's methods and getters. -->
<details>
<summary>Methods & Getters</summary>
<p>
- `<library>#getPlaylists(args?)`
- Retrieves the library's playlists.
- `<library>#getAlbums(args?)`
- Retrieves the library's albums.
- `<library>#getArtists(args?)`
- Retrieves the library's artists.
- `<library>#getSongs(args?)`
- Retrieves the library's songs.
- `<library>#getRecentActivity(args)`
- Retrieves recent activity.
</p>
</details>
<a name="getartist"></a>
### getArtist(artist_id)

View File

@@ -6,6 +6,7 @@ YouTube Studio class (WIP).
* Studio
* [.setThumbnail(video_id, buffer)](#setthumbnail)
* [.updateVideoMetadata(video_id, metadata)](#updatemetadata)
* [.upload(file, metadata)](#upload)
<a name="setthumbnail"></a>
@@ -20,6 +21,18 @@ Uploads a custom thumbnail and sets it for a video.
| video_id | `string` | Video id |
| buffer | `Uint8Array` | Thumbnail buffer |
<a name="updatemetadata"></a>
### updateVideoMetadata(video_id, metadata)
Updates given video's metadata.
**Returns:** `Promise.<ActionsResponse>`
| Param | Type | Description |
| --- | --- | --- |
| video_id | `string` | Video id |
| metadata | `VideoMetadata` | Video metadata |
<a name="upload"></a>
### upload(file, metadata)
@@ -30,4 +43,4 @@ Uploads a video to YouTube.
| Param | Type | Description |
| --- | --- | --- |
| file | `BodyInit` | Video file |
| metadata | `VideoMetadata` | Video metadata |
| metadata | `UploadedVideoMetadata` | Video metadata |

View File

@@ -0,0 +1,54 @@
# Updating the parser
YouTube is constantly changing, so it is not rare to see YouTube crawlers/scrapers breaking every now and then.
Our parser, on the other hand, was written so that it behaves similarly to an official client, parsing and mapping renderers (a.k.a YTNodes) dynamically without hard-coding their path in the response. This way, whenever a new renderer pops up (e.g; YouTube adds a new feature / minor UI changes) the library will print a warning like this in the console:
```
InnertubeError: SomeRenderer not found!
This is a bug, want to help us fix it? Follow the instructions at https://github.com/LuanRT/YouTube.js/blob/main/docs/updating-the-parser.md or report it at https://github.com/LuanRT/YouTube.js/issues!
at Parser.printError (...)
at Parser.parseItem (...)
at Parser.parseArray (...) {
info: {
// renderer data, can be used as a reference to implement the renderer parser
},
date: 2022-05-22T22:16:06.831Z,
version: '2.2.3'
}
```
This warning, however, **does not** throw an error. The parser itself will continue working normally, even if a parsing error occurs in an existing renderer parser.
## Adding a new renderer parser
Thanks to the modularity of the parser, a renderer can be implemented by simply adding a new file anywhere in the [classes directory](../src/parser/classes)!
For example, say we found a new renderer named `verticalListRenderer`, to let the parser know it exists we would have to create a file with the following structure:
> `../classes/VerticalList.ts`
```ts
import Parser from '..';
import { YTNode } from '../helpers';
class VerticalList extends YTNode {
static type = 'VerticalList';
header;
contents;
constructor(data: any) {
// parse the data here, ex;
this.header = Parser.parseItem(data.header);
this.contents = Parser.parseArray(data.contents);
}
}
export default VerticalList;
```
Then update the parser map:
```bash
npm run build:parser-map
```
And that's it!

View File

@@ -21,6 +21,7 @@ const livechat = await info.getLiveChat();
* [.ev](#ev) ⇒ `EventEmitter`
* [.start](#start) ⇒ `function`
* [.stop](#stop) ⇒ `function`
* [.getItemMenu](#getitemmenu) ⇒ `function`
* [.sendMessage](#sendmessage) ⇒ `function`
<a name="ev"></a>
@@ -58,6 +59,16 @@ Starts the Live Chat.
### stop()
Stops the Live Chat.
<a name="getitemmenu"></a>
### getItemMenu(item)
Retrieves given chat item's menu.
| Param | Type | Description |
| --- | --- | --- |
| item | `object` | Chat item |
**Returns:** `Promise<ItemMenu>`
<a name="sendmessage"></a>
### sendMessage(text)
Sends a message.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "youtubei.js",
"version": "2.2.3"
"version": "2.3.2"
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "youtubei.js",
"version": "2.2.3",
"version": "2.3.2",
"funding": [
"https://github.com/sponsors/LuanRT"
],

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "2.2.3",
"version": "2.3.2",
"description": "Full-featured wrapper around YouTube's private API.",
"main": "./dist/index.js",
"browser": "./bundle/browser.js",
@@ -12,7 +12,8 @@
"contributors": [
"Wykerd (https://github.com/wykerd/)",
"MasterOfBob777 (https://github.com/MasterOfBob777)",
"patrickkfkan (https://github.com/patrickkfkan)"
"patrickkfkan (https://github.com/patrickkfkan)",
"akkadaska (https://github.com/akkadaska)"
],
"directories": {
"test": "./test",

View File

@@ -455,8 +455,7 @@ class Actions {
};
break;
case 'live_chat/get_item_context_menu':
// Note: this is currently broken due to a recent refactor
// TODO: this should be implemented
data.params = args.params;
break;
case 'live_chat/moderate':
data.params = args.params;

View File

@@ -1,12 +1,11 @@
import Player from './Player';
import Proto from '../proto/index';
import Actions from './Actions';
import Constants from '../utils/Constants';
import UniversalCache from '../utils/Cache';
import EventEmitterLike from '../utils/EventEmitterLike';
import HTTPClient, { FetchFunction } from '../utils/HTTPClient';
import { DeviceCategory, generateRandomString, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils';
import { DeviceCategory, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils';
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth';
export enum ClientType {
@@ -140,16 +139,12 @@ export default class Session extends EventEmitterLike {
const [ [ device_info ], api_key ] = ytcfg;
const id = generateRandomString(11);
const timestamp = Math.floor(Date.now() / 1000);
const visitor_data = Proto.encodeVisitorData(id, timestamp);
const context: Context = {
client: {
hl: device_info[0],
gl: device_info[2],
remoteHost: device_info[3],
visitorData: visitor_data,
visitorData: data[3],
userAgent: device_info[14],
clientName: client_name,
clientVersion: device_info[16],

View File

@@ -4,12 +4,12 @@ import { AxioslikeResponse } from './Actions';
import { InnertubeError, MissingParamError, uuidv4 } from '../utils/Utils';
import { Constants } from '../utils';
export interface UploadResult {
interface UploadResult {
status: string;
scottyResourceId: string;
}
export interface InitialUploadData {
interface InitialUploadData {
frontend_upload_id: string;
upload_id: string;
upload_url: string;
@@ -18,6 +18,17 @@ export interface InitialUploadData {
}
export interface VideoMetadata {
title?: string;
description?: string;
tags?: string[];
category?: number;
license?: string;
age_restricted?: boolean;
made_for_kids?: boolean;
privacy?: 'PUBLIC' | 'PRIVATE' | 'UNLISTED';
}
export interface UploadedVideoMetadata {
title?: string;
description?: string;
privacy?: 'PUBLIC' | 'PRIVATE' | 'UNLISTED';
@@ -53,6 +64,31 @@ class Studio {
return response;
}
/**
* Updates given video's metadata.
* @example
* ```ts
* const response = await yt.studio.updateVideoMetadata('videoid', {
* tags: [ 'astronomy', 'NASA', 'APOD' ],
* title: 'Artemis Mission',
* description: 'A nicely written description...',
* category: 27,
* licence: 'creative_commons'
* // ...
* });
* ```
*/
async updateVideoMetadata(video_id: string, metadata: VideoMetadata) {
const payload = Proto.encodeVideoMetadataPayload(video_id, metadata);
const response = await this.#session.actions.execute('/video_manager/metadata_update', {
protobuf: true,
serialized_data: payload
});
return response;
}
/**
* Uploads a video to YouTube.
* @example
@@ -61,7 +97,7 @@ class Studio {
* const response = await yt.studio.upload(file.buffer, { title: 'Wow!' });
* ```
*/
async upload(file: BodyInit, metadata: VideoMetadata = {}): Promise<AxioslikeResponse> {
async upload(file: BodyInit, metadata: UploadedVideoMetadata = {}): Promise<AxioslikeResponse> {
const initial_data = await this.#getInitialUploadData();
const upload_result = await this.#uploadVideo(initial_data.upload_url, file);
@@ -128,7 +164,7 @@ class Studio {
return data;
}
async #setVideoMetadata(initial_data: InitialUploadData, upload_result: UploadResult, metadata: VideoMetadata) {
async #setVideoMetadata(initial_data: InitialUploadData, upload_result: UploadResult, metadata: UploadedVideoMetadata) {
const metadata_payload = {
resourceId: {
scottyResourceId: {

View File

@@ -1,6 +1,25 @@
# Parser
Sanitizes and standardizes InnerTube responses while maintaining the integrity of the data. Also [drastically improves](https://github.com/LuanRT/YouTube.js/blob/main/lib/parser/youtube/Library.js#L44) how API calls are made and handled.
Sanitizes and standardizes InnerTube responses while maintaining the integrity of the data. Also [drastically improves](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube/Library.ts#L69) how API calls are made and handled.
<ol>
<li>
<a href="#api">API</a>
</li>
<li>
<a href="#usage">Usage</a>
<ul>
<li><a href="#observedarray">ObservedArray</a></li>
<li><a href="#superparsedresponse">SuperParsedResponse</a></li>
<li><a href="#ytnode">YTNode</a></li>
<li><a href="#memo">Memo</a></li>
</ul>
</li>
<li><a href="#adding-new-nodes">Adding new nodes</a></li>
<li><a href="#how-it-works">How it works</a></li>
</ol>
___
## API
@@ -20,7 +39,7 @@ Responsible for parsing individual nodes.
| --- | --- | --- |
| data | `any` | The data |
| requireArray | `?boolean` | Whether the response should be an array |
| validTypes | `YTNodeConstructor<T> | YTNodeConstructor<T>[] | undefined` | The types of YTNodes are allowed |
| validTypes | `YTNodeConstructor<T> \| YTNodeConstructor<T>[] \| undefined` | Types of `YTNode` allowed |
When `requireArray` is `true`, the response will be an `ObservedArray<YTNodes>`.
@@ -43,6 +62,8 @@ Unlike `parse`, this can be used to parse the entire response object.
| --- | --- | --- |
| data | `object` | Raw InnerTube response |
## Usage
## ObservedArray
You may use `ObservedArray<T extends YTNode>` as a normal array, but it provides additional methods for typesafe access and casting.
@@ -58,13 +79,13 @@ const firstVideo = feed.firstOfType(GridVideo);
// We may cast the whole array to a GridVideo[] and throw if we have any non-GridVideo elements:
const allVideos = feed.as(GridVideo);
// There's some extra methods for ObservedArray<T extends YTNode>
// There are some extra methods for ObservedArray<T extends YTNode>
// which we use internally but not documented here (yet).
// see the source code for more details.
```
## SuperParsedResponse
Represents a parsed response in an unknown state. Either a `YTNode` or a `ObservedArray<YTNode>` or `null`.
Represents a parsed response in an unknown state. Either a `YTNode` or an `ObservedArray<YTNode>` or `null`.
You will need to assert the type and unwrap the response to get the actual value.
@@ -116,14 +137,14 @@ if (node.is(TwoColumnSearchResults, VideoList)) {
```
### Accessing properties without casting
Sometimes multiple nodes have the same properties and we don't want to check the type of the node before accessing the property, for example the property "contents" is used by many node types, and we may add more in the future, as such we want to only assert the property instead of casting to a specific type.
Sometimes multiple nodes have the same properties and we don't want to check the type of the node before accessing the property, for example, the property "contents" is used by many node types, and we may add more in the future, as such we want to only assert the property instead of casting to a specific type.
```ts
// Accesing a property on a node which you aren't sure if it exists.
// Accessing a property on a node which you aren't sure if it exists.
const prop = node.key("contents");
// This returns the value wrapped into a `Maybe` type
// which you can use to find the type of the value
// note however, this throws an error if the key doesn't exist
// note, however, this throws an error if the key doesn't exist
// we may want to check for the key before accessing it.
if (node.hasKey("contents")) {
const prop = node.key("contents");
@@ -146,7 +167,7 @@ if (prop.isInstanceof(Text)) {
});
}
// There's some special methods for using with the parser —
// There are some special methods for using with the parser —
// such as getting the value as a YTNode.
const prop = node.key("contents");
if (prop.isNode()) {
@@ -171,7 +192,7 @@ const prop = node.key("contents");
if (prop.isObserved()) {
const array = prop.observed();
// Now we may use the all the ObservedArray methods as normal,
// Now we may use all the ObservedArray methods as normal,
// like finding nodes of a certain type for example.
const results = array.filterType(GridVideo);
}
@@ -187,7 +208,7 @@ if (prop.isParsed()) {
const videos = results.filterType(Video);
}
// Sometimes we just want to debug something and not interested in finding the type.
// Sometimes we just want to debug something and are not interested in finding the type.
// This will, however, warn you when being used.
const prop = node.key("contents");
const value = prop.any();
@@ -200,7 +221,7 @@ if (prop.isArray()) {
// This will return Maybe[]
}
// Or if you want zero typesafety you can use the `array` method.
// Or if you want zero type safety you can use the `array` method.
const prop = node.key("contents");
if (prop.isArray()) {
const array = prop.array();
@@ -221,13 +242,16 @@ const videos = response.contents_memo.getType(Video);
`Memo` extends `Map<string, YTNode[]>` and can be used as a normal `Map` too if you want.
## Adding new nodes
Instructions can be found [here](https://github.com/LuanRT/YouTube.js/blob/main/docs/updating-the-parser.md).
## How it works
If you decompile a YouTube client and analize it for a while you will notice that it has classes named `protos/youtube/api/innertube/MusicItemRenderer`, `protos/youtube/api/innertube/SectionListRenderer`, etc.
If you decompile a YouTube client and analyze it for a while you will notice that it has classes named `protos/youtube/api/innertube/MusicItemRenderer`, `protos/youtube/api/innertube/SectionListRenderer`, etc.
These classes are used to parse objects from the response, map them into models and generate the UI. The website works in a similar way, the difference is that it uses plain JSON (likely converted from protobuf server-side, hence the weird structure of the response).
These classes are used to parse objects from the response, map them into models and generate the UI. The website works similarly, the difference is that it uses plain JSON (likely converted from protobuf server-side, hence the weird structure of the response).
Here we're taking a similar approach, the parser goes through all the renderers and parses their inner element(s). The final result is a nicely structured JSON, and on top of that it also parses navigation endpoints which allows us to make an API call with all required parameters in one line and even emulate client actions (eg; clicking a button).
Here we're taking a similar approach, the parser goes through all the renderers and parses their inner element(s). The final result is a nicely structured JSON, and on top of that, it also parses navigation endpoints which allow us to make an API call with all required parameters in one line and even emulate client actions (eg; clicking a button).
Here is your average, arguably ugly InnerTube response:
<details>

View File

@@ -230,12 +230,6 @@ class NavigationEndpoint extends YTNode {
params: data.sendLiveChatVoteEndpoint.params
};
}
if (data?.liveChatItemContextMenuEndpoint) {
this.live_chat_item_context_menu = {
params: data.liveChatItemContextMenuEndpoint.params
};
}
}
/**
@@ -249,6 +243,8 @@ class NavigationEndpoint extends YTNode {
return '/player';
case 'watchPlaylistEndpoint':
return '/next';
case 'liveChatItemContextMenuEndpoint':
return 'live_chat/get_item_context_menu';
}
}
@@ -297,6 +293,15 @@ class NavigationEndpoint extends YTNode {
const response = await actions.engage(this.metadata.api_url, { video_id: this.like.target.video_id, params: this.like.params });
return response;
}
if (this.live_chat_item_context_menu) {
if (!this.metadata.api_url)
throw new Error('Live Chat Item Context Menu endpoint requires an api_url, but was not parsed from the response.');
const response = await actions.livechat(this.metadata.api_url, {
params: this.live_chat_item_context_menu.params
});
return response;
}
}
async call(actions: Actions, client: string | undefined, parse: true) : Promise<ParsedResponse | undefined>;
@@ -307,7 +312,7 @@ class NavigationEndpoint extends YTNode {
if (parse && result)
return Parser.parseResponse(result.data);
return this.#call(actions, client);
return result;
}
}

View File

@@ -0,0 +1,14 @@
import { YTNode } from '../../helpers';
class RemoveChatItemAction extends YTNode {
static type = 'RemoveChatItemAction';
target_item_id: string;
constructor(data: any) {
super();
this.target_item_id = data.targetItemId;
}
}
export default RemoveChatItemAction;

View File

@@ -0,0 +1,14 @@
import { YTNode } from '../../helpers';
class RemoveChatItemByAuthorAction extends YTNode {
static type = 'RemoveChatItemByAuthorAction';
external_channel_id: string;
constructor(data: any) {
super();
this.external_channel_id = data.externalChannelId;
}
}
export default RemoveChatItemByAuthorAction;

View File

@@ -1,6 +1,8 @@
import Text from '../../misc/Text';
import Parser from '../../../index';
import { YTNode } from '../../../helpers';
import { ObservedArray, YTNode } from '../../../helpers';
import NavigationEndpoint from '../../NavigationEndpoint';
import Button from '../../Button';
class LiveChatAutoModMessage extends YTNode {
static type = 'LiveChatAutoModMessage';
@@ -8,12 +10,16 @@ class LiveChatAutoModMessage extends YTNode {
auto_moderated_item;
header_text: Text;
menu_endpoint?: NavigationEndpoint;
moderation_buttons: ObservedArray<Button>;
timestamp: number;
id: string;
constructor(data: any) {
super();
this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint);
this.moderation_buttons = Parser.parseArray<Button>(data.moderationButtons, [ Button ]);
this.auto_moderated_item = Parser.parse(data.autoModeratedItem);
this.header_text = new Text(data.headerText);
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);

View File

@@ -23,6 +23,7 @@ class LiveChatPaidSticker extends YTNode {
sticker: Thumbnail[];
purchase_amount: string;
context_menu: NavigationEndpoint;
menu_endpoint?: NavigationEndpoint;
timestamp: number;
constructor(data: any) {
@@ -42,7 +43,8 @@ class LiveChatPaidSticker extends YTNode {
this.author_name_text_color = data.authorNameTextColor;
this.sticker = Thumbnail.fromResponse(data.sticker);
this.purchase_amount = new Text(data.purchaseAmountText).toString();
this.context_menu = new NavigationEndpoint(data.contextMenuEndpoint);
this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint);
this.context_menu = this.menu_endpoint;
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);
}
}

View File

@@ -5,7 +5,8 @@ import MetadataBadge from '../../MetadataBadge';
import LiveChatAuthorBadge from '../../LiveChatAuthorBadge';
import Parser from '../../../index';
import { YTNode } from '../../../helpers';
import { ObservedArray, YTNode } from '../../../helpers';
import Button from '../../Button';
class LiveChatTextMessage extends YTNode {
static type = 'LiveChatTextMessage';
@@ -22,6 +23,7 @@ class LiveChatTextMessage extends YTNode {
};
menu_endpoint?: NavigationEndpoint;
inline_action_buttons: ObservedArray<Button>;
timestamp: number;
id: string;
@@ -47,6 +49,7 @@ class LiveChatTextMessage extends YTNode {
this.author.is_verified_artist = badges ? badges.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST') : null;
this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint);
this.inline_action_buttons = Parser.parseArray<Button>(data.inlineActionButtons, [ Button ]);
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);
this.id = data.id;
}

View File

@@ -0,0 +1,56 @@
import Text from '../../misc/Text';
import Thumbnail from '../../misc/Thumbnail';
import NavigationEndpoint from '../../NavigationEndpoint';
import MetadataBadge from '../../MetadataBadge';
import LiveChatAuthorBadge from '../../LiveChatAuthorBadge';
import Parser from '../../../index';
import { YTNode } from '../../../helpers';
class LiveChatTickerPaidStickerItem extends YTNode {
static type = 'LiveChatTickerPaidStickerItem';
author: {
id: string;
thumbnails: Thumbnail[];
badges: LiveChatAuthorBadge[] | MetadataBadge[];
is_moderator: boolean | null;
is_verified: boolean | null;
is_verified_artist: boolean | null;
};
amount: Text;
duration_sec: string; // Or number?
full_duration_sec: string;
show_item;
show_item_endpoint: NavigationEndpoint;
id: string;
constructor(data: any) {
super();
this.author = {
id: data.authorExternalChannelId,
thumbnails: Thumbnail.fromResponse(data.authorPhoto),
badges: Parser.parseArray<LiveChatAuthorBadge | MetadataBadge>(data.authorBadges, [ MetadataBadge, LiveChatAuthorBadge ]),
is_moderator: null,
is_verified: null,
is_verified_artist: null
};
const badges = Parser.parseArray<LiveChatAuthorBadge | MetadataBadge>(data.authorBadges, [ MetadataBadge, LiveChatAuthorBadge ]);
this.author.badges = badges;
this.author.is_moderator = badges?.some((badge) => badge.icon_type == 'MODERATOR') || null;
this.author.is_verified = badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED') || null;
this.author.is_verified_artist = badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST') || null;
this.amount = new Text(data.amount);
this.duration_sec = data.durationSec;
this.full_duration_sec = data.fullDurationSec;
this.show_item = Parser.parse(data.showItemEndpoint.showLiveChatItemEndpoint.renderer);
this.show_item_endpoint = new NavigationEndpoint(data.showItemEndpoint);
this.id = data.id;
}
}
export default LiveChatTickerPaidStickerItem;

View File

@@ -230,6 +230,13 @@ export default class Parser {
const actions_memo = this.#getMemo();
this.#clearMemo();
this.#createMemo();
const live_chat_item_context_menu_supported_renderers = data.liveChatItemContextMenuSupportedRenderers
? Parser.parseItem(data.liveChatItemContextMenuSupportedRenderers)
: null;
const live_chat_item_context_menu_supported_renderers_memo = this.#getMemo();
this.#clearMemo();
this.applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
return {
@@ -237,6 +244,8 @@ export default class Parser {
actions_memo,
contents,
contents_memo,
live_chat_item_context_menu_supported_renderers,
live_chat_item_context_menu_supported_renderers_memo,
on_response_received_actions,
on_response_received_actions_memo,
on_response_received_endpoints,
@@ -269,7 +278,7 @@ export default class Parser {
formats: Parser.parseFormats(data.streamingData.formats),
adaptive_formats: Parser.parseFormats(data.streamingData.adaptiveFormats),
dash_manifest_url: data.streamingData?.dashManifestUrl || null,
dls_manifest_url: data.streamingData?.dashManifestUrl || null
hls_manifest_url: data.streamingData?.hlsManifestUrl || null
} : undefined,
current_video_endpoint: data.currentVideoEndpoint ? new NavigationEndpoint(data.currentVideoEndpoint) : null,
captions: Parser.parseItem<PlayerCaptionsTracklist>(data.captions, PlayerCaptionsTracklist),
@@ -347,7 +356,7 @@ export default class Parser {
return result as T;
} catch (err) {
this.formatError({ classname, classdata: data[keys[0]], err });
this.printError({ classname, classdata: data[keys[0]], err });
return null;
}
}
@@ -436,12 +445,13 @@ export default class Parser {
}
}
static formatError({ classname, classdata, err }: { classname: string, classdata: any, err: any }) {
static printError({ classname, classdata, err }: { classname: string, classdata: any, err: any }) {
if (err.code == 'MODULE_NOT_FOUND') {
return console.warn(
new InnertubeError(
`${classname} not found!\n` +
`This is a bug, please report it at ${package_json.bugs.url}`, classdata)
`This is a bug, want to help us fix it? Follow the instructions at ${package_json.homepage.split('#')[0]}/blob/main/docs/updating-the-parser.md or report it at ${package_json.bugs.url}!`, classdata
)
);
}

View File

@@ -103,6 +103,7 @@ import { default as LiveChatPlaceholderItem } from './classes/livechat/items/Liv
import { default as LiveChatProductItem } from './classes/livechat/items/LiveChatProductItem';
import { default as LiveChatTextMessage } from './classes/livechat/items/LiveChatTextMessage';
import { default as LiveChatTickerPaidMessageItem } from './classes/livechat/items/LiveChatTickerPaidMessageItem';
import { default as LiveChatTickerPaidStickerItem } from './classes/livechat/items/LiveChatTickerPaidStickerItem';
import { default as LiveChatTickerSponsorItem } from './classes/livechat/items/LiveChatTickerSponsorItem';
import { default as LiveChatViewerEngagementMessage } from './classes/livechat/items/LiveChatViewerEngagementMessage';
import { default as PollHeader } from './classes/livechat/items/PollHeader';
@@ -110,6 +111,8 @@ import { default as LiveChatActionPanel } from './classes/livechat/LiveChatActio
import { default as MarkChatItemAsDeletedAction } from './classes/livechat/MarkChatItemAsDeletedAction';
import { default as MarkChatItemsByAuthorAsDeletedAction } from './classes/livechat/MarkChatItemsByAuthorAsDeletedAction';
import { default as RemoveBannerForLiveChatCommand } from './classes/livechat/RemoveBannerForLiveChatCommand';
import { default as RemoveChatItemAction } from './classes/livechat/RemoveChatItemAction';
import { default as RemoveChatItemByAuthorAction } from './classes/livechat/RemoveChatItemByAuthorAction';
import { default as ReplaceChatItemAction } from './classes/livechat/ReplaceChatItemAction';
import { default as ReplayChatItemAction } from './classes/livechat/ReplayChatItemAction';
import { default as ShowLiveChatActionPanelAction } from './classes/livechat/ShowLiveChatActionPanelAction';
@@ -378,6 +381,7 @@ const map: Record<string, YTNodeConstructor> = {
LiveChatProductItem,
LiveChatTextMessage,
LiveChatTickerPaidMessageItem,
LiveChatTickerPaidStickerItem,
LiveChatTickerSponsorItem,
LiveChatViewerEngagementMessage,
PollHeader,
@@ -385,6 +389,8 @@ const map: Record<string, YTNodeConstructor> = {
MarkChatItemAsDeletedAction,
MarkChatItemsByAuthorAsDeletedAction,
RemoveBannerForLiveChatCommand,
RemoveChatItemAction,
RemoveChatItemByAuthorAction,
ReplaceChatItemAction,
ReplayChatItemAction,
ShowLiveChatActionPanelAction,

View File

@@ -0,0 +1,65 @@
import Actions from '../../core/Actions';
import Menu from '../classes/menus/Menu';
import MenuServiceItem from '../classes/menus/MenuServiceItem';
import NavigationEndpoint from '../classes/NavigationEndpoint';
import Button from '../classes/Button';
import { ParsedResponse } from '..';
import { InnertubeError } from '../../utils/Utils';
import { ObservedArray, YTNode } from '../helpers';
class ItemMenu {
#page: ParsedResponse;
#actions: Actions;
#items: ObservedArray<YTNode>;
constructor(data: ParsedResponse, actions: Actions) {
this.#page = data;
this.#actions = actions;
const menu = data?.live_chat_item_context_menu_supported_renderers;
if (!menu || !menu.is(Menu))
throw new InnertubeError('Response did not have a "live_chat_item_context_menu_supported_renderers" property. The call may have failed.');
this.#items = menu.as(Menu).items;
}
async selectItem(icon_type: string): Promise<ParsedResponse>
async selectItem(button: Button): Promise<ParsedResponse>
async selectItem(item: string | Button): Promise<ParsedResponse> {
let endpoint: NavigationEndpoint;
if (item instanceof Button) {
endpoint = item.endpoint;
} else {
const button = this.#items.find((button) => {
if (!button.is(MenuServiceItem)) {
return false;
}
const menuServiceItem = button.as(MenuServiceItem);
return menuServiceItem.icon_type === item;
});
if (!button)
throw new InnertubeError(`Button "${item}" not found.`);
endpoint = button.as(MenuServiceItem).endpoint;
}
const response = await endpoint.callTest(this.#actions, { parse: true });
return response;
}
items(): ObservedArray<YTNode> {
return this.#items;
}
page(): ParsedResponse {
return this.#page;
}
}
export default ItemMenu;

View File

@@ -1,4 +1,4 @@
import Parser, { LiveChatContinuation } from '../index';
import Parser, { LiveChatContinuation, ParsedResponse } from '../index';
import EventEmitter from '../../utils/EventEmitterLike';
import VideoInfo from './VideoInfo';
@@ -20,14 +20,26 @@ import AddBannerToLiveChatCommand from '../classes/livechat/AddBannerToLiveChatC
import RemoveBannerForLiveChatCommand from '../classes/livechat/RemoveBannerForLiveChatCommand';
import ShowLiveChatTooltipCommand from '../classes/livechat/ShowLiveChatTooltipCommand';
import { InnertubeError } from '../../utils/Utils';
import Proto from '../../proto/index';
import { InnertubeError, uuidv4 } from '../../utils/Utils';
import { ObservedArray, YTNode } from '../helpers';
import LiveChatTextMessage from '../classes/livechat/items/LiveChatTextMessage';
import LiveChatPaidMessage from '../classes/livechat/items/LiveChatPaidMessage';
import LiveChatPaidSticker from '../classes/livechat/items/LiveChatPaidSticker';
import LiveChatAutoModMessage from '../classes/livechat/items/LiveChatAutoModMessage';
import LiveChatMembershipItem from '../classes/livechat/items/LiveChatMembershipItem';
import LiveChatViewerEngagementMessage from '../classes/livechat/items/LiveChatViewerEngagementMessage';
import ItemMenu from './ItemMenu';
import Button from '../classes/Button';
export type ChatAction =
AddChatItemAction | AddBannerToLiveChatCommand | AddLiveChatTickerItemAction |
MarkChatItemAsDeletedAction | MarkChatItemsByAuthorAsDeletedAction | RemoveBannerForLiveChatCommand |
ReplaceChatItemAction | ReplayChatItemAction | ShowLiveChatActionPanelAction | ShowLiveChatTooltipCommand;
export type ChatItemWithMenu = LiveChatAutoModMessage | LiveChatMembershipItem | LiveChatPaidMessage | LiveChatPaidSticker | LiveChatTextMessage | LiveChatViewerEngagementMessage;
export interface LiveMetadata {
title: UpdateTitleAction | undefined;
description: UpdateDescriptionAction | undefined;
@@ -42,9 +54,6 @@ class LiveChat extends EventEmitter {
#continuation;
#mcontinuation?: string;
#lc_polling_interval_ms = 1000;
#md_polling_interval_ms = 5000;
initial_info?: LiveChatContinuation;
metadata?: LiveMetadata;
@@ -73,111 +82,130 @@ class LiveChat extends EventEmitter {
}
#pollLivechat() {
const lc_poller = setTimeout(() => {
(async () => {
const endpoint = this.is_replay ? 'live_chat/get_live_chat_replay' : 'live_chat/get_live_chat';
const response = await this.#actions.livechat(endpoint, { ctoken: this.#continuation });
(async () => {
const endpoint = this.is_replay ? 'live_chat/get_live_chat_replay' : 'live_chat/get_live_chat';
const response = await this.#actions.execute(endpoint, { continuation: this.#continuation });
const data = Parser.parseResponse(response.data);
const contents = data.continuation_contents;
const data = Parser.parseResponse(response.data);
const contents = data.continuation_contents;
if (!(contents instanceof LiveChatContinuation))
throw new InnertubeError('Continuation is not a LiveChatContinuation');
if (!(contents instanceof LiveChatContinuation))
throw new InnertubeError('Continuation is not a LiveChatContinuation');
this.#continuation = contents.continuation.token;
this.#lc_polling_interval_ms = contents.continuation.timeout_ms;
this.#continuation = contents.continuation.token;
// Header only exists in the first request
if (contents.header) {
this.initial_info = contents;
this.emit('start', contents);
} else {
await this.#emitSmoothedActions(contents.actions);
}
// Header only exists in the first request
if (contents.header) {
this.initial_info = contents;
this.emit('start', contents);
} else {
await this.#emitSmoothedActions(contents.actions);
}
clearTimeout(lc_poller);
// If there are no actions then we wait 1000 milliseconds, otherwise
// The amount of items on the action queue will determine the polling interval.
if (!contents.actions.length && !contents.header)
await this.#wait(1000);
this.running && this.#pollLivechat();
})().catch((err) => Promise.reject(err));
}, this.#lc_polling_interval_ms);
if (this.running)
this.#pollLivechat();
})();
}
/**
* Ensures actions are emitted at the right speed.
* This was adapted from YouTube's compiled code (Android).
* This was adapted from YouTube's compiled code (Android & Web).
*/
async #emitSmoothedActions(actions: ObservedArray<YTNode>) {
async #emitSmoothedActions(action_queue: YTNode[]) {
const base = 1E4;
let delay = actions.length < base / 80 ? 1 : 0;
let delay = action_queue.length < base / 80 ? 1 : Math.ceil(action_queue.length / (base / 80));
const emit_delay_ms =
delay == 1 ? (
delay = base / actions.length,
delay = base / action_queue.length,
delay *= Math.random() + 0.5,
delay = Math.min(1E3, delay),
delay = Math.max(80, delay)
) : delay = 80;
for (const action of actions) {
for (const action of action_queue) {
await this.#wait(emit_delay_ms);
this.emit('chat-update', action);
}
}
#pollMetadata() {
const md_poller = setTimeout(() => {
(async () => {
const payload = {
video_id: this.#video_info.basic_info.id,
ctoken: undefined as string | undefined
};
(async () => {
const payload: {
videoId: string | undefined;
continuation?: string;
} = { videoId: this.#video_info.basic_info.id };
if (this.#mcontinuation) {
payload.ctoken = this.#mcontinuation;
}
if (this.#mcontinuation) {
payload.continuation = this.#mcontinuation;
}
const response = await this.#actions.livechat('updated_metadata', payload);
const data = Parser.parseResponse(response.data);
const response = await this.#actions.execute('/updated_metadata', payload);
const data = Parser.parseResponse(response.data);
this.#mcontinuation = data.continuation?.token;
this.#md_polling_interval_ms = data.continuation?.timeout_ms || this.#md_polling_interval_ms;
this.#mcontinuation = data.continuation?.token;
this.metadata = {
title: data.actions?.array().firstOfType(UpdateTitleAction) || this.metadata?.title,
description: data.actions?.array().firstOfType(UpdateDescriptionAction) || this.metadata?.description,
views: data.actions?.array().firstOfType(UpdateViewershipAction) || this.metadata?.views,
likes: data.actions?.array().firstOfType(UpdateToggleButtonTextAction) || this.metadata?.likes,
date: data.actions?.array().firstOfType(UpdateDateTextAction) || this.metadata?.date
};
this.metadata = {
title: data.actions?.array().firstOfType(UpdateTitleAction) || this.metadata?.title,
description: data.actions?.array().firstOfType(UpdateDescriptionAction) || this.metadata?.description,
views: data.actions?.array().firstOfType(UpdateViewershipAction) || this.metadata?.views,
likes: data.actions?.array().firstOfType(UpdateToggleButtonTextAction) || this.metadata?.likes,
date: data.actions?.array().firstOfType(UpdateDateTextAction) || this.metadata?.date
};
this.emit('metadata-update', this.metadata);
this.emit('metadata-update', this.metadata);
clearTimeout(md_poller);
await this.#wait(5000);
this.running && this.#pollMetadata();
})().catch((err) => Promise.reject(err));
}, this.#md_polling_interval_ms);
if (this.running)
this.#pollMetadata();
})();
}
/**
* Sends a message.
*/
async sendMessage(text: string): Promise<ObservedArray<AddChatItemAction>> {
const response = await this.#actions.livechat('live_chat/send_message', {
text,
...{
video_id: this.#video_info.basic_info.id,
channel_id: this.#video_info.basic_info.channel_id
}
const response = await this.#actions.execute('/live_chat/send_message', {
params: Proto.encodeMessageParams(this.#video_info.basic_info.channel_id as string, this.#video_info.basic_info.id as string),
richMessage: { textSegments: [ { text } ] },
clientMessageId: uuidv4(),
parse: true
});
const data = Parser.parseResponse(response.data);
if (!data.actions)
if (!response.actions)
throw new InnertubeError('Response did not have an "actions" property. The call may have failed.');
return data.actions.array().as(AddChatItemAction);
return response.actions.array().as(AddChatItemAction);
}
/**
* Retrieves given chat item's menu.
*/
async getItemMenu(item: ChatItemWithMenu): Promise<ItemMenu> {
if (!item.menu_endpoint)
throw new InnertubeError('This item does not have a menu.', item);
const response = await item.menu_endpoint.call(this.#actions, undefined, true);
if (!response)
throw new InnertubeError('Could not retrieve item menu.', item);
return new ItemMenu(response, this.#actions);
}
/**
* Equivalent to "clicking" a button.
*/
async selectButton(button: Button): Promise<ParsedResponse> {
const response = await button.endpoint.callTest(this.#actions, { parse: true });
return response;
}
async #wait(ms: number) {

View File

@@ -1,17 +1,20 @@
import Parser, { GridContinuation, MusicShelfContinuation, ParsedResponse, PlaylistPanelContinuation, SectionListContinuation } from '..';
import Actions from '../../core/Actions';
import { InnertubeError } from '../../utils/Utils';
import DropdownItem from '../classes/DropdownItem';
import NavigationEndpoint from '../classes/NavigationEndpoint';
import PlaylistPanel from '../classes/PlaylistPanel';
import SectionList from '../classes/SectionList';
type ContentType = 'history' | 'playlists' | 'albums' | 'songs' | 'artists' | 'subscriptions';
type Continuation = {
type: 'browse' | 'next';
token: string,
payload?: {}
};
type ItemFilter = ((item: any) => boolean) | null;
type SortBy = 'recently_added' | 'a_z' | 'z_a';

View File

@@ -1,22 +1,9 @@
import { CLIENTS } from '../utils/Constants';
import { u8ToBase64 } from '../utils/Utils';
import { ChannelAnalytics, CreateCommentParams, CreateCommentReplyParams, GetCommentsSectionParams, InnertubePayload, LiveMessageParams, MusicSearchFilter, NotificationPreferences, PeformCommentActionParams, SearchFilter, SoundInfoParams, VisitorData } from './youtube';
import { VideoMetadata } from '../core/Studio';
import { ChannelAnalytics, CreateCommentParams, CreateCommentReplyParams, GetCommentsSectionParams, InnertubePayload, LiveMessageParams, MusicSearchFilter, NotificationPreferences, PeformCommentActionParams, SearchFilter, SoundInfoParams } from './youtube';
class Proto {
/**
* Encodes visitor data.
*/
static encodeVisitorData(id: string, timestamp: number) {
const buf = VisitorData.toBinary({
id,
timestamp
});
return encodeURIComponent(u8ToBase64(buf).replace(/\/|\+/g, '_'));
}
/**
* Encodes basic channel analytics parameters.
*/
static encodeChannelAnalyticsParams(channel_id: string) {
const buf = ChannelAnalytics.toBinary({
params: {
@@ -26,9 +13,6 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes search filters.
*/
static encodeSearchFilters(filters: {
upload_date?: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year',
type?: 'all' | 'video' | 'channel' | 'playlist' | 'movie',
@@ -98,9 +82,6 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes YouTube Music search filters.
*/
static encodeMusicSearchFilters(filters: {
type?: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist'
}) {
@@ -118,9 +99,6 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes livechat message parameters.
*/
static encodeMessageParams(channel_id: string, video_id: string) {
const buf = LiveMessageParams.toBinary({
params: {
@@ -134,9 +112,6 @@ class Proto {
return btoa(encodeURIComponent(u8ToBase64(buf)));
}
/**
* Encodes comment section parameters.
*/
static encodeCommentsSectionParams(video_id: string, options: {
type?: number,
sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'
@@ -164,9 +139,6 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes comment replies parameters.
*/
static encodeCommentRepliesParams(video_id: string, comment_id: string) {
const buf = GetCommentsSectionParams.toBinary({
ctx: {
@@ -189,9 +161,6 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes comment parameters.
*/
static encodeCommentParams(video_id: string) {
const buf = CreateCommentParams.toBinary({
videoId: video_id,
@@ -203,9 +172,6 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes comment reply parameters.
*/
static encodeCommentReplyParams(comment_id: string, video_id: string) {
const buf = CreateCommentReplyParams.toBinary({
videoId: video_id,
@@ -218,9 +184,6 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes comment action parameters.
*/
static encodeCommentActionParams(type: number, args: {
comment_id?: string,
video_id?: string,
@@ -253,9 +216,6 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes notification preference parameters.
*/
static encodeNotificationPref(channel_id: string, index: number) {
const buf = NotificationPreferences.toBinary({
channelId: channel_id,
@@ -268,9 +228,68 @@ class Proto {
return encodeURIComponent(u8ToBase64(buf));
}
/**
* Encodes a custom thumbnail payload.
*/
static encodeVideoMetadataPayload(video_id: string, metadata: VideoMetadata) {
const data: InnertubePayload = {
context: {
client: {
unkparam: 14,
clientName: CLIENTS.ANDROID.NAME,
clientVersion: CLIENTS.ANDROID.VERSION
}
},
target: video_id
};
if (Reflect.has(metadata, 'title'))
data.title = { text: metadata.title || '' };
if (Reflect.has(metadata, 'description'))
data.description = { text: metadata.description || '' };
if (Reflect.has(metadata, 'license'))
data.license = { type: metadata.license || '' };
if (Reflect.has(metadata, 'tags'))
data.tags = { list: metadata.tags || [] };
if (Reflect.has(metadata, 'category'))
data.category = { id: metadata.category || 0 };
if (Reflect.has(metadata, 'privacy')) {
switch (metadata.privacy) {
case 'PUBLIC':
data.privacy = { type: 1 };
break;
case 'UNLISTED':
data.privacy = { type: 2 };
break;
case 'PRIVATE':
data.privacy = { type: 3 };
break;
default:
throw new Error('Invalid visibility option');
}
}
if (Reflect.has(metadata, 'made_for_kids')) {
data.madeForKids = {
unkparam: 1,
choice: metadata.made_for_kids ? 1 : 2
};
}
if (Reflect.has(metadata, 'age_restricted')) {
data.ageRestricted = {
unkparam: 1,
choice: metadata.age_restricted ? 1 : 2
};
}
const buf = InnertubePayload.toBinary(data);
return buf;
}
static encodeCustomThumbnailPayload(video_id: string, bytes: Uint8Array) {
const data: InnertubePayload = {
context: {
@@ -281,7 +300,7 @@ class Proto {
}
},
target: video_id,
videoSettings: {
videoThumbnail: {
type: 3,
thumbnail: {
imageData: bytes
@@ -290,12 +309,10 @@ class Proto {
};
const buf = InnertubePayload.toBinary(data);
return buf;
}
/**
* Encodes sound info parameters.
*/
static encodeSoundInfoParams(id: string) {
const data: SoundInfoParams = {
sound: {

View File

@@ -1,11 +1,8 @@
// TODO: clean this up
syntax = "proto2";
package youtube;
message VisitorData {
required string id = 1;
required int32 timestamp = 5;
}
message ChannelAnalytics {
message Params {
required string channel_id = 1001;
@@ -29,8 +26,59 @@ message InnertubePayload {
// This can be either a target id or a video id.
optional string target = 2;
// Note: I'm not entirely sure this message is only used for video settings
message VideoSettings {
/**** YT Sudio stuff ****/
message Title {
required string text = 1;
}
optional Title title = 3;
message Description {
required string text = 1;
}
optional Description description = 4;
message Tags {
repeated string list = 1;
}
optional Tags tags = 6;
message Privacy {
required int32 type = 1;
}
optional Privacy privacy = 38;
message Category {
required int32 id = 1;
}
optional Category category = 7;
message MadeForKids {
required int32 unkparam = 1;
required int32 choice = 2;
}
optional MadeForKids made_for_kids = 68;
message AgeRestricted {
required int32 unkparam = 1;
required int32 choice = 2;
}
optional AgeRestricted age_restricted = 69;
message License {
required string type = 1;
}
optional License license = 8;
message VideoThumbnail {
required int32 type = 1; // is this something else?
message Thumbnail {
@@ -40,7 +88,7 @@ message InnertubePayload {
required Thumbnail thumbnail = 3;
}
optional VideoSettings video_settings = 20;
optional VideoThumbnail video_thumbnail = 20;
}
message SoundInfoParams {

View File

@@ -1,6 +1,9 @@
// @generated by protobuf-ts 2.7.0
// @generated by protobuf-ts 2.8.1
// @generated from protobuf file "youtube.proto" (package "youtube", syntax proto2)
// tslint:disable
//
// TODO: clean this up
//
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
import type { IBinaryWriter } from "@protobuf-ts/runtime";
import { WireType } from "@protobuf-ts/runtime";
@@ -11,19 +14,6 @@ import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
/**
* @generated from protobuf message youtube.VisitorData
*/
export interface VisitorData {
/**
* @generated from protobuf field: string id = 1;
*/
id: string;
/**
* @generated from protobuf field: int32 timestamp = 5;
*/
timestamp: number;
}
/**
* @generated from protobuf message youtube.ChannelAnalytics
*/
@@ -57,9 +47,41 @@ export interface InnertubePayload {
*/
target?: string;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.VideoSettings video_settings = 20;
* @generated from protobuf field: optional youtube.InnertubePayload.Title title = 3;
*/
videoSettings?: InnertubePayload_VideoSettings;
title?: InnertubePayload_Title;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.Description description = 4;
*/
description?: InnertubePayload_Description;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.Tags tags = 6;
*/
tags?: InnertubePayload_Tags;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.Privacy privacy = 38;
*/
privacy?: InnertubePayload_Privacy;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.Category category = 7;
*/
category?: InnertubePayload_Category;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.MadeForKids made_for_kids = 68;
*/
madeForKids?: InnertubePayload_MadeForKids;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.AgeRestricted age_restricted = 69;
*/
ageRestricted?: InnertubePayload_AgeRestricted;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.License license = 8;
*/
license?: InnertubePayload_License;
/**
* @generated from protobuf field: optional youtube.InnertubePayload.VideoThumbnail video_thumbnail = 20;
*/
videoThumbnail?: InnertubePayload_VideoThumbnail;
}
/**
* @generated from protobuf message youtube.InnertubePayload.Context
@@ -87,25 +109,105 @@ export interface InnertubePayload_Context_Client {
*/
clientName: string;
}
// *** YT Sudio stuff ***
/**
* Note: I'm not entirely sure this message is only used for video settings
*
* @generated from protobuf message youtube.InnertubePayload.VideoSettings
* @generated from protobuf message youtube.InnertubePayload.Title
*/
export interface InnertubePayload_VideoSettings {
export interface InnertubePayload_Title {
/**
* @generated from protobuf field: string text = 1;
*/
text: string;
}
/**
* @generated from protobuf message youtube.InnertubePayload.Description
*/
export interface InnertubePayload_Description {
/**
* @generated from protobuf field: string text = 1;
*/
text: string;
}
/**
* @generated from protobuf message youtube.InnertubePayload.Tags
*/
export interface InnertubePayload_Tags {
/**
* @generated from protobuf field: repeated string list = 1;
*/
list: string[];
}
/**
* @generated from protobuf message youtube.InnertubePayload.Privacy
*/
export interface InnertubePayload_Privacy {
/**
* @generated from protobuf field: int32 type = 1;
*/
type: number;
}
/**
* @generated from protobuf message youtube.InnertubePayload.Category
*/
export interface InnertubePayload_Category {
/**
* @generated from protobuf field: int32 id = 1;
*/
id: number;
}
/**
* @generated from protobuf message youtube.InnertubePayload.MadeForKids
*/
export interface InnertubePayload_MadeForKids {
/**
* @generated from protobuf field: int32 unkparam = 1;
*/
unkparam: number;
/**
* @generated from protobuf field: int32 choice = 2;
*/
choice: number;
}
/**
* @generated from protobuf message youtube.InnertubePayload.AgeRestricted
*/
export interface InnertubePayload_AgeRestricted {
/**
* @generated from protobuf field: int32 unkparam = 1;
*/
unkparam: number;
/**
* @generated from protobuf field: int32 choice = 2;
*/
choice: number;
}
/**
* @generated from protobuf message youtube.InnertubePayload.License
*/
export interface InnertubePayload_License {
/**
* @generated from protobuf field: string type = 1;
*/
type: string;
}
/**
* @generated from protobuf message youtube.InnertubePayload.VideoThumbnail
*/
export interface InnertubePayload_VideoThumbnail {
/**
* @generated from protobuf field: int32 type = 1;
*/
type: number; // is this something else?
/**
* @generated from protobuf field: youtube.InnertubePayload.VideoSettings.Thumbnail thumbnail = 3;
* @generated from protobuf field: youtube.InnertubePayload.VideoThumbnail.Thumbnail thumbnail = 3;
*/
thumbnail?: InnertubePayload_VideoSettings_Thumbnail;
thumbnail?: InnertubePayload_VideoThumbnail_Thumbnail;
}
/**
* @generated from protobuf message youtube.InnertubePayload.VideoSettings.Thumbnail
* @generated from protobuf message youtube.InnertubePayload.VideoThumbnail.Thumbnail
*/
export interface InnertubePayload_VideoSettings_Thumbnail {
export interface InnertubePayload_VideoThumbnail_Thumbnail {
/**
* @generated from protobuf field: bytes image_data = 1;
*/
@@ -530,60 +632,6 @@ export interface SearchFilter_Filters {
duration?: number;
}
// @generated message type with reflection information, may provide speed optimized methods
class VisitorData$Type extends MessageType<VisitorData> {
constructor() {
super("youtube.VisitorData", [
{ no: 1, name: "id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 5, name: "timestamp", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
]);
}
create(value?: PartialMessage<VisitorData>): VisitorData {
const message = { id: "", timestamp: 0 };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<VisitorData>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: VisitorData): VisitorData {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string id */ 1:
message.id = reader.string();
break;
case /* int32 timestamp */ 5:
message.timestamp = reader.int32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: VisitorData, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string id = 1; */
if (message.id !== "")
writer.tag(1, WireType.LengthDelimited).string(message.id);
/* int32 timestamp = 5; */
if (message.timestamp !== 0)
writer.tag(5, WireType.Varint).int32(message.timestamp);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.VisitorData
*/
export const VisitorData = new VisitorData$Type();
// @generated message type with reflection information, may provide speed optimized methods
class ChannelAnalytics$Type extends MessageType<ChannelAnalytics> {
constructor() {
super("youtube.ChannelAnalytics", [
@@ -683,7 +731,15 @@ class InnertubePayload$Type extends MessageType<InnertubePayload> {
super("youtube.InnertubePayload", [
{ no: 1, name: "context", kind: "message", T: () => InnertubePayload_Context },
{ no: 2, name: "target", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
{ no: 20, name: "video_settings", kind: "message", T: () => InnertubePayload_VideoSettings }
{ no: 3, name: "title", kind: "message", T: () => InnertubePayload_Title },
{ no: 4, name: "description", kind: "message", T: () => InnertubePayload_Description },
{ no: 6, name: "tags", kind: "message", T: () => InnertubePayload_Tags },
{ no: 38, name: "privacy", kind: "message", T: () => InnertubePayload_Privacy },
{ no: 7, name: "category", kind: "message", T: () => InnertubePayload_Category },
{ no: 68, name: "made_for_kids", kind: "message", T: () => InnertubePayload_MadeForKids },
{ no: 69, name: "age_restricted", kind: "message", T: () => InnertubePayload_AgeRestricted },
{ no: 8, name: "license", kind: "message", T: () => InnertubePayload_License },
{ no: 20, name: "video_thumbnail", kind: "message", T: () => InnertubePayload_VideoThumbnail }
]);
}
create(value?: PartialMessage<InnertubePayload>): InnertubePayload {
@@ -704,8 +760,32 @@ class InnertubePayload$Type extends MessageType<InnertubePayload> {
case /* optional string target */ 2:
message.target = reader.string();
break;
case /* optional youtube.InnertubePayload.VideoSettings video_settings */ 20:
message.videoSettings = InnertubePayload_VideoSettings.internalBinaryRead(reader, reader.uint32(), options, message.videoSettings);
case /* optional youtube.InnertubePayload.Title title */ 3:
message.title = InnertubePayload_Title.internalBinaryRead(reader, reader.uint32(), options, message.title);
break;
case /* optional youtube.InnertubePayload.Description description */ 4:
message.description = InnertubePayload_Description.internalBinaryRead(reader, reader.uint32(), options, message.description);
break;
case /* optional youtube.InnertubePayload.Tags tags */ 6:
message.tags = InnertubePayload_Tags.internalBinaryRead(reader, reader.uint32(), options, message.tags);
break;
case /* optional youtube.InnertubePayload.Privacy privacy */ 38:
message.privacy = InnertubePayload_Privacy.internalBinaryRead(reader, reader.uint32(), options, message.privacy);
break;
case /* optional youtube.InnertubePayload.Category category */ 7:
message.category = InnertubePayload_Category.internalBinaryRead(reader, reader.uint32(), options, message.category);
break;
case /* optional youtube.InnertubePayload.MadeForKids made_for_kids */ 68:
message.madeForKids = InnertubePayload_MadeForKids.internalBinaryRead(reader, reader.uint32(), options, message.madeForKids);
break;
case /* optional youtube.InnertubePayload.AgeRestricted age_restricted */ 69:
message.ageRestricted = InnertubePayload_AgeRestricted.internalBinaryRead(reader, reader.uint32(), options, message.ageRestricted);
break;
case /* optional youtube.InnertubePayload.License license */ 8:
message.license = InnertubePayload_License.internalBinaryRead(reader, reader.uint32(), options, message.license);
break;
case /* optional youtube.InnertubePayload.VideoThumbnail video_thumbnail */ 20:
message.videoThumbnail = InnertubePayload_VideoThumbnail.internalBinaryRead(reader, reader.uint32(), options, message.videoThumbnail);
break;
default:
let u = options.readUnknownField;
@@ -725,9 +805,33 @@ class InnertubePayload$Type extends MessageType<InnertubePayload> {
/* optional string target = 2; */
if (message.target !== undefined)
writer.tag(2, WireType.LengthDelimited).string(message.target);
/* optional youtube.InnertubePayload.VideoSettings video_settings = 20; */
if (message.videoSettings)
InnertubePayload_VideoSettings.internalBinaryWrite(message.videoSettings, writer.tag(20, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.Title title = 3; */
if (message.title)
InnertubePayload_Title.internalBinaryWrite(message.title, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.Description description = 4; */
if (message.description)
InnertubePayload_Description.internalBinaryWrite(message.description, writer.tag(4, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.Tags tags = 6; */
if (message.tags)
InnertubePayload_Tags.internalBinaryWrite(message.tags, writer.tag(6, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.Privacy privacy = 38; */
if (message.privacy)
InnertubePayload_Privacy.internalBinaryWrite(message.privacy, writer.tag(38, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.Category category = 7; */
if (message.category)
InnertubePayload_Category.internalBinaryWrite(message.category, writer.tag(7, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.MadeForKids made_for_kids = 68; */
if (message.madeForKids)
InnertubePayload_MadeForKids.internalBinaryWrite(message.madeForKids, writer.tag(68, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.AgeRestricted age_restricted = 69; */
if (message.ageRestricted)
InnertubePayload_AgeRestricted.internalBinaryWrite(message.ageRestricted, writer.tag(69, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.License license = 8; */
if (message.license)
InnertubePayload_License.internalBinaryWrite(message.license, writer.tag(8, WireType.LengthDelimited).fork(), options).join();
/* optional youtube.InnertubePayload.VideoThumbnail video_thumbnail = 20; */
if (message.videoThumbnail)
InnertubePayload_VideoThumbnail.internalBinaryWrite(message.videoThumbnail, writer.tag(20, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -847,30 +951,26 @@ class InnertubePayload_Context_Client$Type extends MessageType<InnertubePayload_
*/
export const InnertubePayload_Context_Client = new InnertubePayload_Context_Client$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_VideoSettings$Type extends MessageType<InnertubePayload_VideoSettings> {
class InnertubePayload_Title$Type extends MessageType<InnertubePayload_Title> {
constructor() {
super("youtube.InnertubePayload.VideoSettings", [
{ no: 1, name: "type", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 3, name: "thumbnail", kind: "message", T: () => InnertubePayload_VideoSettings_Thumbnail }
super("youtube.InnertubePayload.Title", [
{ no: 1, name: "text", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_VideoSettings>): InnertubePayload_VideoSettings {
const message = { type: 0 };
create(value?: PartialMessage<InnertubePayload_Title>): InnertubePayload_Title {
const message = { text: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_VideoSettings>(this, message, value);
reflectionMergePartial<InnertubePayload_Title>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_VideoSettings): InnertubePayload_VideoSettings {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_Title): InnertubePayload_Title {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 type */ 1:
message.type = reader.int32();
break;
case /* youtube.InnertubePayload.VideoSettings.Thumbnail thumbnail */ 3:
message.thumbnail = InnertubePayload_VideoSettings_Thumbnail.internalBinaryRead(reader, reader.uint32(), options, message.thumbnail);
case /* string text */ 1:
message.text = reader.string();
break;
default:
let u = options.readUnknownField;
@@ -883,13 +983,10 @@ class InnertubePayload_VideoSettings$Type extends MessageType<InnertubePayload_V
}
return message;
}
internalBinaryWrite(message: InnertubePayload_VideoSettings, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int32 type = 1; */
if (message.type !== 0)
writer.tag(1, WireType.Varint).int32(message.type);
/* youtube.InnertubePayload.VideoSettings.Thumbnail thumbnail = 3; */
if (message.thumbnail)
InnertubePayload_VideoSettings_Thumbnail.internalBinaryWrite(message.thumbnail, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
internalBinaryWrite(message: InnertubePayload_Title, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string text = 1; */
if (message.text !== "")
writer.tag(1, WireType.LengthDelimited).string(message.text);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -897,24 +994,421 @@ class InnertubePayload_VideoSettings$Type extends MessageType<InnertubePayload_V
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.VideoSettings
* @generated MessageType for protobuf message youtube.InnertubePayload.Title
*/
export const InnertubePayload_VideoSettings = new InnertubePayload_VideoSettings$Type();
export const InnertubePayload_Title = new InnertubePayload_Title$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_VideoSettings_Thumbnail$Type extends MessageType<InnertubePayload_VideoSettings_Thumbnail> {
class InnertubePayload_Description$Type extends MessageType<InnertubePayload_Description> {
constructor() {
super("youtube.InnertubePayload.VideoSettings.Thumbnail", [
super("youtube.InnertubePayload.Description", [
{ no: 1, name: "text", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_Description>): InnertubePayload_Description {
const message = { text: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_Description>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_Description): InnertubePayload_Description {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string text */ 1:
message.text = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InnertubePayload_Description, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string text = 1; */
if (message.text !== "")
writer.tag(1, WireType.LengthDelimited).string(message.text);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.Description
*/
export const InnertubePayload_Description = new InnertubePayload_Description$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_Tags$Type extends MessageType<InnertubePayload_Tags> {
constructor() {
super("youtube.InnertubePayload.Tags", [
{ no: 1, name: "list", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_Tags>): InnertubePayload_Tags {
const message = { list: [] };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_Tags>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_Tags): InnertubePayload_Tags {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* repeated string list */ 1:
message.list.push(reader.string());
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InnertubePayload_Tags, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated string list = 1; */
for (let i = 0; i < message.list.length; i++)
writer.tag(1, WireType.LengthDelimited).string(message.list[i]);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.Tags
*/
export const InnertubePayload_Tags = new InnertubePayload_Tags$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_Privacy$Type extends MessageType<InnertubePayload_Privacy> {
constructor() {
super("youtube.InnertubePayload.Privacy", [
{ no: 1, name: "type", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_Privacy>): InnertubePayload_Privacy {
const message = { type: 0 };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_Privacy>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_Privacy): InnertubePayload_Privacy {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 type */ 1:
message.type = reader.int32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InnertubePayload_Privacy, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int32 type = 1; */
if (message.type !== 0)
writer.tag(1, WireType.Varint).int32(message.type);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.Privacy
*/
export const InnertubePayload_Privacy = new InnertubePayload_Privacy$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_Category$Type extends MessageType<InnertubePayload_Category> {
constructor() {
super("youtube.InnertubePayload.Category", [
{ no: 1, name: "id", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_Category>): InnertubePayload_Category {
const message = { id: 0 };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_Category>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_Category): InnertubePayload_Category {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 id */ 1:
message.id = reader.int32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InnertubePayload_Category, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int32 id = 1; */
if (message.id !== 0)
writer.tag(1, WireType.Varint).int32(message.id);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.Category
*/
export const InnertubePayload_Category = new InnertubePayload_Category$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_MadeForKids$Type extends MessageType<InnertubePayload_MadeForKids> {
constructor() {
super("youtube.InnertubePayload.MadeForKids", [
{ no: 1, name: "unkparam", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 2, name: "choice", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_MadeForKids>): InnertubePayload_MadeForKids {
const message = { unkparam: 0, choice: 0 };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_MadeForKids>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_MadeForKids): InnertubePayload_MadeForKids {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 unkparam */ 1:
message.unkparam = reader.int32();
break;
case /* int32 choice */ 2:
message.choice = reader.int32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InnertubePayload_MadeForKids, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int32 unkparam = 1; */
if (message.unkparam !== 0)
writer.tag(1, WireType.Varint).int32(message.unkparam);
/* int32 choice = 2; */
if (message.choice !== 0)
writer.tag(2, WireType.Varint).int32(message.choice);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.MadeForKids
*/
export const InnertubePayload_MadeForKids = new InnertubePayload_MadeForKids$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_AgeRestricted$Type extends MessageType<InnertubePayload_AgeRestricted> {
constructor() {
super("youtube.InnertubePayload.AgeRestricted", [
{ no: 1, name: "unkparam", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 2, name: "choice", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_AgeRestricted>): InnertubePayload_AgeRestricted {
const message = { unkparam: 0, choice: 0 };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_AgeRestricted>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_AgeRestricted): InnertubePayload_AgeRestricted {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 unkparam */ 1:
message.unkparam = reader.int32();
break;
case /* int32 choice */ 2:
message.choice = reader.int32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InnertubePayload_AgeRestricted, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int32 unkparam = 1; */
if (message.unkparam !== 0)
writer.tag(1, WireType.Varint).int32(message.unkparam);
/* int32 choice = 2; */
if (message.choice !== 0)
writer.tag(2, WireType.Varint).int32(message.choice);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.AgeRestricted
*/
export const InnertubePayload_AgeRestricted = new InnertubePayload_AgeRestricted$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_License$Type extends MessageType<InnertubePayload_License> {
constructor() {
super("youtube.InnertubePayload.License", [
{ no: 1, name: "type", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_License>): InnertubePayload_License {
const message = { type: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_License>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_License): InnertubePayload_License {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string type */ 1:
message.type = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InnertubePayload_License, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string type = 1; */
if (message.type !== "")
writer.tag(1, WireType.LengthDelimited).string(message.type);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.License
*/
export const InnertubePayload_License = new InnertubePayload_License$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_VideoThumbnail$Type extends MessageType<InnertubePayload_VideoThumbnail> {
constructor() {
super("youtube.InnertubePayload.VideoThumbnail", [
{ no: 1, name: "type", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 3, name: "thumbnail", kind: "message", T: () => InnertubePayload_VideoThumbnail_Thumbnail }
]);
}
create(value?: PartialMessage<InnertubePayload_VideoThumbnail>): InnertubePayload_VideoThumbnail {
const message = { type: 0 };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_VideoThumbnail>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_VideoThumbnail): InnertubePayload_VideoThumbnail {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 type */ 1:
message.type = reader.int32();
break;
case /* youtube.InnertubePayload.VideoThumbnail.Thumbnail thumbnail */ 3:
message.thumbnail = InnertubePayload_VideoThumbnail_Thumbnail.internalBinaryRead(reader, reader.uint32(), options, message.thumbnail);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: InnertubePayload_VideoThumbnail, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int32 type = 1; */
if (message.type !== 0)
writer.tag(1, WireType.Varint).int32(message.type);
/* youtube.InnertubePayload.VideoThumbnail.Thumbnail thumbnail = 3; */
if (message.thumbnail)
InnertubePayload_VideoThumbnail_Thumbnail.internalBinaryWrite(message.thumbnail, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.VideoThumbnail
*/
export const InnertubePayload_VideoThumbnail = new InnertubePayload_VideoThumbnail$Type();
// @generated message type with reflection information, may provide speed optimized methods
class InnertubePayload_VideoThumbnail_Thumbnail$Type extends MessageType<InnertubePayload_VideoThumbnail_Thumbnail> {
constructor() {
super("youtube.InnertubePayload.VideoThumbnail.Thumbnail", [
{ no: 1, name: "image_data", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<InnertubePayload_VideoSettings_Thumbnail>): InnertubePayload_VideoSettings_Thumbnail {
create(value?: PartialMessage<InnertubePayload_VideoThumbnail_Thumbnail>): InnertubePayload_VideoThumbnail_Thumbnail {
const message = { imageData: new Uint8Array(0) };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<InnertubePayload_VideoSettings_Thumbnail>(this, message, value);
reflectionMergePartial<InnertubePayload_VideoThumbnail_Thumbnail>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_VideoSettings_Thumbnail): InnertubePayload_VideoSettings_Thumbnail {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: InnertubePayload_VideoThumbnail_Thumbnail): InnertubePayload_VideoThumbnail_Thumbnail {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
@@ -933,7 +1427,7 @@ class InnertubePayload_VideoSettings_Thumbnail$Type extends MessageType<Innertub
}
return message;
}
internalBinaryWrite(message: InnertubePayload_VideoSettings_Thumbnail, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
internalBinaryWrite(message: InnertubePayload_VideoThumbnail_Thumbnail, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bytes image_data = 1; */
if (message.imageData.length)
writer.tag(1, WireType.LengthDelimited).bytes(message.imageData);
@@ -944,9 +1438,9 @@ class InnertubePayload_VideoSettings_Thumbnail$Type extends MessageType<Innertub
}
}
/**
* @generated MessageType for protobuf message youtube.InnertubePayload.VideoSettings.Thumbnail
* @generated MessageType for protobuf message youtube.InnertubePayload.VideoThumbnail.Thumbnail
*/
export const InnertubePayload_VideoSettings_Thumbnail = new InnertubePayload_VideoSettings_Thumbnail$Type();
export const InnertubePayload_VideoThumbnail_Thumbnail = new InnertubePayload_VideoThumbnail_Thumbnail$Type();
// @generated message type with reflection information, may provide speed optimized methods
class SoundInfoParams$Type extends MessageType<SoundInfoParams> {
constructor() {

View File

@@ -10,29 +10,46 @@ describe('YouTube.js Tests', () => {
yt = await Innertube.create();
});
describe('Info', () => {
let info: any;
it('should retrieve full video info', async () => {
info = await yt.getInfo(VIDEOS[0].ID);
expect(info.basic_info.id).toBe(VIDEOS[0].ID);
});
it('should have captions on full video info', async () => {
expect(info.captions?.caption_tracks.length).toBeGreaterThan(0);
});
it('should retrieve basic video info', async () => {
const b_info = await yt.getBasicInfo(VIDEOS[0].ID);
expect(b_info.basic_info.id).toBe(VIDEOS[0].ID);
});
});
describe('Search', () => {
let search: any;
it('should search', async () => {
const search = await yt.search(VIDEOS[0].QUERY);
expect(search.results?.length).toBeLessThanOrEqual(35);
expect(search.videos.length).toBeLessThanOrEqual(35);
expect(search.playlists.length).toBeLessThanOrEqual(35);
expect(search.channels.length).toBeLessThanOrEqual(35);
search = await yt.search(VIDEOS[0].QUERY);
expect(search.results.length).toBeGreaterThanOrEqual(5);
expect(search.playlists).toBeDefined();
expect(search.channels).toBeDefined();
expect(search.has_continuation).toBe(true);
});
it('should retrieve search continuation', async () => {
const search = await yt.search(VIDEOS[0].QUERY);
const next = await search.getContinuation()
expect(next.results?.length).toBeLessThanOrEqual(35);
expect(next.videos.length).toBeLessThanOrEqual(35);
expect(next.playlists.length).toBeLessThanOrEqual(35);
expect(next.channels.length).toBeLessThanOrEqual(35);
expect(next.has_continuation).toBe(true);
const next = await search.getContinuation();
expect(next.results.length).toBeGreaterThanOrEqual(5);
expect(search.playlists).toBeDefined();
expect(search.channels).toBeDefined();
expect(search.has_continuation).toBe(true);
});
it('should retrieve search suggestions', async () => {
const suggestions = await yt.getSearchSuggestions(VIDEOS[0].QUERY);
expect(suggestions.length).toBeLessThanOrEqual(10);
expect(suggestions.length).toBeGreaterThanOrEqual(1);
});
});
@@ -58,23 +75,6 @@ describe('YouTube.js Tests', () => {
expect(thread.replies.length).toBeLessThanOrEqual(10);
});
});
describe('Info', () => {
it('should retrieve full video info', async () => {
const info = await yt.getInfo(VIDEOS[0].ID);
expect(info.basic_info.id).toBe(VIDEOS[0].ID);
});
it('should have captions on full video info', async () => {
const info = await yt.getInfo(VIDEOS[0].ID);
expect(info.captions?.caption_tracks.length).toBeGreaterThan(0);
});
it('should retrieve basic video info', async () => {
const info = await yt.getBasicInfo(VIDEOS[0].ID);
expect(info.basic_info.id).toBe(VIDEOS[0].ID);
});
});
describe('General', () => {
it('should retrieve playlist', async () => {