mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc14d3785f | ||
|
|
088f909515 | ||
|
|
2a78d77aa3 | ||
|
|
1b2862c00f | ||
|
|
477c030084 | ||
|
|
19d579df13 | ||
|
|
5313c57783 | ||
|
|
190f7681be | ||
|
|
6e027bcc85 | ||
|
|
6b531dd0ea | ||
|
|
92f24076db | ||
|
|
a9eba7ca62 | ||
|
|
2f56c15ecc | ||
|
|
95e0479745 | ||
|
|
556c7cd6e8 | ||
|
|
a4a88419ef | ||
|
|
aefecd061e | ||
|
|
7485726f1e | ||
|
|
9e703abe3a | ||
|
|
affbe84284 | ||
|
|
fcbdae3e34 | ||
|
|
059c858021 | ||
|
|
4ecd3360e0 | ||
|
|
08e9527931 | ||
|
|
a9f03a1523 | ||
|
|
c8980c7985 | ||
|
|
2e5688f235 | ||
|
|
dcf2b720a0 | ||
|
|
a90f5eb853 | ||
|
|
c6482e07b9 | ||
|
|
2de77c8f2c | ||
|
|
2aaa209906 |
@@ -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
|
||||
|
||||
18
README.md
18
README.md
@@ -42,7 +42,9 @@
|
||||
[][codefactor]
|
||||
[][npm]
|
||||
[][say-thanks]
|
||||
|
||||
<br>
|
||||
[][github-sponsors]
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SPONSORS -->
|
||||
@@ -292,7 +294,7 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The id of the video |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID` or `YTMUSIC` |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC`, `YTMUSIC_ANDROID` or `TV_EMBEDDED` |
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
@@ -347,7 +349,7 @@ Suitable for cases where you only need basic video metadata. Also, it is faster
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The id of the video |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID` or `YTMUSIC` |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC_ANDROID`, `YTMUSIC`, `TV_EMBEDDED` |
|
||||
|
||||
<a name="search"></a>
|
||||
### search(query, filters?)
|
||||
@@ -527,6 +529,14 @@ Retrieves playlist contents.
|
||||
### getStreamingData(video_id, options)
|
||||
Returns deciphered streaming data.
|
||||
|
||||
**Note:**
|
||||
It is recommended to retrieve streaming data from a `VideoInfo`/`TrackInfo` object instead if you want to select formats manually, example:
|
||||
```ts
|
||||
const info = await yt.getBasicInfo('somevideoid');
|
||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
console.info('Playback url:', url);
|
||||
```
|
||||
|
||||
**Returns**: `Promise.<object>`
|
||||
|
||||
| Param | Type | Description |
|
||||
@@ -569,7 +579,7 @@ For example, you may want to call an endpoint directly, that can be achieved wit
|
||||
|
||||
const payload = {
|
||||
videoId: 'jLTOuvBTLxA',
|
||||
client: 'YTMUSIC', // InnerTube client, can be ANDROID, YTMUSIC, WEB
|
||||
client: 'YTMUSIC', // InnerTube client, can be ANDROID, YTMUSIC, YTMUSIC_ANDROID, WEB or TV_EMBEDDED
|
||||
parse: true // tells YouTube.js to parse the response, this is not sent to InnerTube.
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -41,7 +41,7 @@ InnerTube API key.
|
||||
**Returns:** `string`
|
||||
|
||||
<a name="api_version"></a>
|
||||
### key
|
||||
### api_version
|
||||
|
||||
InnerTube API version.
|
||||
|
||||
@@ -80,4 +80,4 @@ Player script object.
|
||||
|
||||
Client language.
|
||||
|
||||
**Returns:** `string`
|
||||
**Returns:** `string`
|
||||
|
||||
@@ -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 |
|
||||
54
docs/updating-the-parser.md
Normal file
54
docs/updating-the-parser.md
Normal 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!
|
||||
@@ -6,17 +6,52 @@ YouTube.js works in the browser!
|
||||
|
||||
To use YouTube.js in the browser you must proxy requests through your own server. You can see our simple reference implementation in Deno in `examples/browser/proxy/deno.ts`.
|
||||
|
||||
Once the proxy is set up you need to tell Innertube about it when instantiating it.
|
||||
We'll use our own fetch implementation to proxy requests through our server. This is a simple example, but you can use any fetch implementation you want.
|
||||
|
||||
```ts
|
||||
import { Innertube } from "youtubei.js/build/browser";
|
||||
|
||||
const yt = await Innertube.create({
|
||||
browser_proxy: {
|
||||
host: "localhost",
|
||||
schema: 'http',
|
||||
}
|
||||
})
|
||||
fetch: async (input, init) => {
|
||||
// url
|
||||
const url = typeof input === 'string'
|
||||
? new URL(input)
|
||||
: input instanceof URL
|
||||
? input
|
||||
: new URL(input.url);
|
||||
|
||||
// transform the url for use with our proxy
|
||||
url.searchParams.set('__host', url.host);
|
||||
url.host = 'localhost:8080';
|
||||
url.protocol = 'http';
|
||||
|
||||
const headers = init?.headers
|
||||
? new Headers(init.headers)
|
||||
: input instanceof Request
|
||||
? input.headers
|
||||
: new Headers();
|
||||
|
||||
// now serialize the headers
|
||||
url.searchParams.set('__headers', JSON.stringify([...headers]));
|
||||
|
||||
// copy over the request
|
||||
const request = new Request(
|
||||
url,
|
||||
input instanceof Request ? input : undefined,
|
||||
);
|
||||
|
||||
headers.delete('user-agent');
|
||||
|
||||
// fetch the url
|
||||
return fetch(request, init ? {
|
||||
...init,
|
||||
headers
|
||||
} : {
|
||||
headers
|
||||
});
|
||||
},
|
||||
cache: new UniversalCache(),
|
||||
});
|
||||
```
|
||||
|
||||
after that you can use the library as normal.
|
||||
|
||||
@@ -18,7 +18,7 @@ const handler = async (request: Request): Promise<Response> => {
|
||||
'Access-Control-Allow-Origin': request.headers.get('origin') || '*',
|
||||
'Access-Control-Allow-Methods': '*',
|
||||
'Access-Control-Allow-Headers':
|
||||
'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-origin, x-youtube-client-version, Accept-Language, Range',
|
||||
'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-origin, x-youtube-client-version, Accept-Language, Range, Referer',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
}),
|
||||
@@ -45,7 +45,7 @@ const handler = async (request: Request): Promise<Response> => {
|
||||
JSON.parse(url.searchParams.get('__headers') || '{}'),
|
||||
);
|
||||
copyHeader('range', request_headers, request.headers);
|
||||
copyHeader('user-agent', request_headers, request.headers);
|
||||
!request_headers.has('user-agent') && copyHeader('user-agent', request_headers, request.headers);
|
||||
url.searchParams.delete('__headers');
|
||||
|
||||
// Make the request to YouTube
|
||||
@@ -62,6 +62,8 @@ const handler = async (request: Request): Promise<Response> => {
|
||||
copyHeader('content-length', headers, fetchRes.headers);
|
||||
copyHeader('content-type', headers, fetchRes.headers);
|
||||
copyHeader('content-disposition', headers, fetchRes.headers);
|
||||
copyHeader('accept-ranges', headers, fetchRes.headers);
|
||||
copyHeader('content-range', headers, fetchRes.headers);
|
||||
|
||||
// add cors headers
|
||||
headers.set(
|
||||
|
||||
@@ -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.
|
||||
|
||||
5
index.ts
5
index.ts
@@ -10,6 +10,11 @@ if (getRuntime() === 'node') {
|
||||
Reflect.set(globalThis, 'Response', undici.Response);
|
||||
Reflect.set(globalThis, 'FormData', undici.FormData);
|
||||
Reflect.set(globalThis, 'File', undici.File);
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
const { ReadableStream } = require('node:stream/web');
|
||||
Reflect.set(globalThis, 'ReadableStream', ReadableStream);
|
||||
} catch { /* do nothing */ }
|
||||
}
|
||||
|
||||
import Innertube from './src/Innertube';
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.1.0",
|
||||
"version": "2.3.2"
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "2.1.0",
|
||||
"version": "2.3.2",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.7.0",
|
||||
"jintr": "^0.2.0",
|
||||
"jintr": "^0.3.1",
|
||||
"linkedom": "^0.14.12",
|
||||
"undici": "^5.7.0"
|
||||
},
|
||||
@@ -3960,9 +3960,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jintr": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.2.0.tgz",
|
||||
"integrity": "sha512-b1TmzzCnVivT6ftmRFtvT/pPu/t/pCDH/GA8yzXKfOGOVG6X/pmrRU9vdWgV1I/EJTKt/i/cvx6MfmGMjlhWMA==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.3.1.tgz",
|
||||
"integrity": "sha512-AUcq8fKL4BE9jDx8TizZmJ9UOvk1CHKFW0nQcWaOaqk9tkLS9S10fNmusTWGEYTncn7U43nXrCbhYko/ylqrSg==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
@@ -8180,9 +8180,9 @@
|
||||
}
|
||||
},
|
||||
"jintr": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.2.0.tgz",
|
||||
"integrity": "sha512-b1TmzzCnVivT6ftmRFtvT/pPu/t/pCDH/GA8yzXKfOGOVG6X/pmrRU9vdWgV1I/EJTKt/i/cvx6MfmGMjlhWMA==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.3.1.tgz",
|
||||
"integrity": "sha512-AUcq8fKL4BE9jDx8TizZmJ9UOvk1CHKFW0nQcWaOaqk9tkLS9S10fNmusTWGEYTncn7U43nXrCbhYko/ylqrSg==",
|
||||
"requires": {
|
||||
"acorn": "^8.8.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.1.0",
|
||||
"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",
|
||||
@@ -39,7 +40,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.7.0",
|
||||
"jintr": "^0.2.0",
|
||||
"jintr": "^0.3.1",
|
||||
"linkedom": "^0.14.12",
|
||||
"undici": "^5.7.0"
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface SearchFilters {
|
||||
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count';
|
||||
}
|
||||
|
||||
export type InnerTubeClient = 'ANDROID' | 'YTMUSIC_ANDROID' | 'WEB' | 'YTMUSIC';
|
||||
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'TV_EMBEDDED';
|
||||
|
||||
class Innertube {
|
||||
session;
|
||||
|
||||
@@ -64,11 +64,11 @@ class Actions {
|
||||
/**
|
||||
* Mimmics the Axios API using Fetch's Response object.
|
||||
*/
|
||||
async #wrap(response: Response, protobuf?: boolean) {
|
||||
async #wrap(response: Response) {
|
||||
return {
|
||||
success: response.ok,
|
||||
status_code: response.status,
|
||||
data: protobuf ? await response.text() : JSON.parse(await response.text())
|
||||
data: JSON.parse(await response.text())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -776,7 +775,7 @@ class Actions {
|
||||
return Parser.parseResponse(await response.json());
|
||||
}
|
||||
|
||||
return this.#wrap(response, args.protobuf);
|
||||
return this.#wrap(response);
|
||||
}
|
||||
|
||||
#needsLogin(id: string) {
|
||||
|
||||
@@ -38,7 +38,7 @@ class Music {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrives track info. Passing a list item of type MusicTwoRowItem automatically starts a radio.
|
||||
* Retrieves track info. Passing a list item of type MusicTwoRowItem automatically starts a radio.
|
||||
* @param target - video id or a list item.
|
||||
*/
|
||||
getInfo(target: string | MusicTwoRowItem): Promise<TrackInfo> {
|
||||
|
||||
@@ -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 {
|
||||
@@ -25,6 +24,7 @@ export interface Context {
|
||||
userAgent: string;
|
||||
clientName: string;
|
||||
clientVersion: string;
|
||||
clientScreen?: string,
|
||||
androidSdkVersion?: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
@@ -42,6 +42,9 @@ export interface Context {
|
||||
user: {
|
||||
lockedSafetyMode: false;
|
||||
};
|
||||
thirdParty?: {
|
||||
embedUrl: string;
|
||||
};
|
||||
request: {
|
||||
useSsl: true;
|
||||
};
|
||||
@@ -136,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],
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -89,7 +110,7 @@ const is_null = response.is_null;
|
||||
## YTNode
|
||||
All renderers returned by InnerTube are converted to this generic class and then extended for the specific renderers.
|
||||
|
||||
This class is allows us a typesafe way to use data returned by the InnerTube API.
|
||||
This class is what allows us a typesafe way to use data returned by the InnerTube API.
|
||||
|
||||
Here's how to use this class to access returned data:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,8 +6,8 @@ class Card extends YTNode {
|
||||
|
||||
teaser;
|
||||
content;
|
||||
card_id: string;
|
||||
feature: string;
|
||||
card_id: string | null;
|
||||
feature: string | null;
|
||||
|
||||
cue_ranges: {
|
||||
start_card_active_ms: string;
|
||||
@@ -18,10 +18,10 @@ class Card extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.teaser = Parser.parse(data.teaser);
|
||||
this.content = Parser.parse(data.content);
|
||||
this.card_id = data.cardId;
|
||||
this.feature = data.feature;
|
||||
this.teaser = Parser.parseItem(data.teaser);
|
||||
this.content = Parser.parseItem(data.content);
|
||||
this.card_id = data.cardId || null;
|
||||
this.feature = data.feature || null;
|
||||
|
||||
this.cue_ranges = data.cueRanges.map((cr: any) => ({
|
||||
start_card_active_ms: cr.startCardActiveMs,
|
||||
@@ -32,4 +32,4 @@ class Card extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
export default Card;
|
||||
export default Card;
|
||||
|
||||
24
src/parser/classes/ConfirmDialog.ts
Normal file
24
src/parser/classes/ConfirmDialog.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Parser from '..';
|
||||
import Text from './misc/Text';
|
||||
import Button from './Button';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class ConfirmDialog extends YTNode {
|
||||
static type = 'ConfirmDialog';
|
||||
|
||||
title: Text;
|
||||
confirm_button: Button | null;
|
||||
cancel_button: Button | null;
|
||||
dialog_messages: Text[];
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.confirm_button = Parser.parseItem<Button>(data.confirmButton, Button);
|
||||
this.cancel_button = Parser.parseItem<Button>(data.cancelButton, Button);
|
||||
this.dialog_messages = data.dialogMessages.map((txt: any) => new Text(txt));
|
||||
}
|
||||
}
|
||||
|
||||
export default ConfirmDialog;
|
||||
15
src/parser/classes/MetadataScreen.ts
Normal file
15
src/parser/classes/MetadataScreen.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Parser from '..';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MetadataScreen extends YTNode {
|
||||
static type = 'MetadataScreen';
|
||||
|
||||
section_list;
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.section_list = Parser.parseItem(data);
|
||||
}
|
||||
}
|
||||
|
||||
export default MetadataScreen;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import Button from './Button';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class PlayerErrorMessage extends YTNode {
|
||||
@@ -8,17 +10,17 @@ class PlayerErrorMessage extends YTNode {
|
||||
|
||||
subreason: Text;
|
||||
reason: Text;
|
||||
proceed_button;
|
||||
proceed_button: Button | null;
|
||||
thumbnails: Thumbnail[];
|
||||
icon_type: string;
|
||||
icon_type: string | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.subreason = new Text(data.subreason);
|
||||
this.reason = new Text(data.reason);
|
||||
this.proceed_button = Parser.parse(data.proceedButton);
|
||||
this.proceed_button = Parser.parseItem<Button>(data.proceedButton, Button);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.icon_type = data.icon.iconType;
|
||||
this.icon_type = data.icon?.iconType || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class ProfileColumnStats extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.items = Parser.parse(data.items);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
|
||||
// XXX: alias for consistency
|
||||
@@ -17,4 +17,4 @@ class ProfileColumnStats extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileColumnStats;
|
||||
export default ProfileColumnStats;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import SubscriptionNotificationToggleButton from './SubscriptionNotificationToggleButton';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class SubscribeButton extends YTNode {
|
||||
@@ -14,7 +15,7 @@ class SubscribeButton extends YTNode {
|
||||
show_preferences: boolean;
|
||||
subscribed_text: Text;
|
||||
unsubscribed_text: Text;
|
||||
notification_preference_button;
|
||||
notification_preference_button: SubscriptionNotificationToggleButton | null;
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
@@ -27,7 +28,7 @@ class SubscribeButton extends YTNode {
|
||||
this.show_preferences = data.showPreferences;
|
||||
this.subscribed_text = new Text(data.subscribedButtonText);
|
||||
this.unsubscribed_text = new Text(data.unsubscribedButtonText);
|
||||
this.notification_preference_button = Parser.parse(data.notificationPreferenceButton);
|
||||
this.notification_preference_button = Parser.parseItem<SubscriptionNotificationToggleButton>(data.notificationPreferenceButton, SubscriptionNotificationToggleButton);
|
||||
this.endpoint = new NavigationEndpoint(data.serviceEndpoints?.[0] || data.onSubscribeEndpoints?.[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import Author from './misc/Author';
|
||||
import Menu from './menus/Menu';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import { timeToSeconds } from '../../utils/Utils';
|
||||
@@ -53,8 +54,8 @@ class Video extends YTNode {
|
||||
})) || [];
|
||||
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
|
||||
this.rich_thumbnail = data.richThumbnail && Parser.parse(data.richThumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.rich_thumbnail = data.richThumbnail ? Parser.parse(data.richThumbnail) : null;
|
||||
this.author = new Author(data.ownerText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
@@ -73,7 +74,7 @@ class Video extends YTNode {
|
||||
|
||||
this.show_action_menu = data.showActionMenu;
|
||||
this.is_watched = data.isWatched || false;
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import Button from './Button';
|
||||
import VideoOwner from './VideoOwner';
|
||||
import SubscribeButton from './SubscribeButton';
|
||||
import MetadataRowContainer from './MetadataRowContainer';
|
||||
import { YTNode } from '../helpers';
|
||||
@@ -8,7 +9,7 @@ import { YTNode } from '../helpers';
|
||||
class VideoSecondaryInfo extends YTNode {
|
||||
static type = 'VideoSecondaryInfo';
|
||||
|
||||
owner; // TODO: VideoOwner?
|
||||
owner: VideoOwner | null;// TODO: VideoOwner?
|
||||
description: Text;
|
||||
subscribe_button;
|
||||
metadata: MetadataRowContainer | null;
|
||||
@@ -19,7 +20,7 @@ class VideoSecondaryInfo extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.owner = Parser.parse(data.owner);
|
||||
this.owner = Parser.parseItem<VideoOwner>(data.owner);
|
||||
this.description = new Text(data.description);
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton | Button>(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
this.metadata = Parser.parseItem<MetadataRowContainer>(data.metadataRowContainer, MetadataRowContainer);
|
||||
|
||||
14
src/parser/classes/livechat/RemoveChatItemAction.ts
Normal file
14
src/parser/classes/livechat/RemoveChatItemAction.ts
Normal 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;
|
||||
14
src/parser/classes/livechat/RemoveChatItemByAuthorAction.ts
Normal file
14
src/parser/classes/livechat/RemoveChatItemByAuthorAction.ts
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
@@ -259,7 +268,7 @@ export default class Parser {
|
||||
} : null,
|
||||
playability_status: data.playabilityStatus ? {
|
||||
status: data.playabilityStatus.status as string,
|
||||
error_screen: Parser.parse(data.playabilityStatus.errorScreen),
|
||||
error_screen: Parser.parseItem(data.playabilityStatus.errorScreen),
|
||||
audio_only_playablility: Parser.parseItem<AudioOnlyPlayability>(data.playabilityStatus.audioOnlyPlayability, AudioOnlyPlayability),
|
||||
embeddable: !!data.playabilityStatus.playableInEmbed || false,
|
||||
reason: data.playabilityStatus?.reason || ''
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import { default as CompactLink } from './classes/CompactLink';
|
||||
import { default as CompactMix } from './classes/CompactMix';
|
||||
import { default as CompactPlaylist } from './classes/CompactPlaylist';
|
||||
import { default as CompactVideo } from './classes/CompactVideo';
|
||||
import { default as ConfirmDialog } from './classes/ConfirmDialog';
|
||||
import { default as ContinuationItem } from './classes/ContinuationItem';
|
||||
import { default as CopyLink } from './classes/CopyLink';
|
||||
import { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog';
|
||||
@@ -102,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';
|
||||
@@ -109,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';
|
||||
@@ -143,6 +147,7 @@ import { default as MetadataBadge } from './classes/MetadataBadge';
|
||||
import { default as MetadataRow } from './classes/MetadataRow';
|
||||
import { default as MetadataRowContainer } from './classes/MetadataRowContainer';
|
||||
import { default as MetadataRowHeader } from './classes/MetadataRowHeader';
|
||||
import { default as MetadataScreen } from './classes/MetadataScreen';
|
||||
import { default as MicroformatData } from './classes/MicroformatData';
|
||||
import { default as Mix } from './classes/Mix';
|
||||
import { default as Movie } from './classes/Movie';
|
||||
@@ -328,6 +333,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
CompactMix,
|
||||
CompactPlaylist,
|
||||
CompactVideo,
|
||||
ConfirmDialog,
|
||||
ContinuationItem,
|
||||
CopyLink,
|
||||
CreatePlaylistDialog,
|
||||
@@ -375,6 +381,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
LiveChatProductItem,
|
||||
LiveChatTextMessage,
|
||||
LiveChatTickerPaidMessageItem,
|
||||
LiveChatTickerPaidStickerItem,
|
||||
LiveChatTickerSponsorItem,
|
||||
LiveChatViewerEngagementMessage,
|
||||
PollHeader,
|
||||
@@ -382,6 +389,8 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MarkChatItemAsDeletedAction,
|
||||
MarkChatItemsByAuthorAsDeletedAction,
|
||||
RemoveBannerForLiveChatCommand,
|
||||
RemoveChatItemAction,
|
||||
RemoveChatItemByAuthorAction,
|
||||
ReplaceChatItemAction,
|
||||
ReplayChatItemAction,
|
||||
ShowLiveChatActionPanelAction,
|
||||
@@ -416,6 +425,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MetadataRow,
|
||||
MetadataRowContainer,
|
||||
MetadataRowHeader,
|
||||
MetadataScreen,
|
||||
MicroformatData,
|
||||
Mix,
|
||||
Movie,
|
||||
|
||||
65
src/parser/youtube/ItemMenu.ts
Normal file
65
src/parser/youtube/ItemMenu.ts
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,7 +24,7 @@ class Search extends Feed {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const contents =
|
||||
this.page.contents?.item().as(TwoColumnSearchResults).primary_contents.item().as(SectionList).contents.array() ||
|
||||
this.page.contents?.item().as(TwoColumnSearchResults).primary_contents?.item().as(SectionList).contents.array() ||
|
||||
this.page.on_response_received_commands?.[0].contents;
|
||||
|
||||
const secondary_contents_maybe = this.page.contents?.item().key('secondary_contents');
|
||||
@@ -59,7 +59,7 @@ class Search extends Feed {
|
||||
if (typeof card === 'string') {
|
||||
target_card = this.refinement_cards.cards.get({ query: card });
|
||||
if (!target_card)
|
||||
throw new InnertubeError('Refinement card not found!', { available_cards: this.refinement_card_queries });
|
||||
throw new InnertubeError(`Refinement card "${card}" not found`, { available_cards: this.refinement_card_queries });
|
||||
} else if (card.type === 'SearchRefinementCard') {
|
||||
target_card = card;
|
||||
} else {
|
||||
|
||||
@@ -46,9 +46,9 @@ export interface FormatOptions {
|
||||
*/
|
||||
format?: string;
|
||||
/**
|
||||
* InnerTube client, can be ANDROID, WEB or YTMUSIC
|
||||
* InnerTube client, can be ANDROID, WEB, YTMUSIC, YTMUSIC_ANDROID or TV_EMBEDDED
|
||||
*/
|
||||
client?: 'ANDROID' | 'WEB' | 'YTMUSIC'
|
||||
client?: 'ANDROID' | 'WEB' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'TV_EMBEDDED'
|
||||
}
|
||||
|
||||
export interface DownloadOptions extends FormatOptions {
|
||||
@@ -229,7 +229,8 @@ class VideoInfo {
|
||||
* Likes the video.
|
||||
*/
|
||||
async like() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ button_id: 'TOGGLE_BUTTON_ID_TYPE_LIKE' })?.as(ToggleButton);
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const button = segmented_like_dislike_button?.like_button?.as(ToggleButton);
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('Like button not found', { video_id: this.basic_info.id });
|
||||
@@ -246,7 +247,8 @@ class VideoInfo {
|
||||
* Dislikes the video.
|
||||
*/
|
||||
async dislike() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ button_id: 'TOGGLE_BUTTON_ID_TYPE_DISLIKE' })?.as(ToggleButton);
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const button = segmented_like_dislike_button?.dislike_button?.as(ToggleButton);
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('Dislike button not found', { video_id: this.basic_info.id });
|
||||
@@ -263,7 +265,17 @@ class VideoInfo {
|
||||
* Removes like/dislike.
|
||||
*/
|
||||
async removeLike() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ is_toggled: true })?.as(ToggleButton);
|
||||
let button;
|
||||
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const like_button = segmented_like_dislike_button?.like_button?.as(ToggleButton);
|
||||
const dislike_button = segmented_like_dislike_button?.dislike_button?.as(ToggleButton);
|
||||
|
||||
if (like_button?.is_toggled) {
|
||||
button = like_button;
|
||||
} else if (dislike_button?.is_toggled) {
|
||||
button = dislike_button;
|
||||
}
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('This video is not liked/disliked', { video_id: this.basic_info.id });
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// @generated by protobuf-ts 2.8.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() {
|
||||
|
||||
@@ -6,7 +6,8 @@ export const URLS = Object.freeze({
|
||||
YT_UPLOAD: 'https://upload.youtube.com/',
|
||||
API: Object.freeze({
|
||||
BASE: 'https://youtubei.googleapis.com',
|
||||
PRODUCTION: 'https://youtubei.googleapis.com/youtubei/',
|
||||
PRODUCTION_1: 'https://www.youtube.com/youtubei/',
|
||||
PRODUCTION_2: 'https://youtubei.googleapis.com/youtubei/',
|
||||
STAGING: 'https://green-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
RELEASE: 'https://release-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
TEST: 'https://test-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
@@ -48,6 +49,10 @@ export const CLIENTS = Object.freeze({
|
||||
YTMUSIC_ANDROID: {
|
||||
NAME: 'ANDROID_MUSIC',
|
||||
VERSION: '5.17.51'
|
||||
},
|
||||
TV_EMBEDDED: {
|
||||
NAME: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
|
||||
VERSION: '2.0'
|
||||
}
|
||||
});
|
||||
export const STREAM_HEADERS = Object.freeze({
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class HTTPClient {
|
||||
input: URL | Request | string,
|
||||
init?: RequestInit & HTTPClientInit
|
||||
) {
|
||||
const innertube_url = Constants.URLS.API.PRODUCTION + this.#session.api_version;
|
||||
const innertube_url = Constants.URLS.API.PRODUCTION_1 + this.#session.api_version;
|
||||
const baseURL = init?.baseURL || innertube_url;
|
||||
|
||||
const request_url =
|
||||
@@ -143,6 +143,12 @@ export default class HTTPClient {
|
||||
ctx.client.clientName = Constants.CLIENTS.YTMUSIC_ANDROID.NAME;
|
||||
ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION;
|
||||
break;
|
||||
case 'TV_EMBEDDED':
|
||||
ctx.client.clientVersion = Constants.CLIENTS.TV_EMBEDDED.VERSION;
|
||||
ctx.client.clientName = Constants.CLIENTS.TV_EMBEDDED.NAME;
|
||||
ctx.client.clientScreen = 'EMBED';
|
||||
ctx.thirdParty = { embedUrl: Constants.URLS.YT_BASE };
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user