mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a90e5e0d07 | ||
|
|
955c8010a6 | ||
|
|
b2269deb79 | ||
|
|
573c8643aa | ||
|
|
e21542c227 | ||
|
|
9d912e5938 | ||
|
|
7ca0607004 | ||
|
|
20d84265b5 | ||
|
|
b13bf6e992 | ||
|
|
3d3436472f | ||
|
|
1a2fc3abd7 | ||
|
|
8ef4b42d44 | ||
|
|
b71f03caf2 | ||
|
|
dae7d6e40c | ||
|
|
2cee59024c | ||
|
|
ffd7d79308 | ||
|
|
9b005d62d6 | ||
|
|
a8e7e644ec | ||
|
|
ad1d3dbf91 | ||
|
|
3df3261488 | ||
|
|
1b1ce41c00 | ||
|
|
b82b720e4b | ||
|
|
4784dfa563 | ||
|
|
3e4d41bf06 | ||
|
|
9f1c31d7a0 | ||
|
|
9cb4530299 | ||
|
|
cb9a0c5410 | ||
|
|
427db5bbc2 | ||
|
|
2b29244b41 | ||
|
|
f9754f5ac6 | ||
|
|
b2253df802 | ||
|
|
f3517708ff | ||
|
|
0d35fe0ca5 | ||
|
|
3e3dc351bb |
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,5 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## [4.0.1](https://github.com/LuanRT/YouTube.js/compare/v4.0.0...v4.0.1) (2023-03-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Channel:** type mismatch in `subscribe_button` prop ([573c864](https://github.com/LuanRT/YouTube.js/commit/573c8643aae16ec7b6be5b333619a5d8c91ca5c1))
|
||||
|
||||
## [4.0.0](https://github.com/LuanRT/YouTube.js/compare/v3.3.0...v4.0.0) (2023-03-15)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **Parser:** general refactoring of parsers ([#344](https://github.com/LuanRT/YouTube.js/issues/344))
|
||||
* The `toDash` functions are now asynchronous, they now return a `Promise<string>` instead of a `string`, as we need to fetch the first sequence of the OTF format streams while building the manifest.
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for OTF format streams ([3e4d41b](https://github.com/LuanRT/YouTube.js/commit/3e4d41bf06ba16232979977c705444f2032bcde6))
|
||||
* **parser:** add `GridMix` ([#356](https://github.com/LuanRT/YouTube.js/issues/356)) ([a8e7e64](https://github.com/LuanRT/YouTube.js/commit/a8e7e644ec6df3b3c98a313f0321da27b4ca456e))
|
||||
* **parser:** add `GridShow` and `ShowCustomThumbnail` ([8ef4b42](https://github.com/LuanRT/YouTube.js/commit/8ef4b42d444c4fbe5cd65a55c0e0e7aa31738755)), closes [#459](https://github.com/LuanRT/YouTube.js/issues/459)
|
||||
* **parser:** add `MusicCardShelf` ([#358](https://github.com/LuanRT/YouTube.js/issues/358)) ([9b005d6](https://github.com/LuanRT/YouTube.js/commit/9b005d62d6590a2ddf6848dabfa33fce36e8df9c))
|
||||
* **parser:** Add `play_all_button` to `Shelf` ([#345](https://github.com/LuanRT/YouTube.js/issues/345)) ([427db5b](https://github.com/LuanRT/YouTube.js/commit/427db5bbc2bf3e8ec60371d504c2ab1cdae6e918))
|
||||
* **parser:** add `view_playlist` to `Playlist` ([#348](https://github.com/LuanRT/YouTube.js/issues/348)) ([9cb4530](https://github.com/LuanRT/YouTube.js/commit/9cb45302997771d909487b1ecba6f38655abef48))
|
||||
* **parser:** add InfoPanelContent and InfoPanelContainer nodes ([4784dfa](https://github.com/LuanRT/YouTube.js/commit/4784dfa563a4dbeaee31811824d5aa37a67f5557)), closes [#326](https://github.com/LuanRT/YouTube.js/issues/326)
|
||||
* **Parser:** just-in-time YTNode generation ([#310](https://github.com/LuanRT/YouTube.js/issues/310)) ([2cee590](https://github.com/LuanRT/YouTube.js/commit/2cee59024c730c34aa06052849ed6fb3f862ef33))
|
||||
* **yt:** add support for movie items and trailers ([#349](https://github.com/LuanRT/YouTube.js/issues/349)) ([9f1c31d](https://github.com/LuanRT/YouTube.js/commit/9f1c31d7a09532e80a187b14acceff31c22579bf))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **Parser:** general refactoring of parsers ([#344](https://github.com/LuanRT/YouTube.js/issues/344)) ([b13bf6e](https://github.com/LuanRT/YouTube.js/commit/b13bf6e9926c19a1939e0f4b69cbd53d1af0f7c8))
|
||||
|
||||
## [3.3.0](https://github.com/LuanRT/YouTube.js/compare/v3.2.0...v3.3.0) (2023-03-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **parser:** add `ConversationBar` node ([b2253df](https://github.com/LuanRT/YouTube.js/commit/b2253df8022217dc486155d8cacbf22db04dd9c2))
|
||||
* **VideoInfo:** support get by endpoint + more info ([#342](https://github.com/LuanRT/YouTube.js/issues/342)) ([0d35fe0](https://github.com/LuanRT/YouTube.js/commit/0d35fe0ca5e87a877b76cbb6cf3c92843eac5a99))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MultiMarkersPlayerBar:** avoid observing undefined objects ([f351770](https://github.com/LuanRT/YouTube.js/commit/f3517708ff34093a544c09d6f5f1ec806130d5cc))
|
||||
* **SharedPost:** import `Menu` node directly (oops) ([3e3dc35](https://github.com/LuanRT/YouTube.js/commit/3e3dc351bb44faec87616d9b922924d14a95f29f))
|
||||
* **ytmusic:** use static visitor id to avoid empty API responses ([f9754f5](https://github.com/LuanRT/YouTube.js/commit/f9754f5ac61d0f11b025f37f93783f971560268b)), closes [#279](https://github.com/LuanRT/YouTube.js/issues/279)
|
||||
|
||||
## [3.2.0](https://github.com/LuanRT/YouTube.js/compare/v3.1.1...v3.2.0) (2023-03-08)
|
||||
|
||||
|
||||
|
||||
18
COLLABORATORS.md
Normal file
18
COLLABORATORS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Collaborators
|
||||
|
||||
This page lists the collaborators who have contributed to the development and success of the project.
|
||||
|
||||
## [LuanRT](https://github.com/LuanRT)
|
||||
[](https://github.com/sponsors/LuanRT)
|
||||
[](https://ko-fi.com/luanrt)
|
||||
|
||||
Owner and maintainer.
|
||||
|
||||
## [Wykerd](https://github.com/wykerd/)
|
||||
Initial parser implementation, several bug fixes, major refactorings and general maintenance.
|
||||
|
||||
## [MasterOfBob777](https://github.com/MasterOfBob777)
|
||||
Bug fixes and TypeScript support.
|
||||
|
||||
## [patrickkfkan](https://github.com/patrickkfkan)
|
||||
Major refactorings, improved YouTube Music support, and bug fixes.
|
||||
@@ -1,71 +1,45 @@
|
||||
# Contributing to YouTube.js
|
||||
Welcome to YouTube.js! We're thrilled to have you interested in contributing to our project. As a community-driven project, we believe in the power of collaboration and look forward to working with you. To get started, please follow our easy-to-follow guidelines:
|
||||
|
||||
Thank you for taking the time to contribute!
|
||||
The following is a set of guidelines for contributing to YouTube.js.
|
||||
___
|
||||
- [Contributing to YouTube.js](#contributing-to-youtubejs)
|
||||
- [Issues](#issues)
|
||||
- [Create a new issue](#create-a-new-issue)
|
||||
- [Solve an issue](#solve-an-issue)
|
||||
- [Make changes](#make-changes)
|
||||
- [Commit your updates](#commit-your-updates)
|
||||
- [Pull Request](#pull-request)
|
||||
- [Test](#test)
|
||||
- [Lint](#lint)
|
||||
- [Build](#build)
|
||||
|
||||
## Issues
|
||||
|
||||
<a id="issue-1"></a>
|
||||
#### Create a new issue
|
||||
If you find a problem, search if an issue already exists. If a related issue doesn't exist, you can open a new issue using a relevant issue form.
|
||||
### Creating a new issue
|
||||
Before creating a new issue, we recommend searching for similar or related issues to avoid duplication efforts. However, if you can't find one, you're more than welcome to create a new issue using a relevant issue form. Please make sure to describe the issue as clearly and concisely as possible.
|
||||
|
||||
<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. Documentation updates and grammar fixes are also appreciated!
|
||||
### Solving an issue
|
||||
If you want to lend a hand by solving an issue, it's always good to browse existing issues to find one that grabs your attention. You can narrow down the search using tags as filters. If you find an issue you'd like to help with, please feel free to open a Pull Request with a fix. We appreciate documentation updates and grammar fixes too!
|
||||
|
||||
<a id="changes"></a>
|
||||
## Make changes
|
||||
## Making Changes
|
||||
|
||||
1. Fork the repository
|
||||
2. Install or update to **Node.js v16**
|
||||
3. Create a working branch and start with your changes!
|
||||
1. Fork the repository on GitHub.
|
||||
2. Ensure that you have the latest Node.js v16 version installed.
|
||||
3. Create a working branch and start making your changes and improvements!
|
||||
|
||||
<a id="changes-1"></a>
|
||||
#### Commit your updates
|
||||
### Committing updates
|
||||
When you're done with the changes, make sure to commit them. Don't forget to write a clear, descriptive commit message. We recommend following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
Commit the changes once you're happy with them.
|
||||
### Creating a Pull Request
|
||||
Once you're happy with your updates, create a pull request on GitHub. This is the most efficient way to get your contribution reviewed and eventually merged into our codebase.
|
||||
|
||||
<a id="changes-2"></a>
|
||||
#### Pull Request
|
||||
- Use the pull request template to fill in the necessary details.
|
||||
- If you're solving an issue, link the pull request to that issue.
|
||||
- Enable the checkbox to allow maintainers to edit the branch and update it for merging.
|
||||
- Changes may be required before we can merge your changes, and we'll let you know what needs to be done.
|
||||
|
||||
When you think the code is ready for review a pull request should be created on Github. Owners of the repository will watch out for new PR‘s and review them in regular intervals.
|
||||
### Testing, Linting, and Building
|
||||
We have some automated processes set up for testing, linting, and building. Please run the following commands to test, lint, and build your code before submitting it:
|
||||
|
||||
- Fill the template.
|
||||
- Link the PR to an issue, if you are solving one.
|
||||
- Enable the checkbox to allow maintainer edits so the branch can be updated for a merge.
|
||||
- Changes may be requested before a PR can be merged.
|
||||
- As you update your PR and apply changes, mark each conversation as resolved.
|
||||
|
||||
<a id="test"></a>
|
||||
#### Test
|
||||
|
||||
```bash
|
||||
Testing:
|
||||
```sh
|
||||
npm run test
|
||||
```
|
||||
|
||||
<a id="lint"></a>
|
||||
#### Lint
|
||||
|
||||
```bash
|
||||
Linting:
|
||||
```sh
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
<a id="build"></a>
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
Building:
|
||||
```sh
|
||||
# Build all
|
||||
npm run build
|
||||
|
||||
@@ -87,5 +61,6 @@ npm run bundle:node
|
||||
# Browser
|
||||
npm run bundle:browser
|
||||
npm run bundle:browser:prod
|
||||
```
|
||||
|
||||
```
|
||||
We appreciate your efforts and contributions to YouTube.js! Together, we can make this project even better.
|
||||
179
README.md
179
README.md
@@ -4,7 +4,7 @@
|
||||
[codefactor]: https://www.codefactor.io/repository/github/luanrt/youtube.js
|
||||
[actions]: https://github.com/LuanRT/YouTube.js/actions
|
||||
[say-thanks]: https://saythanks.io/to/LuanRT
|
||||
[github-sponsors]:https://github.com/sponsors/LuanRT
|
||||
[collaborators]: https://github.com/LuanRT/YouTube.js/blob/main/COLLABORATORS.md
|
||||
|
||||
<!-- OTHER LINKS -->
|
||||
[project]: https://github.com/LuanRT/YouTube.js
|
||||
@@ -25,7 +25,7 @@
|
||||
[][discord]
|
||||
[][say-thanks]
|
||||
<br>
|
||||
[][github-sponsors]
|
||||
[][collaborators]
|
||||
|
||||
</div>
|
||||
|
||||
@@ -51,40 +51,36 @@
|
||||
</body>
|
||||
</table>
|
||||
|
||||
___
|
||||
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#about">About</a>
|
||||
</li>
|
||||
<li>
|
||||
## Table of Contents
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#description">Description</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#getting-started">Getting Started</a>
|
||||
<ul>
|
||||
<li><a href="#prerequisites">Prerequisites</a></li>
|
||||
<li><a href="#installation">Installation</a></li>
|
||||
<li><a href="#prerequisites">Prerequisites</a></li>
|
||||
<li><a href="#installation">Installation</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#usage">Usage</a>
|
||||
<ul>
|
||||
<li><a href="#browser-usage">Browser Usage</a></li>
|
||||
<li><a href="#caching">Caching</a></li>
|
||||
<li><a href="#api">API</a></li>
|
||||
<li><a href="#browser-usage">Browser Usage</a></li>
|
||||
<li><a href="#caching">Caching</a></li>
|
||||
<li><a href="#api">API</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#extending-the-library">Extending the library</a></li>
|
||||
<li><a href="#contributing">Contributing</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
<li><a href="#disclaimer">Disclaimer</a></li>
|
||||
<li><a href="#license">License</a></li>
|
||||
</ol>
|
||||
</details>
|
||||
</li>
|
||||
<li><a href="#extending-the-library">Extending the library</a></li>
|
||||
<li><a href="#contributing">Contributing</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
<li><a href="#disclaimer">Disclaimer</a></li>
|
||||
<li><a href="#license">License</a></li>
|
||||
</ol>
|
||||
|
||||
## Description
|
||||
|
||||
InnerTube is an API used by all YouTube clients. It was created to simplify the deployment of new features and experiments across the platform[^1]. This library handles all the low-level communication with InnerTube, providing a simple, and efficient way to interact with YouTube programmatically. It is designed to emulate an actual client as closely as possible, including how API responses are parsed.
|
||||
InnerTube is an API used by all YouTube clients. It was created to simplify the deployment of new features and experiments across the platform [^1]. This library manages all low-level communication with InnerTube, providing a simple and efficient way to interact with YouTube programmatically. Its design aims to closely emulate an actual client, including the parsing of API responses.
|
||||
|
||||
If you have any questions or need help, feel free to reach out to us on our [Discord server][discord] or open an issue [here](https://github.com/LuanRT/YouTube.js/issues).
|
||||
|
||||
@@ -95,7 +91,7 @@ YouTube.js runs on Node.js, Deno, and modern browsers.
|
||||
|
||||
It requires a runtime with the following features:
|
||||
- [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
||||
- On Node we use [undici]()'s fetch implementation which requires Node.js 16.8+. You may provide your fetch implementation if you need to use an older version. See [providing your own fetch implementation](#custom-fetch) for more information.
|
||||
- On Node, we use [undici](https://github.com/nodejs/undici)'s fetch implementation, which requires Node.js 16.8+. If you need to use an older version, you may provide your own fetch implementation. See [providing your own fetch implementation](#custom-fetch) for more information.
|
||||
- The `Response` object returned by fetch must thus be spec compliant and return a `ReadableStream` object if you want to use the `VideoInfo#download` method. (Implementations like `node-fetch` returns a non-standard `Readable` object.)
|
||||
- [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) and [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) are required.
|
||||
|
||||
@@ -122,16 +118,37 @@ Create an InnerTube instance:
|
||||
```ts
|
||||
// const { Innertube } = require('youtubei.js');
|
||||
import { Innertube } from 'youtubei.js';
|
||||
const youtube = await Innertube.create();
|
||||
const youtube = await Innertube.create(/* options */);
|
||||
```
|
||||
|
||||
## Browser Usage
|
||||
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`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/proxy/deno.ts).
|
||||
### Initialization Options
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
You may provide your own fetch implementation to be used by YouTube.js. Which we will use here to modify and send the requests through our proxy. See [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/web) for a simple example using [Vite](https://vitejs.dev/).
|
||||
| Option | Type | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| `lang` | `string` | Language. | `en` |
|
||||
| `location` | `string` | Geolocation. | `US` |
|
||||
| `account_index` | `number` | The account index to use. This is useful if you have multiple accounts logged in. **NOTE:** Only works if you are signed in with cookies. | `0` |
|
||||
| `retrieve_player` | `boolean` | Specifies whether to retrieve the JS player. Disabling this will make session creation faster. **NOTE:** Deciphering formats is not possible without the JS player. | `true` |
|
||||
| `enable_safety_mode` | `boolean` | Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content. | `false` |
|
||||
| `generate_session_locally` | `boolean` | Specifies whether to generate the session data locally or retrieve it from YouTube. This can be useful if you need more performance. | `false` |
|
||||
| `device_category` | `DeviceCategory` | Platform to use for the session. | `DESKTOP` |
|
||||
| `client_type` | `ClientType` | InnerTube client type. | `WEB` |
|
||||
| `timezone` | `string` | The time zone. | `*` |
|
||||
| `cache` | `ICache` | Used to cache the deciphering functions from the JS player. | `undefined` |
|
||||
| `cookie` | `string` | YouTube cookies. | `undefined` |
|
||||
| `fetch` | `FetchFunction` | Fetch function to use. | `fetch` |
|
||||
|
||||
</details>
|
||||
|
||||
## Browser Usage
|
||||
To use YouTube.js in the browser, you must proxy requests through your own server. You can see our simple reference implementation in Deno at [`examples/browser/proxy/deno.ts`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/proxy/deno.ts).
|
||||
|
||||
You may provide your own fetch implementation to be used by YouTube.js, which we will use to modify and send the requests through a proxy. See [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/web) for a simple example using [Vite](https://vitejs.dev/).
|
||||
|
||||
```ts
|
||||
// We provide multiple exports for the web.
|
||||
// Multiple exports are available for the web.
|
||||
// Unbundled ESM version
|
||||
import { Innertube } from 'youtubei.js/web';
|
||||
// Bundled ESM version
|
||||
@@ -166,7 +183,7 @@ const videoInfo = await youtube.getInfo('videoId');
|
||||
// now convert to a dash manifest
|
||||
// again - to be able to stream the video in the browser - we must proxy the requests through our own server
|
||||
// to do this, we provide a method to transform the URLs before writing them to the manifest
|
||||
const manifest = videoInfo.toDash(url => {
|
||||
const manifest = await videoInfo.toDash(url => {
|
||||
// modify the url
|
||||
// and return it
|
||||
return url;
|
||||
@@ -202,23 +219,25 @@ const yt = await Innertube.create({
|
||||
<a name="caching"></a>
|
||||
|
||||
## Caching
|
||||
To improve performance, you may wish to cache the transformed player instance which we use to decode the streaming urls.
|
||||
Caching the transformed player instance can greatly improve the performance. Our `UniversalCache` implementation uses different caching methods depending on the environment.
|
||||
|
||||
Our cache uses the `node:fs` module in Node-like environments, `Deno.writeFile` in Deno, and `indexedDB` in browsers.
|
||||
In Node.js, we use the `node:fs` module, `Deno.writeFile()` in Deno, and `indexedDB` in browsers.
|
||||
|
||||
By default, the cache stores data in the operating system's temporary directory (or `indexedDB` in browsers). You can make this cache persistent by specifying the path to the cache directory, which will be created if it doesn't exist.
|
||||
|
||||
```ts
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
// By default, cache stores files in the OS temp directory (or indexedDB in browsers).
|
||||
// Create a cache that stores files in the OS temp directory (or indexedDB in browsers) by default.
|
||||
const yt = await Innertube.create({
|
||||
cache: new UniversalCache(false)
|
||||
});
|
||||
|
||||
// You may wish to make the cache persistent (on Node and Deno)
|
||||
// You may want to create a persistent cache instead (on Node and Deno).
|
||||
const yt = await Innertube.create({
|
||||
cache: new UniversalCache(
|
||||
// Enables persistent caching
|
||||
true,
|
||||
// Path to the cache directory will create the directory if it doesn't exist
|
||||
// Path to the cache directory. The directory will be created if it doesn't exist
|
||||
'./.cache'
|
||||
)
|
||||
});
|
||||
@@ -248,7 +267,7 @@ const yt = await Innertube.create({
|
||||
<summary>Methods</summary>
|
||||
<p>
|
||||
|
||||
* [.getInfo(video_id, client?)](#getinfo)
|
||||
* [.getInfo(target, client?)](#getinfo)
|
||||
* [.getBasicInfo(video_id, client?)](#getbasicinfo)
|
||||
* [.search(query, filters?)](#search)
|
||||
* [.getSearchSuggestions(query)](#getsearchsuggestions)
|
||||
@@ -273,15 +292,15 @@ const yt = await Innertube.create({
|
||||
</details>
|
||||
|
||||
<a name="getinfo"></a>
|
||||
### getInfo(video_id, client?)
|
||||
### getInfo(target, client?)
|
||||
|
||||
Retrieves video info, including playback data and even layout elements such as menus, buttons, etc — all nicely parsed.
|
||||
Retrieves video info.
|
||||
|
||||
**Returns**: `Promise<VideoInfo>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The id of the video |
|
||||
| target | `string` \| `NavigationEndpoint` | If `string`, the id of the video. If `NavigationEndpoint`, the endpoint of watchable elements such as `Video`, `Mix` and `Playlist`. To clarify, valid endpoints have payloads containing at least `videoId` and optionally `playlistId`, `params` and `index`. |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC`, `YTMUSIC_ANDROID` or `TV_EMBEDDED` |
|
||||
|
||||
<details>
|
||||
@@ -300,6 +319,9 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
- `<info>#getLiveChat()`
|
||||
- Returns a LiveChat instance.
|
||||
|
||||
- `<info>#getTrailerInfo()`
|
||||
- Returns trailer info in a new `VideoInfo` instance, or `null` if none. Typically available for non-purchased movies or films.
|
||||
|
||||
- `<info>#chooseFormat(options)`
|
||||
- Used to choose streaming data formats.
|
||||
|
||||
@@ -321,6 +343,12 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
- `<info>#addToWatchHistory()`
|
||||
- Adds the video to the watch history.
|
||||
|
||||
- `<info>#autoplay_video_endpoint`
|
||||
- Returns the endpoint of the video for Autoplay.
|
||||
|
||||
- `<info>#has_trailer`
|
||||
- Checks if trailer is available.
|
||||
|
||||
- `<info>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
|
||||
@@ -354,6 +382,20 @@ Searches the given query on YouTube.
|
||||
| query | `string` | The search query |
|
||||
| filters? | `SearchFilters` | Search filters |
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Search Filters</summary>
|
||||
|
||||
| Filter | Type | Value | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| upload_date | `string` | `all` \| `hour` \| `today` \| `week` \| `month` \| `year` | Filter by upload date |
|
||||
| type | `string` | `all` \| `video` \| `channel` \| `playlist` \| `movie` | Filter by type |
|
||||
| duration | `string` | `all` \| `short` \| `medium` \| `long` | Filter by duration |
|
||||
| sort_by | `string` | `relevance` \| `rating` \| `upload_date` \| `view_count` | Sort by |
|
||||
| features | `string[]` | `hd` \| `subtitles` \| `creative_commons` \| `3d` \| `live` \| `purchased` \| `4k` \| `360` \| `location` \| `hdr` \| `vr180` | Filter by features |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
<p>
|
||||
@@ -601,12 +643,18 @@ Retrieves a given hashtag's page.
|
||||
Returns deciphered streaming data.
|
||||
|
||||
> **Note**
|
||||
> This will be deprecated in the future. It is recommended to retrieve streaming data from a `VideoInfo`/`TrackInfo` object instead if you want to select formats manually, see the example below.
|
||||
> This method will be deprecated in the future. We recommend retrieving streaming data from a `VideoInfo` or `TrackInfo` object instead if you want to select formats manually. Please refer to the following example:
|
||||
|
||||
```ts
|
||||
const info = await yt.getBasicInfo('somevideoid');
|
||||
|
||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
console.info('Playback url:', url);
|
||||
|
||||
// or:
|
||||
const format = info.chooseFormat({ type: 'audio', quality: 'best' });
|
||||
const url = format?.decipher(yt.session.player);
|
||||
console.info('Playback url:', url);
|
||||
```
|
||||
|
||||
**Returns**: `Promise<object>`
|
||||
@@ -652,10 +700,9 @@ Utility to call navigation endpoints.
|
||||
|
||||
## Extending the library
|
||||
|
||||
YouTube.js is completely modular and easy to extend. Almost all methods, classes, and utilities used internally are exposed and can be used to implement your own extensions without having to modify the library's source code.
|
||||
|
||||
For example, let's say we want to implement a method to retrieve video info manually. We can do that by using an instance of the `Actions` class:
|
||||
YouTube.js is modular and easy to extend. Most of the methods, classes, and utilities used internally are exposed and can be used to implement your own extensions without having to modify the library's source code.
|
||||
|
||||
For example, let's say we want to implement a method to retrieve video info. We can do that by using an instance of the `Actions` class:
|
||||
```ts
|
||||
import { Innertube } from 'youtubei.js';
|
||||
|
||||
@@ -664,10 +711,10 @@ import { Innertube } from 'youtubei.js';
|
||||
|
||||
async function getVideoInfo(videoId: string) {
|
||||
const videoInfo = await yt.actions.execute('/player', {
|
||||
// anything added here will be merged with the default payload and sent to InnerTube.
|
||||
// You can add any additional payloads here, and they'll merge with the default payload sent to InnerTube.
|
||||
videoId,
|
||||
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.
|
||||
client: 'YTMUSIC', // InnerTube client options: ANDROID, YTMUSIC, YTMUSIC_ANDROID, WEB, or TV_EMBEDDED.
|
||||
parse: true // tells YouTube.js to parse the response (not sent to InnerTube).
|
||||
});
|
||||
|
||||
return videoInfo;
|
||||
@@ -678,8 +725,7 @@ import { Innertube } from 'youtubei.js';
|
||||
})();
|
||||
```
|
||||
|
||||
Or perhaps there's a `NavigationEndpoint` in a parsed response and we want to call it to see what happens:
|
||||
|
||||
Alternatively, suppose we locate a `NavigationEndpoint` in a parsed response and want to see what happens when we call it:
|
||||
```ts
|
||||
import { Innertube, YTNodes } from 'youtubei.js';
|
||||
|
||||
@@ -689,11 +735,11 @@ import { Innertube, YTNodes } from 'youtubei.js';
|
||||
const artist = await yt.music.getArtist('UC52ZqHVQz5OoGhvbWiRal6g');
|
||||
const albums = artist.sections[1].as(YTNodes.MusicCarouselShelf);
|
||||
|
||||
// Say we want to click the “More” button:
|
||||
// Let's imagine that we wish to click on the “More” button:
|
||||
const button = albums.as(YTNodes.MusicCarouselShelf).header?.more_content;
|
||||
|
||||
if (button) {
|
||||
// After making sure it exists, we can call its navigation endpoint:
|
||||
// Having ensured that it exists, we can then call its navigation endpoint using the following code:
|
||||
const page = await button.endpoint.call(yt.actions, { parse: true });
|
||||
console.info(page);
|
||||
}
|
||||
@@ -702,16 +748,16 @@ import { Innertube, YTNodes } from 'youtubei.js';
|
||||
|
||||
### Parser
|
||||
|
||||
YouTube.js' parser allows you to parse InnerTube responses and turn their nodes into strongly typed objects that can be easily manipulated. It also provides a set of utility methods that make working with InnerTube much easier.
|
||||
YouTube.js' parser enables you to parse InnerTube responses and convert their nodes into strongly-typed objects that are simple to manipulate. Additionally, it provides numerous utility methods that make working with InnerTube a breeze.
|
||||
|
||||
Example:
|
||||
Here's an example of its usage:
|
||||
```ts
|
||||
// See ./examples/parser
|
||||
|
||||
import { Parser, YTNodes } from 'youtubei.js';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Artist page response from YouTube Music
|
||||
// YouTube Music's artist page response
|
||||
const data = readFileSync('./artist.json').toString();
|
||||
|
||||
const page = Parser.parseResponse(JSON.parse(data));
|
||||
@@ -720,15 +766,9 @@ const header = page.header?.item().as(YTNodes.MusicImmersiveHeader, YTNodes.Musi
|
||||
|
||||
console.info('Header:', header);
|
||||
|
||||
/**
|
||||
* The parser encapsulates all arrays in a proxy object.
|
||||
* A proxy intercepts access to the actual data, allowing
|
||||
* the parser to add type safety and many utility methods
|
||||
* that make working with InnerTube much easier.
|
||||
*/
|
||||
// The parser uses a proxy object to add type safety and utility methods for working with InnerTube's data arrays:
|
||||
const tab = page.contents?.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
|
||||
|
||||
if (!tab)
|
||||
throw new Error('Target tab not found');
|
||||
|
||||
@@ -742,12 +782,10 @@ console.info('Sections:', sections);
|
||||
|
||||
Documentation for the parser can be found [here](https://github.com/LuanRT/YouTube.js/blob/main/src/parser).
|
||||
|
||||
<!-- CONTRIBUTING -->
|
||||
## Contributing
|
||||
Contributions, issues, and feature requests are welcome.
|
||||
Feel free to check the [issues page](https://github.com/LuanRT/YouTube.js/issues) and our [guidelines](https://github.com/LuanRT/YouTube.js/blob/main/CONTRIBUTING.md) if you want to contribute.
|
||||
We welcome all contributions, issues and feature requests, whether small or large. If you want to contribute, feel free to check out our [issues page](https://github.com/LuanRT/YouTube.js/issues) and our [guidelines](https://github.com/LuanRT/YouTube.js/blob/main/CONTRIBUTING.md).
|
||||
|
||||
Thank you to all the wonderful people who have contributed to this project:
|
||||
We are immensely grateful to all the wonderful people who have contributed to this project. A special shoutout to all our contributors! 🎉
|
||||
<a href="https://github.com/LuanRT/YouTube.js/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=LuanRT/YouTube.js" />
|
||||
</a>
|
||||
@@ -759,10 +797,9 @@ LuanRT - [@thesciencephile][twitter] - luan.lrt4@gmail.com
|
||||
Project Link: [https://github.com/LuanRT/YouTube.js][project]
|
||||
|
||||
## Disclaimer
|
||||
This project is not affiliated with, endorsed, or sponsored by YouTube or any of its affiliates or subsidiaries.
|
||||
All trademarks, logos, and brand names are the property of their respective owners and are used only to directly describe the services being provided, as such, any usage of trademarks to refer to such services is considered nominative use.
|
||||
This project is not affiliated with, endorsed, or sponsored by YouTube or any of its affiliates or subsidiaries. All trademarks, logos, and brand names used in this project are the property of their respective owners and are used solely to describe the services provided.
|
||||
|
||||
Should you have any questions or concerns please contact me directly via email.
|
||||
As such, any usage of trademarks to refer to such services is considered nominative use. If you have any questions or concerns, please contact me directly via email.
|
||||
|
||||
[^1]: https://gizmodo.com/how-project-innertube-helped-pull-youtube-out-of-the-gu-1704946491
|
||||
|
||||
|
||||
@@ -77,7 +77,16 @@ Searches on YouTube Music.
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| query | `string` | Search query |
|
||||
| filters? | `object` | Search filters |
|
||||
| filters? | `MusicSearchFilters` | Search filters |
|
||||
|
||||
<details>
|
||||
<summary>Search Filters</summary>
|
||||
|
||||
| Filter | Type | Value | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| type | `string` | `all`, `song`, `video`, `album`, `playlist`, `artist` | Search type |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
# Updating the parser
|
||||
# Updating the Parser
|
||||
|
||||
YouTube is constantly changing, so it is not rare to see YouTube crawlers/scrapers breaking every now and then.
|
||||
YouTube is constantly changing, so it is not uncommon 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 similar to this:
|
||||
Our parser, on the other hand, was written so that it behaves similarly to an official client, parsing and mapping renderers (also known as YTNodes) dynamically without hard-coding their path in the response. This way, whenever a new renderer pops up (e.g., when YouTube adds a new feature or makes a minor UI change), the library will print a warning similar to this:
|
||||
|
||||
```
|
||||
InnertubeError: SomeRenderer not found!
|
||||
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'
|
||||
Introspected and JIT generated this class in the meantime:
|
||||
class SomeRenderer extends YTNode {
|
||||
static type = 'SomeRenderer';
|
||||
|
||||
// ...
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This warning **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
|
||||
## 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:
|
||||
For example, suppose we have found a new renderer named `verticalListRenderer`. In that case, to let the parser know it exists at compile-time, we would have to create a file with the following structure:
|
||||
|
||||
> `../classes/VerticalList.ts`
|
||||
|
||||
@@ -49,6 +51,8 @@ class VerticalList extends YTNode {
|
||||
export default VerticalList;
|
||||
```
|
||||
|
||||
You may use the parser's generated class for the new renderer as a starting point for your own implementation.
|
||||
|
||||
Then update the parser map:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -17,8 +17,9 @@ const { Innertube, UniversalCache } = require('youtubei.js');
|
||||
});
|
||||
|
||||
// 'update-credentials' is fired when the access token expires, if you do not save the updated credentials any subsequent request will fail
|
||||
yt.session.on('update-credentials', ({ credentials }) => {
|
||||
yt.session.on('update-credentials', async ({ credentials }) => {
|
||||
console.log('Credentials updated:', credentials);
|
||||
await yt.session.oauth.cacheCredentials();
|
||||
});
|
||||
|
||||
// Attempt to sign in
|
||||
|
||||
@@ -113,7 +113,7 @@ async function main() {
|
||||
|
||||
showUI(true);
|
||||
|
||||
const dash = info.toDash((url) => {
|
||||
const dash = await info.toDash((url) => {
|
||||
url.searchParams.set('__host', url.host);
|
||||
url.host = 'localhost:8080';
|
||||
url.protocol = 'http';
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "3.2.0",
|
||||
"version": "4.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "3.2.0",
|
||||
"version": "4.0.1",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "3.2.0",
|
||||
"version": "4.0.1",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
|
||||
@@ -3,9 +3,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const import_list = [];
|
||||
|
||||
const json = [];
|
||||
const misc_exports = [];
|
||||
const misc_imports = [];
|
||||
|
||||
glob.sync('../src/parser/classes/**/*.{js,ts}', { cwd: __dirname })
|
||||
.forEach((file) => {
|
||||
@@ -16,44 +14,26 @@ glob.sync('../src/parser/classes/**/*.{js,ts}', { cwd: __dirname })
|
||||
|
||||
if (is_misc) {
|
||||
const class_name = file.split('/').pop().replace('.js', '').replace('.ts', '');
|
||||
import_list.push(`import { default as ${class_name} } from './classes/${file}.js';`);
|
||||
misc_exports.push(class_name);
|
||||
misc_imports.push(`export { default as ${class_name} } from './classes/${file}.js';`);
|
||||
} else {
|
||||
import_list.push(`import { default as ${import_name} } from './classes/${file}.js';
|
||||
export { ${import_name} };`);
|
||||
json.push(import_name);
|
||||
import_list.push(`export { default as ${import_name} } from './classes/${file}.js';`);
|
||||
}
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../src/parser/map.ts'),
|
||||
path.resolve(__dirname, '../src/parser/nodes.ts'),
|
||||
`// This file was auto generated, do not edit.
|
||||
// See ./scripts/build-parser-map.js
|
||||
import { YTNodeConstructor } from './helpers.js';
|
||||
|
||||
${import_list.join('\n')}
|
||||
`
|
||||
);
|
||||
|
||||
const map: Record<string, YTNodeConstructor> = {
|
||||
${json.join(',\n ')}
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../src/parser/misc.ts'),
|
||||
`// This file was auto generated, do not edit.
|
||||
// See ./scripts/build-parser-map.js
|
||||
|
||||
export const Misc = {
|
||||
${misc_exports.join(',\n ')}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param name - Name of the node to be parsed
|
||||
*/
|
||||
export default function GetParserByName(name: string) {
|
||||
const ParserConstructor = map[name];
|
||||
|
||||
if (!ParserConstructor) {
|
||||
const error = new Error(\`Module not found: \${name}\`);
|
||||
(error as any).code = 'MODULE_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
return ParserConstructor;
|
||||
}
|
||||
${misc_imports.join('\n')}
|
||||
`
|
||||
);
|
||||
139
src/Innertube.ts
139
src/Innertube.ts
@@ -31,7 +31,7 @@ import type Format from './parser/classes/misc/Format.js';
|
||||
import type { ApiResponse } from './core/Actions.js';
|
||||
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js';
|
||||
import type { DownloadOptions, FormatOptions } from './utils/FormatUtils.js';
|
||||
import { generateRandomString, throwIfMissing } from './utils/Utils.js';
|
||||
import { generateRandomString, InnertubeError, throwIfMissing } from './utils/Utils.js';
|
||||
|
||||
export type InnertubeConfig = SessionOptions;
|
||||
|
||||
@@ -45,25 +45,14 @@ export interface SearchFilters {
|
||||
|
||||
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS'
|
||||
|
||||
class Innertube {
|
||||
session: Session;
|
||||
account: AccountManager;
|
||||
playlist: PlaylistManager;
|
||||
interact: InteractionManager;
|
||||
music: YTMusic;
|
||||
studio: YTStudio;
|
||||
kids: YTKids;
|
||||
actions: Actions;
|
||||
/**
|
||||
* Provides access to various services and modules in the YouTube API.
|
||||
*/
|
||||
export default class Innertube {
|
||||
#session: Session;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.session = session;
|
||||
this.account = new AccountManager(this.session.actions);
|
||||
this.playlist = new PlaylistManager(this.session.actions);
|
||||
this.interact = new InteractionManager(this.session.actions);
|
||||
this.music = new YTMusic(this.session);
|
||||
this.studio = new YTStudio(this.session);
|
||||
this.kids = new YTKids(this.session);
|
||||
this.actions = this.session.actions;
|
||||
this.#session = session;
|
||||
}
|
||||
|
||||
static async create(config: InnertubeConfig = {}): Promise<Innertube> {
|
||||
@@ -72,19 +61,55 @@ class Innertube {
|
||||
|
||||
/**
|
||||
* Retrieves video info.
|
||||
* @param video_id - The video id.
|
||||
* @param target - The video id or `NavigationEndpoint`.
|
||||
* @param client - The client to use.
|
||||
*/
|
||||
async getInfo(video_id: string, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
throwIfMissing({ video_id });
|
||||
async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
throwIfMissing({ target });
|
||||
|
||||
let payload: {
|
||||
videoId: string,
|
||||
playlistId?: string,
|
||||
params?: string,
|
||||
playlistIndex?: number
|
||||
};
|
||||
|
||||
if (target instanceof NavigationEndpoint) {
|
||||
const video_id = target.payload?.videoId;
|
||||
|
||||
if (!video_id)
|
||||
throw new InnertubeError('Missing video id in endpoint payload.', target);
|
||||
|
||||
payload = {
|
||||
videoId: video_id
|
||||
};
|
||||
|
||||
if (target.payload.playlistId) {
|
||||
payload.playlistId = target.payload.playlistId;
|
||||
}
|
||||
|
||||
if (target.payload.params) {
|
||||
payload.params = target.payload.params;
|
||||
}
|
||||
|
||||
if (target.payload.index) {
|
||||
payload.playlistIndex = target.payload.index;
|
||||
}
|
||||
} else if (typeof target === 'string') {
|
||||
payload = {
|
||||
videoId: target
|
||||
};
|
||||
} else {
|
||||
throw new InnertubeError('Invalid target, expected either a video id or a valid NavigationEndpoint', target);
|
||||
}
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = this.actions.getVideoInfo(video_id, cpn, client);
|
||||
const continuation = this.actions.execute('/next', { videoId: video_id });
|
||||
const initial_info = this.actions.getVideoInfo(payload.videoId, cpn, client);
|
||||
const continuation = this.actions.execute('/next', payload);
|
||||
|
||||
const response = await Promise.all([ initial_info, continuation ]);
|
||||
return new VideoInfo(response, this.actions, this.session.player, cpn);
|
||||
return new VideoInfo(response, this.actions, cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +123,7 @@ class Innertube {
|
||||
const cpn = generateRandomString(16);
|
||||
const response = await this.actions.getVideoInfo(video_id, cpn, client);
|
||||
|
||||
return new VideoInfo([ response ], this.actions, this.session.player, cpn);
|
||||
return new VideoInfo([ response ], this.actions, cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,14 +155,14 @@ class Innertube {
|
||||
|
||||
const url = new URL(`${Constants.URLS.YT_SUGGESTIONS}search`);
|
||||
url.searchParams.set('q', query);
|
||||
url.searchParams.set('hl', this.session.context.client.hl);
|
||||
url.searchParams.set('gl', this.session.context.client.gl);
|
||||
url.searchParams.set('hl', this.#session.context.client.hl);
|
||||
url.searchParams.set('gl', this.#session.context.client.gl);
|
||||
url.searchParams.set('ds', 'yt');
|
||||
url.searchParams.set('client', 'youtube');
|
||||
url.searchParams.set('xssi', 't');
|
||||
url.searchParams.set('oe', 'UTF');
|
||||
|
||||
const response = await this.session.http.fetch(url);
|
||||
const response = await this.#session.http.fetch(url);
|
||||
const response_data = await response.text();
|
||||
|
||||
const data = JSON.parse(response_data.replace(')]}\'', ''));
|
||||
@@ -311,6 +336,60 @@ class Innertube {
|
||||
call(endpoint: NavigationEndpoint, args?: object): Promise<IParsedResponse | ApiResponse> {
|
||||
return endpoint.call(this.actions, args);
|
||||
}
|
||||
}
|
||||
|
||||
export default Innertube;
|
||||
/**
|
||||
* An instance of YTMusic for interacting with the YouTube Music service.
|
||||
*/
|
||||
get music(): YTMusic {
|
||||
return new YTMusic(this.#session);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of YTStudio for interacting with the YouTube Studio service.
|
||||
*/
|
||||
get studio(): YTStudio {
|
||||
return new YTStudio(this.#session);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of YTKids for interacting with the YouTube Kids service.
|
||||
*/
|
||||
get kids(): YTKids {
|
||||
return new YTKids(this.#session);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of AccountManager for managing a user's account.
|
||||
*/
|
||||
get account(): AccountManager {
|
||||
return new AccountManager(this.#session.actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of PlaylistManager for managing playlists.
|
||||
*/
|
||||
get playlist(): PlaylistManager {
|
||||
return new PlaylistManager(this.#session.actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of InteractionManager for interacting with contents in YouTube.
|
||||
*/
|
||||
get interact(): InteractionManager {
|
||||
return new InteractionManager(this.#session.actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of Actions.
|
||||
*/
|
||||
get actions(): Actions {
|
||||
return this.#session.actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the InnerTube session instance.
|
||||
*/
|
||||
get session(): Session {
|
||||
return this.#session;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
* Get all videos on a given page via memo
|
||||
*/
|
||||
static getVideosFromMemo(memo: Memo) {
|
||||
return memo.getType<Video | GridVideo | ReelItem | CompactVideo | PlaylistVideo | PlaylistPanelVideo | WatchCardCompactVideo>([
|
||||
return memo.getType(
|
||||
Video,
|
||||
GridVideo,
|
||||
ReelItem,
|
||||
@@ -80,14 +80,14 @@ class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
PlaylistVideo,
|
||||
PlaylistPanelVideo,
|
||||
WatchCardCompactVideo
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all playlists on a given page via memo
|
||||
*/
|
||||
static getPlaylistsFromMemo(memo: Memo) {
|
||||
return memo.getType<Playlist | GridPlaylist>([ Playlist, GridPlaylist ]);
|
||||
return memo.getType(Playlist, GridPlaylist);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,14 +101,14 @@ class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
* Get all the community posts in the feed
|
||||
*/
|
||||
get posts() {
|
||||
return this.#memo.getType<Post | BackstagePost | SharedPost>([ BackstagePost, Post, SharedPost ]);
|
||||
return this.#memo.getType(BackstagePost, Post, SharedPost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the channels in the feed
|
||||
*/
|
||||
get channels() {
|
||||
return this.#memo.getType<Channel | GridChannel>([ Channel, GridChannel ]);
|
||||
return this.#memo.getType(Channel, GridChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +137,7 @@ class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
* Returns all segments/sections from the page.
|
||||
*/
|
||||
get shelves() {
|
||||
return this.#memo.getType<Shelf | RichShelf | ReelShelf>([ Shelf, RichShelf, ReelShelf ]);
|
||||
return this.#memo.getType(Shelf, RichShelf, ReelShelf);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
103
src/core/MediaInfo.ts
Normal file
103
src/core/MediaInfo.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import Actions, { ApiResponse } from './Actions.js';
|
||||
import Constants from '../utils/Constants.js';
|
||||
import FormatUtils, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../utils/FormatUtils.js';
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
import Format from '../parser/classes/misc/Format.js';
|
||||
import Parser, { INextResponse, IPlayerResponse } from '../parser/index.js';
|
||||
|
||||
export class MediaInfo {
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
#actions: Actions;
|
||||
#cpn: string;
|
||||
#playback_tracking;
|
||||
streaming_data;
|
||||
playability_status;
|
||||
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
|
||||
this.#actions = actions;
|
||||
|
||||
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
|
||||
|
||||
this.#page = [ info, next ];
|
||||
this.#cpn = cpn;
|
||||
|
||||
if (info.playability_status?.status === 'ERROR')
|
||||
throw new InnertubeError('This video is unavailable', info.playability_status);
|
||||
|
||||
this.streaming_data = info.streaming_data;
|
||||
this.playability_status = info.playability_status;
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a DASH manifest from the streaming data.
|
||||
* @param url_transformer - Function to transform the URLs.
|
||||
* @param format_filter - Function to filter the formats.
|
||||
* @returns DASH manifest
|
||||
*/
|
||||
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter): Promise<string> {
|
||||
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the format that best matches the given options.
|
||||
* @param options - Options
|
||||
*/
|
||||
chooseFormat(options: FormatOptions): Format {
|
||||
return FormatUtils.chooseFormat(options, this.streaming_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the video.
|
||||
* @param options - Download options.
|
||||
*/
|
||||
async download(options: DownloadOptions = {}): Promise<ReadableStream<Uint8Array>> {
|
||||
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds video to the watch history.
|
||||
*/
|
||||
async addToWatchHistory(client_name = Constants.CLIENTS.WEB.NAME, client_version = Constants.CLIENTS.WEB.VERSION, replacement = 'https://www.'): Promise<Response> {
|
||||
if (!this.#playback_tracking)
|
||||
throw new InnertubeError('Playback tracking not available');
|
||||
|
||||
const url_params = {
|
||||
cpn: this.#cpn,
|
||||
fmt: 251,
|
||||
rtn: 0,
|
||||
rt: 0
|
||||
};
|
||||
|
||||
const url = this.#playback_tracking.videostats_playback_url.replace('https://s.', replacement);
|
||||
|
||||
const response = await this.#actions.stats(url, {
|
||||
client_name,
|
||||
client_version
|
||||
}, url_params);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions instance.
|
||||
*/
|
||||
get actions(): Actions {
|
||||
return this.#actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Playback Nonce.
|
||||
*/
|
||||
get cpn(): string {
|
||||
return this.#cpn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Original parsed InnerTube response.
|
||||
*/
|
||||
get page(): [IPlayerResponse, INextResponse?] {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,9 @@ import type { ObservedArray, YTNode } from '../parser/helpers.js';
|
||||
import type Actions from './Actions.js';
|
||||
import type Session from './Session.js';
|
||||
|
||||
export type SearchFilters = {
|
||||
export interface MusicSearchFilters {
|
||||
type?: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist';
|
||||
};
|
||||
}
|
||||
|
||||
class Music {
|
||||
#session: Session;
|
||||
@@ -112,7 +112,7 @@ class Music {
|
||||
* @param query - Search query.
|
||||
* @param filters - Search filters.
|
||||
*/
|
||||
async search(query: string, filters: SearchFilters = {}): Promise<Search> {
|
||||
async search(query: string, filters: MusicSearchFilters = {}): Promise<Search> {
|
||||
throwIfMissing({ query });
|
||||
|
||||
const payload: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import Actions from './Actions.js';
|
||||
import Player from './Player.js';
|
||||
|
||||
import HTTPClient from '../utils/HTTPClient.js';
|
||||
import { Platform, DeviceCategory, generateRandomString, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils.js';
|
||||
import { Platform, DeviceCategory, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils.js';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
|
||||
import Proto from '../proto/index.js';
|
||||
import { ICache } from '../types/Cache.js';
|
||||
@@ -232,7 +232,7 @@ export default class Session extends EventEmitterLike {
|
||||
'user-agent': getRandomUserAgent('desktop'),
|
||||
'accept': '*/*',
|
||||
'referer': 'https://www.youtube.com/sw.js',
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')}`
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${Constants.CLIENTS.WEB.STATIC_VISITOR_ID};`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -294,7 +294,7 @@ export default class Session extends EventEmitterLike {
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean
|
||||
}): SessionData {
|
||||
const id = generateRandomString(11);
|
||||
const id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const context: Context = {
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
# Parser
|
||||
|
||||
Sanitizes and standardizes InnerTube responses while maintaining the integrity of the data.
|
||||
|
||||
Structure:
|
||||
|
||||
* [`/classes`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/classes) - InnerTube nodes.
|
||||
* [`/types`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/types) - General response types.
|
||||
* [`/youtube`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube) - Contains the logic for parsing YouTube responses.
|
||||
* [`/ytmusic`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytmusic) - Contains the logic for parsing YouTube Music responses.
|
||||
* [`/ytkids`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytkids) - Contains the logic for parsing YouTube Kids responses.
|
||||
* [`helpers.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/helpers.ts) - Helper functions/classes for the parser.
|
||||
* [`parser.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/parser.ts) - The core of the parser.
|
||||
* [`map.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/map.ts) - A list of all InnerTube nodes, it is used to determine which node to use for a given renderer. Note that this file is auto-generated and should not be edited manually.
|
||||
The parser is responsible for sanitizing and standardizing InnerTube responses while preserving the integrity of the data.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
<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>
|
||||
- [Parser](#parser)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Structure](#structure)
|
||||
- [Core](#core)
|
||||
- [Clients](#clients)
|
||||
- [API](#api)
|
||||
- [`parse(data?: RawData, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[])`](#parsedata-rawdata-requirearray-boolean-validtypes-ytnodeconstructort--ytnodeconstructort)
|
||||
- [`parseResponse(data: IRawResponse): T`](#parseresponsedata-irawresponse-t)
|
||||
- [Usage](#usage)
|
||||
- [ObservedArray](#observedarray)
|
||||
- [SuperParsedResponse](#superparsedresponse)
|
||||
- [YTNode](#ytnode)
|
||||
- [Type Casting](#type-casting)
|
||||
- [Accessing properties without casting](#accessing-properties-without-casting)
|
||||
- [Memo](#memo)
|
||||
- [Adding new nodes](#adding-new-nodes)
|
||||
- [Generating nodes at runtime](#generating-nodes-at-runtime)
|
||||
- [How it works](#how-it-works)
|
||||
|
||||
___
|
||||
## Structure
|
||||
### Core
|
||||
|
||||
* [`/types`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/types) - General response types.
|
||||
* [`/classes`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/classes) - InnerTube nodes.
|
||||
* [`generator.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/generator.ts) - Used to generate missing nodes at runtime.
|
||||
* [`helpers.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/helpers.ts) - Helper functions/classes for the parser.
|
||||
* [`parser.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/parser.ts) - The core of the parser.
|
||||
* [`nodes.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/nodes.ts) - Contains a list of all the InnerTube nodes, which is used to determine the appropriate node for a given renderer. It's important to note that this file is automatically generated and should not be edited manually.
|
||||
* [`misc.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/misc.ts) - Miscellaneous classes for. Also automatically generated.
|
||||
|
||||
### Clients
|
||||
|
||||
The parser itself is not tied to any specific client. Therefore, we have a separate folder for each client that the library supports. These folders are responsible for arranging the parsed data into a format that can be easily consumed and understood. Additionally, the underlying data is also exposed for those who wish to access it.
|
||||
|
||||
* [`/youtube`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube)
|
||||
* [`/ytmusic`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytmusic)
|
||||
* [`/ytkids`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytkids)
|
||||
|
||||
## API
|
||||
|
||||
@@ -44,86 +52,82 @@ ___
|
||||
|
||||
<a name="parse"></a>
|
||||
|
||||
#### parse(data, requireArray, validTypes)
|
||||
### `parse(data?: RawData, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[])`
|
||||
|
||||
Responsible for parsing individual nodes.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| data | `any` | The data |
|
||||
| data | `RawData` | The data to parse |
|
||||
| requireArray | `?boolean` | Whether the response should be an array |
|
||||
| validTypes | `YTNodeConstructor<T> \| YTNodeConstructor<T>[] \| undefined` | Types of `YTNode` allowed |
|
||||
|
||||
When `requireArray` is `true`, the response will be an `ObservedArray<YTNodes>`.
|
||||
- If `requireArray` is `true`, the response will be an `ObservedArray<YTNodes>`.
|
||||
- If `validTypes` is `undefined`, the response will be an array of YTNodes.
|
||||
- If `validTypes` is an array, the response will be an array of YTNodes that are of the types specified in the array.
|
||||
- If `validTypes` is a single type, the response will be an array of YTNodes that are of the type specified.
|
||||
|
||||
When `validTypes` is `undefined`, the response will be an array of YTNodes.
|
||||
|
||||
When `validTypes` is an array, the response will be an array of YTNodes that are of the types specified in the array.
|
||||
|
||||
When `validTypes` is a single type, the response will be an array of YTNodes that are of the type specified.
|
||||
|
||||
If you do not specify `requireArray`, the return type of the function will not be known at runtime, and therefore we return the response wrapped in a helper, `SuperParsedResponse`, to gain access to the response.
|
||||
If you do not specify `requireArray`, the return type of the function will not be known at runtime. Therefore, to gain access to the response, we return it wrapped in a helper, `SuperParsedResponse`.
|
||||
|
||||
You may use the `Parser#parseArray` and `Parser#parseItem` methods to parse the response in a deterministic way.
|
||||
|
||||
<a name="parseresponse"></a>
|
||||
#### parseResponse(data)
|
||||
|
||||
### `parseResponse(data: IRawResponse): T`
|
||||
|
||||
Unlike `parse`, this can be used to parse the entire response object.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| data | `object` | Raw InnerTube response |
|
||||
| data | `IRawResponse` | 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.
|
||||
### ObservedArray
|
||||
You can utilize an `ObservedArray<T extends YTNode>` as a regular array, but it also offers further methods for accessing and casting values in a type-safe manner.
|
||||
|
||||
```ts
|
||||
// For example, we have a feed, and want all the videos:
|
||||
const feed = new ObservedArray<YTNode>([...feed.contents]);
|
||||
const videos = feed.filterType(GridVideo);
|
||||
// This is now a GridVideo[]
|
||||
|
||||
// Or we want only the first video:
|
||||
// Here, we use the filterType method to retrieve only GridVideo items from the feed.
|
||||
const videos = feed.filterType(GridVideo);
|
||||
// `videos` is now a GridVideo[] array.
|
||||
|
||||
// Alternatively, we can use firstOfType to retrieve the first GridVideo item from the feed.
|
||||
const firstVideo = feed.firstOfType(GridVideo);
|
||||
|
||||
// We may cast the whole array to a GridVideo[] and throw if we have any non-GridVideo elements:
|
||||
// If we want to make sure that all elements in the `feed` array are of the `GridVideo` type, we can use the `as` method to cast the entire array to a `GridVideo[]` type. If the cast fails because of non-GridVideo items, an exception is thrown.
|
||||
const allVideos = feed.as(GridVideo);
|
||||
|
||||
// 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.
|
||||
// Note that ObservedArray provides additional methods beyond what's shown here, which we use internally. For more information, see the source code or documentation.
|
||||
```
|
||||
|
||||
## SuperParsedResponse
|
||||
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.
|
||||
### SuperParsedResponse
|
||||
Represents a parsed response in an unknown state. Either a `YTNode`, an `ObservedArray<YTNode>`, or `null`. To extract the actual value, you must first assert the type and unwrap the response.
|
||||
|
||||
```ts
|
||||
// We can assert we have a YTNode:
|
||||
// First, parse the data and store it in `response`.
|
||||
const response = Parser.parse(data);
|
||||
|
||||
// Check whether `response` is a YTNode.
|
||||
if (response.is_item) {
|
||||
// If so, we can assert that it is a YTNode and retrieve it.
|
||||
const node = response.item();
|
||||
}
|
||||
|
||||
// We can assert we have an ObservedArray<YTNode>:
|
||||
const response = Parser.parse(data);
|
||||
// Check whether `response` is an ObservedArray<YTNode>.
|
||||
if (response.is_array) {
|
||||
// If so, we can assert that it is an ObservedArray<YTNode> and retrieve its contents as an array of YTNode objects.
|
||||
const nodes = response.array();
|
||||
}
|
||||
|
||||
// Or lastly a null response:
|
||||
const response = Parser.parse(data);
|
||||
// Finally, to check if `response` is a null value, use the `is_null` getter.
|
||||
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 what allows us a typesafe way to use data returned by the InnerTube API.
|
||||
### YTNode
|
||||
All renderers returned by InnerTube are converted to this generic class and then extended for the specific renderers. This class is what allows us a type-safe way to use data returned by the InnerTube API.
|
||||
|
||||
Here's how to use this class to access returned data:
|
||||
|
||||
@@ -131,10 +135,10 @@ Here's how to use this class to access returned data:
|
||||
```ts
|
||||
// We can cast a YTNode to a child class of YTNode
|
||||
const results = node.as(TwoColumnSearchResults);
|
||||
// This will throw if the node is not a TwoColumnSearchResults
|
||||
// We thus may want to check for the type of the node before casting
|
||||
// This will throw an error if the node is not a TwoColumnSearchResults.
|
||||
// Therefore, we may want to check for the type of the node before casting.
|
||||
if (node.is(TwoColumnSearchResults)) {
|
||||
// We do not need to recast the node, it is already a TwoColumnSearchResults after calling is() and using it in the branch where is() returns true
|
||||
// We do not need to recast the node; it is already a TwoColumnSearchResults after calling is() and using it in the branch where is() returns true.
|
||||
const results = node;
|
||||
}
|
||||
|
||||
@@ -144,21 +148,20 @@ const results = node.as(TwoColumnSearchResults, VideoList);
|
||||
|
||||
// Similarly, we can check if the node is of a certain type.
|
||||
if (node.is(TwoColumnSearchResults, VideoList)) {
|
||||
// Again no casting is needed, the node is already of the correct type.
|
||||
// // Again, no casting is needed; the node is already of the correct type.
|
||||
const results = node;
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
// Accessing a property on a node which you aren't sure if it exists.
|
||||
// Accessing a property on a node when 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
|
||||
// we may want to check for the key before accessing it.
|
||||
|
||||
// This returns the value wrapped into a `Maybe` type, which you can use to determine the type of the value.
|
||||
// However, this throws an error if the key doesn't exist, so we may want to check for the key before accessing it.
|
||||
if (node.hasKey("contents")) {
|
||||
const prop = node.key("contents");
|
||||
}
|
||||
@@ -169,19 +172,18 @@ if (prop.isString()) {
|
||||
const value = prop.string();
|
||||
}
|
||||
|
||||
// We can do more complex assertions too,
|
||||
// like checking for instanceof.
|
||||
// We can do more complex assertions, like checking for instanceof.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isInstanceof(Text)) {
|
||||
const text = prop.instanceof(Text);
|
||||
// and then use the value as the given type
|
||||
if (prop.isInstanceOf(Text)) {
|
||||
const text = prop.instanceOf(Text);
|
||||
|
||||
// Then use the value as the given type.
|
||||
text.runs.forEach(run => {
|
||||
console.log(run.text);
|
||||
});
|
||||
}
|
||||
|
||||
// There are some special methods for using with the parser —
|
||||
// such as getting the value as a YTNode.
|
||||
// There are special methods for use with the parser, such as getting the value as a YTNode.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isNode()) {
|
||||
const node = prop.node();
|
||||
@@ -200,13 +202,12 @@ if (prop.isNodeOfType([TwoColumnSearchResults, VideoList])) {
|
||||
}
|
||||
|
||||
// Sometimes an ObservedArray is returned when working with parsed data.
|
||||
// We've got a helper for that too;
|
||||
// We also have a helper for this.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isObserved()) {
|
||||
const array = prop.observed();
|
||||
|
||||
// Now we may use all the ObservedArray methods as normal,
|
||||
// like finding nodes of a certain type for example.
|
||||
// Now we can use all the ObservedArray methods as normal, such as finding nodes of a certain type.
|
||||
const results = array.filterType(GridVideo);
|
||||
}
|
||||
|
||||
@@ -215,8 +216,8 @@ const prop = node.key("contents");
|
||||
if (prop.isParsed()) {
|
||||
const result = prop.parsed();
|
||||
|
||||
// SuperParsedResult is another helper for typesafe access to the parsed data,
|
||||
// it is explained above with the `Parser#parse` method.
|
||||
// SuperParsedResult is another helper for type-safe access to the parsed data.
|
||||
// It is explained above with the `Parser#parse` method.
|
||||
const results = results.array();
|
||||
const videos = results.filterType(Video);
|
||||
}
|
||||
@@ -226,51 +227,106 @@ if (prop.isParsed()) {
|
||||
const prop = node.key("contents");
|
||||
const value = prop.any();
|
||||
|
||||
// Arrays are also a special case as every element may be of a different type,
|
||||
// the `arrayOfMaybe` method will return an array of `Maybe`s.
|
||||
// Arrays are a special case, as every element may be of a different type.
|
||||
// The `arrayOfMaybe` method will return an array of `Maybe`s.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isArray()) {
|
||||
const array = prop.arrayOfMaybe();
|
||||
// This will return Maybe[]
|
||||
// This will return `Maybe[]`.
|
||||
}
|
||||
|
||||
// Or if you want zero type safety you can use the `array` method.
|
||||
// Or, if you don't need type safety, you can use the `array` method.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isArray()) {
|
||||
const array = prop.array();
|
||||
// This will return any[]
|
||||
// This will return any[].
|
||||
}
|
||||
```
|
||||
|
||||
## Memo
|
||||
The `Memo` class is a helper class for memoizing values in the `Parser#parseResponse` method. It is useful for finding nodes after parsing the response.
|
||||
### Memo
|
||||
The `Memo` class is a helper class for memoizing values in the `Parser#parseResponse` method. It can be used to conveniently access nodes after parsing the response.
|
||||
|
||||
Say we want all of the videos in a search result. We can use the `Memo` to find all of them quickly without recursing through the response.
|
||||
For example, if we'd like to obtain all of the videos from a search result, we can use the `Memo#getType` method to find them quickly without needing to traverse the entire response.
|
||||
|
||||
```ts
|
||||
const response = Parser.parseResponse(data);
|
||||
const videos = response.contents_memo.getType(Video);
|
||||
// This returns the nodes as a ObservedArray<Video>.
|
||||
// This returns the nodes as an `ObservedArray<Video>`.
|
||||
```
|
||||
|
||||
`Memo` extends `Map<string, YTNode[]>` and can be used as a normal `Map` too if you want.
|
||||
`Memo` extends `Map<string, YTNode[]>` and can be used as a regular `Map` if desired.
|
||||
|
||||
## Adding new nodes
|
||||
Instructions can be found [here](https://github.com/LuanRT/YouTube.js/blob/main/docs/updating-the-parser.md).
|
||||
|
||||
## Generating nodes at runtime
|
||||
YouTube constantly updates their client, and sometimes they add new nodes to the response. The parser needs to know about these new nodes in order to parse them correctly. Once a new node is dicovered by the parser, it will attempt to generate a new node class for it.
|
||||
|
||||
Using the existing `YTNode` class, you may interact with these new nodes in a type-safe way. However, you will not be able to cast them to the node's specific type, as this requires the node to be defined at compile-time.
|
||||
|
||||
The current implementation recognises the following values:
|
||||
- Renderers
|
||||
- Renderer arrays
|
||||
- Text
|
||||
- Navigation endpoints
|
||||
- Author (does not currently detect the author thumbnails)
|
||||
- Thumbnails
|
||||
- Objects (key-value pairs)
|
||||
- Primatives (string, number, boolean, etc.)
|
||||
|
||||
This may be expanded in the future.
|
||||
|
||||
At runtime, these JIT-generated nodes will revalidate themselves when constructed so that when the types change, the node will be re-generated.
|
||||
|
||||
To access these nodes that have been generated at runtime, you may use the `Parser.getParserByName(name: string)` method. You may also check if a parser has been generated for a node by using the `Parser.hasParser(name: string)` method.
|
||||
|
||||
```ts
|
||||
import { Parser } from "youtubei.js";
|
||||
|
||||
// We may check if we have a parser for a node.
|
||||
if (Parser.hasParser('Example')) {
|
||||
// Then retrieve it.
|
||||
const Example = Parser.getParserByName('Example');
|
||||
// We may then use the parser as normal.
|
||||
const example = new Example(data);
|
||||
}
|
||||
```
|
||||
|
||||
You may also generate your own nodes ahead of time, given you have an example of one of the nodes.
|
||||
|
||||
```ts
|
||||
import { Generator } from "youtubei.js";
|
||||
|
||||
// Provided you have an example of the node `Example`
|
||||
const example_data = {
|
||||
"title": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Example"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// The first argument is the name of the class, the second is the data you have for the node.
|
||||
// It will return a class that extends YTNode.
|
||||
const Example = Generator.YTNodeGenerator.generateRuntimeClass('Example', example_data);
|
||||
|
||||
// You may now use this class as you would any other node.
|
||||
const example = new Example(example_data);
|
||||
|
||||
const title = example.key('title').instanceof(Text).toString();
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
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.
|
||||
If you decompile a YouTube client and analyze it, it becomes apparent that it uses classes such as `../youtube/api/innertube/MusicItemRenderer` and `../youtube/api/innertube/SectionListRenderer` to parse objects from the response, map them into models, and generate the UI. The website operates similarly, but instead uses plain JSON. You can think of renderers as components in a web framework.
|
||||
|
||||
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).
|
||||
Our approach is similar to YouTube's: our parser goes through all the renderers and parses their inner element(s). The final result is a nicely structured JSON, it even parses navigation endpoints, which allow us to make an API call with all required parameters in one line and emulate client actions, such as 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>
|
||||
<summary>Click to see</summary>
|
||||
<p>
|
||||
To illustrate the transformation we make, let's take an unstructured InnerTube response and parse it into a cleaner format:
|
||||
|
||||
Raw InnerTube Response:
|
||||
```js
|
||||
{
|
||||
sidebar: {
|
||||
@@ -309,14 +365,7 @@ Here is your average, arguably ugly InnerTube response:
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
And what we get after parsing it:
|
||||
<details>
|
||||
<summary>Click to see</summary>
|
||||
<p>
|
||||
|
||||
Clean Parsed Response:
|
||||
```js
|
||||
{
|
||||
sidebar: {
|
||||
@@ -324,21 +373,28 @@ And what we get after parsing it:
|
||||
contents: [
|
||||
{
|
||||
type: 'PlaylistSidebarPrimaryInfo',
|
||||
title: { text: '..' },
|
||||
description: { text: '..' },
|
||||
title: { text: '..', runs: [ { text: '..' } ] },
|
||||
description: { text: '..', runs: [ { text: '..' } ] },
|
||||
stats: [
|
||||
{
|
||||
text: '..'
|
||||
text: '..',
|
||||
runs: [
|
||||
{
|
||||
text: '..'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '..'
|
||||
text: '..',
|
||||
runs: [
|
||||
{
|
||||
text: '..'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
```
|
||||
@@ -39,7 +39,7 @@ class AccountItemSection extends YTNode {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = data.contents.map((ac: any) => new AccountItem(ac.accountItem));
|
||||
this.header = Parser.parseItem<AccountItemSectionHeader>(data.header, AccountItemSectionHeader);
|
||||
this.header = Parser.parseItem(data.header, AccountItemSectionHeader);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ class AccountSectionList extends YTNode {
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseItem<AccountItemSection>(data.contents[0], AccountItemSection);
|
||||
this.footers = Parser.parseItem<AccountChannel>(data.footers[0], AccountChannel);
|
||||
this.contents = Parser.parseItem(data.contents[0], AccountItemSection);
|
||||
this.footers = Parser.parseItem(data.footers[0], AccountChannel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import Parser from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import type CommentActionButtons from './comments/CommentActionButtons.js';
|
||||
import type Menu from './menus/Menu.js';
|
||||
import CommentActionButtons from './comments/CommentActionButtons.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
@@ -49,15 +49,15 @@ class BackstagePost extends YTNode {
|
||||
}
|
||||
|
||||
if (data.actionMenu) {
|
||||
this.menu = Parser.parseItem<Menu>(data.actionMenu);
|
||||
this.menu = Parser.parseItem(data.actionMenu, Menu);
|
||||
}
|
||||
|
||||
if (data.actionButtons) {
|
||||
this.action_buttons = Parser.parseItem<CommentActionButtons>(data.actionButtons);
|
||||
this.action_buttons = Parser.parseItem(data.actionButtons, CommentActionButtons);
|
||||
}
|
||||
|
||||
if (data.voteButton) {
|
||||
this.vote_button = Parser.parseItem(data.voteButton);
|
||||
this.vote_button = Parser.parseItem(data.voteButton, CommentActionButtons);
|
||||
}
|
||||
|
||||
if (data.navigationEndpoint) {
|
||||
|
||||
@@ -3,9 +3,9 @@ import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
import type Button from './Button.js';
|
||||
import type ChannelHeaderLinks from './ChannelHeaderLinks.js';
|
||||
import type SubscribeButton from './SubscribeButton.js';
|
||||
import Button from './Button.js';
|
||||
import ChannelHeaderLinks from './ChannelHeaderLinks.js';
|
||||
import SubscribeButton from './SubscribeButton.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
@@ -19,7 +19,7 @@ class C4TabbedHeader extends YTNode {
|
||||
subscribers?: Text;
|
||||
videos_count?: Text;
|
||||
sponsor_button?: Button | null;
|
||||
subscribe_button?: SubscribeButton | null;
|
||||
subscribe_button?: SubscribeButton | Button | null;
|
||||
header_links?: ChannelHeaderLinks | null;
|
||||
channel_handle?: Text;
|
||||
channel_id?: string;
|
||||
@@ -52,15 +52,15 @@ class C4TabbedHeader extends YTNode {
|
||||
}
|
||||
|
||||
if (data.sponsorButton) {
|
||||
this.sponsor_button = Parser.parseItem<Button>(data.sponsorButton);
|
||||
this.sponsor_button = Parser.parseItem(data.sponsorButton, Button);
|
||||
}
|
||||
|
||||
if (data.subscribeButton) {
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
|
||||
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
}
|
||||
|
||||
if (data.headerLinks) {
|
||||
this.header_links = Parser.parseItem<ChannelHeaderLinks>(data.headerLinks);
|
||||
this.header_links = Parser.parseItem(data.headerLinks, ChannelHeaderLinks);
|
||||
}
|
||||
|
||||
if (data.channelHandleText) {
|
||||
|
||||
@@ -4,7 +4,8 @@ import Text from './misc/Text.js';
|
||||
import Author from './misc/Author.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import type SubscribeButton from './SubscribeButton.js';
|
||||
import SubscribeButton from './SubscribeButton.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
@@ -18,7 +19,7 @@ class Channel extends YTNode {
|
||||
long_byline: Text;
|
||||
short_byline: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
subscribe_button: SubscribeButton | null;
|
||||
subscribe_button: SubscribeButton | Button | null;
|
||||
description_snippet: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
@@ -36,9 +37,9 @@ class Channel extends YTNode {
|
||||
this.long_byline = new Text(data.longBylineText);
|
||||
this.short_byline = new Text(data.shortBylineText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
|
||||
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
this.description_snippet = new Text(data.descriptionSnippet);
|
||||
}
|
||||
}
|
||||
|
||||
export default Channel;
|
||||
export default Channel;
|
||||
|
||||
@@ -4,7 +4,7 @@ import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import type Button from './Button.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
@@ -49,7 +49,7 @@ class ChannelAboutFullMetadata extends YTNode {
|
||||
this.email_reveal = new NavigationEndpoint(data.onBusinessEmailRevealClickCommand);
|
||||
this.can_reveal_email = !data.signInForBusinessEmail;
|
||||
this.country = new Text(data.country);
|
||||
this.buttons = Parser.parseArray<Button>(data.actionButtons);
|
||||
this.buttons = Parser.parseArray(data.actionButtons, Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class ChannelAgeGate extends YTNode {
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
this.header = new Text(data.header);
|
||||
this.main_text = new Text(data.mainText);
|
||||
this.sign_in_button = Parser.parseItem<Button>(data.signInButton, Button);
|
||||
this.sign_in_button = Parser.parseItem(data.signInButton, Button);
|
||||
this.secondary_text = new Text(data.secondaryText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
class ChannelFeaturedContent extends YTNode {
|
||||
static type = 'ChannelFeaturedContent';
|
||||
@@ -8,10 +8,10 @@ class ChannelFeaturedContent extends YTNode {
|
||||
title: Text;
|
||||
items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.items = Parser.parse(data.items);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ class ChipCloud extends YTNode {
|
||||
constructor(data: any) {
|
||||
super();
|
||||
// TODO: check this assumption that chipcloudchip is always returned
|
||||
this.chips = Parser.parseArray<ChipCloudChip>(data.chips, ChipCloudChip);
|
||||
this.next_button = Parser.parseItem<Button>(data.nextButton, Button);
|
||||
this.previous_button = Parser.parseItem<Button>(data.previousButton, Button);
|
||||
this.chips = Parser.parseArray(data.chips, ChipCloudChip);
|
||||
this.next_button = Parser.parseItem(data.nextButton, Button);
|
||||
this.previous_button = Parser.parseItem(data.previousButton, Button);
|
||||
this.horizontal_scrollable = data.horizontalScrollable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import type Menu from './menus/Menu.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class CompactChannel extends YTNode {
|
||||
@@ -28,7 +28,7 @@ class CompactChannel extends YTNode {
|
||||
this.subscriber_count = new Text(data.subscriberCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.tv_banner = Thumbnail.fromResponse(data.tvBanner);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Author from './misc/Author.js';
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import MetadataBadge from './MetadataBadge.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import type Menu from './menus/Menu.js';
|
||||
import MetadataBadge from './MetadataBadge.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class CompactVideo extends YTNode {
|
||||
static type = 'CompactVideo';
|
||||
@@ -31,7 +30,7 @@ class CompactVideo extends YTNode {
|
||||
endpoint: NavigationEndpoint;
|
||||
menu: Menu | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.id = data.videoId;
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail) || null;
|
||||
@@ -50,7 +49,7 @@ class CompactVideo extends YTNode {
|
||||
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
}
|
||||
|
||||
get best_thumbnail() {
|
||||
|
||||
@@ -15,8 +15,8 @@ class ConfirmDialog extends YTNode {
|
||||
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.confirm_button = Parser.parseItem(data.confirmButton, Button);
|
||||
this.cancel_button = Parser.parseItem(data.cancelButton, Button);
|
||||
this.dialog_messages = data.dialogMessages.map((txt: any) => new Text(txt));
|
||||
}
|
||||
}
|
||||
|
||||
16
src/parser/classes/ConversationBar.ts
Normal file
16
src/parser/classes/ConversationBar.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Message from './Message.js';
|
||||
|
||||
class ConversationBar extends YTNode {
|
||||
static type = 'ConversationBar';
|
||||
|
||||
availability_message: Message | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.availability_message = Parser.parseItem(data.availabilityMessage, Message);
|
||||
}
|
||||
}
|
||||
|
||||
export default ConversationBar;
|
||||
@@ -11,7 +11,7 @@ class CopyLink extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.copy_button = Parser.parseItem<Button>(data.copyButton, Button);
|
||||
this.copy_button = Parser.parseItem(data.copyButton, Button);
|
||||
this.short_url = data.shortUrl;
|
||||
this.style = data.style;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ class CreatePlaylistDialog extends YTNode {
|
||||
this.title = new Text(data.dialogTitle).toString();
|
||||
this.title_placeholder = data.titlePlaceholder || '';
|
||||
this.privacy_option = Parser.parseItem(data.privacyOption, Dropdown)?.entries || null;
|
||||
this.create_button = Parser.parseItem(data.cancelButton);
|
||||
this.cancel_button = Parser.parseItem(data.cancelButton);
|
||||
this.create_button = Parser.parseItem(data.cancelButton, Button);
|
||||
this.cancel_button = Parser.parseItem(data.cancelButton, Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type Button from './Button.js';
|
||||
import type MultiMarkersPlayerBar from './MultiMarkersPlayerBar.js';
|
||||
import Button from './Button.js';
|
||||
import MultiMarkersPlayerBar from './MultiMarkersPlayerBar.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class DecoratedPlayerBar extends YTNode {
|
||||
static type = 'DecoratedPlayerBar';
|
||||
@@ -9,10 +10,10 @@ class DecoratedPlayerBar extends YTNode {
|
||||
player_bar: MultiMarkersPlayerBar | null;
|
||||
player_bar_action_button: Button | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.player_bar = Parser.parseItem<MultiMarkersPlayerBar>(data.playerBar);
|
||||
this.player_bar_action_button = Parser.parseItem<Button>(data.playerBarActionButton);
|
||||
this.player_bar = Parser.parseItem(data.playerBar, MultiMarkersPlayerBar);
|
||||
this.player_bar_action_button = Parser.parseItem(data.playerBarActionButton, Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ class Element extends YTNode {
|
||||
super();
|
||||
|
||||
if (Reflect.has(data, 'elementRenderer')) {
|
||||
return Parser.parseItem<Element>(data, Element) as Element;
|
||||
return Parser.parseItem(data, Element) as Element;
|
||||
}
|
||||
|
||||
const type = data.newElement.type.componentType;
|
||||
this.model = Parser.parse(type?.model);
|
||||
this.model = Parser.parseItem(type?.model);
|
||||
|
||||
if (data.newElement?.childElements) {
|
||||
this.child_elements = data.newElement?.childElements?.map((el: any) => new ChildElement(el)) || null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Text from './misc/Text.js';
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
class EmergencyOnebox extends YTNode {
|
||||
static type = 'EmergencyOnebox';
|
||||
@@ -9,11 +9,11 @@ class EmergencyOnebox extends YTNode {
|
||||
first_option;
|
||||
menu;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.first_option = Parser.parse(data.firstOption);
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.first_option = Parser.parseItem(data.firstOption);
|
||||
this.menu = Parser.parseItem(data.menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class EndScreenVideo extends YTNode {
|
||||
static type = 'EndScreenVideo';
|
||||
@@ -22,12 +22,12 @@ class EndScreenVideo extends YTNode {
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.author = new Author(data.shortBylineText, data.ownerBadges);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
|
||||
@@ -32,9 +32,9 @@ class ExpandableMetadata extends YTNode {
|
||||
expanded_title: new Text(data.header.expandedTitle)
|
||||
};
|
||||
|
||||
this.expanded_content = Parser.parseItem<HorizontalCardList>(data.expandedContent);
|
||||
this.expand_button = Parser.parseItem<Button>(data.expandButton);
|
||||
this.collapse_button = Parser.parseItem<Button>(data.collapseButton);
|
||||
this.expanded_content = Parser.parseItem(data.expandedContent, HorizontalCardList);
|
||||
this.expand_button = Parser.parseItem(data.expandButton, Button);
|
||||
this.collapse_button = Parser.parseItem(data.collapseButton, Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class Grid extends YTNode {
|
||||
@@ -11,13 +11,13 @@ class Grid extends YTNode {
|
||||
continuation: string | null;
|
||||
header?;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.items = Parser.parseArray(data.items);
|
||||
|
||||
if (data.header) {
|
||||
this.header = Parser.parse(data.header);
|
||||
this.header = Parser.parseItem(data.header);
|
||||
}
|
||||
|
||||
if (data.isCollapsible) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Author from './misc/Author.js';
|
||||
import Parser from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
class GridChannel extends YTNode {
|
||||
static type = 'GridChannel';
|
||||
@@ -14,7 +14,7 @@ class GridChannel extends YTNode {
|
||||
endpoint: NavigationEndpoint;
|
||||
subscribe_button;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.id = data.channelId;
|
||||
|
||||
@@ -26,7 +26,7 @@ class GridChannel extends YTNode {
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
this.video_count = new Text(data.videoCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.subscribe_button = Parser.parse(data.subscribeButton);
|
||||
this.subscribe_button = Parser.parseItem(data.subscribeButton);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
38
src/parser/classes/GridMix.ts
Normal file
38
src/parser/classes/GridMix.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import Text from './misc/Text.js';
|
||||
import Parser from '../index.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class GridMix extends YTNode {
|
||||
static type = 'GridMix';
|
||||
|
||||
id: string;
|
||||
title: Text;
|
||||
author: Text | null;
|
||||
thumbnails: Thumbnail[];
|
||||
video_count: Text;
|
||||
video_count_short: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
secondary_endpoint: NavigationEndpoint;
|
||||
thumbnail_overlays;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.id = data.playlistId;
|
||||
this.title = new Text(data.title);
|
||||
|
||||
this.author = data.shortBylineText?.simpleText ?
|
||||
new Text(data.shortBylineText) : data.longBylineText?.simpleText ?
|
||||
new Text(data.longBylineText) : null;
|
||||
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.video_count = new Text(data.videoCountText);
|
||||
this.video_count_short = new Text(data.videoCountShortText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.secondary_endpoint = new NavigationEndpoint(data.secondaryNavigationEndpoint);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
}
|
||||
}
|
||||
|
||||
export default GridMix;
|
||||
34
src/parser/classes/GridMovie.ts
Normal file
34
src/parser/classes/GridMovie.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import MetadataBadge from './MetadataBadge.js';
|
||||
|
||||
class GridMovie extends YTNode {
|
||||
static type = 'GridMovie';
|
||||
|
||||
id: string;
|
||||
title: Text;
|
||||
thumbnails: Thumbnail[];
|
||||
duration: Text | null;
|
||||
endpoint: NavigationEndpoint;
|
||||
badges: MetadataBadge[];
|
||||
metadata: Text;
|
||||
thumbnail_overlays;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
const length_alt = data.thumbnailOverlays.find((overlay: any) => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer;
|
||||
this.id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.duration = data.lengthText ? new Text(data.lengthText) : length_alt?.text ? new Text(length_alt.text) : null;
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.badges = Parser.parseArray<MetadataBadge>(data.badges, MetadataBadge);
|
||||
this.metadata = new Text(data.metadata);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
}
|
||||
}
|
||||
|
||||
export default GridMovie;
|
||||
@@ -1,40 +1,39 @@
|
||||
import Text from './misc/Text.js';
|
||||
import Parser from '../index.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import PlaylistAuthor from './misc/PlaylistAuthor.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import NavigatableText from './misc/NavigatableText.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
class GridPlaylist extends YTNode {
|
||||
static type = 'GridPlaylist';
|
||||
|
||||
id: string;
|
||||
title: Text;
|
||||
author?: PlaylistAuthor;
|
||||
author?: Author;
|
||||
badges;
|
||||
endpoint: NavigationEndpoint;
|
||||
view_playlist: NavigatableText;
|
||||
view_playlist: Text;
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_renderer;
|
||||
sidebar_thumbnails: Thumbnail[] | null;
|
||||
video_count: Text;
|
||||
video_count_short: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.id = data.playlistId;
|
||||
this.title = new Text(data.title);
|
||||
|
||||
if (data.shortBylineText) {
|
||||
this.author = new PlaylistAuthor(data.shortBylineText, data.ownerBadges);
|
||||
this.author = new Author(data.shortBylineText, data.ownerBadges);
|
||||
}
|
||||
|
||||
this.badges = Parser.parse(data.ownerBadges);
|
||||
this.badges = Parser.parseArray(data.ownerBadges);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.view_playlist = new NavigatableText(data.viewPlaylistText);
|
||||
this.view_playlist = new Text(data.viewPlaylistText);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_renderer = Parser.parse(data.thumbnailRenderer);
|
||||
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer);
|
||||
this.sidebar_thumbnails = [].concat(...data.sidebarThumbnails?.map((thumbnail: any) => Thumbnail.fromResponse(thumbnail)) || []) || null;
|
||||
this.video_count = new Text(data.thumbnailText);
|
||||
this.video_count_short = new Text(data.videoCountShortText);
|
||||
|
||||
29
src/parser/classes/GridShow.ts
Normal file
29
src/parser/classes/GridShow.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ObservedArray, YTNode } from '../helpers.js';
|
||||
import { RawNode } from '../index.js';
|
||||
import Parser from '../parser.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import ShowCustomThumbnail from './ShowCustomThumbnail.js';
|
||||
import ThumbnailOverlayBottomPanel from './ThumbnailOverlayBottomPanel.js';
|
||||
|
||||
export default class GridShow extends YTNode {
|
||||
static type = 'GridShow';
|
||||
|
||||
title: Text;
|
||||
thumbnail_renderer: ShowCustomThumbnail | null;
|
||||
endpoint: NavigationEndpoint;
|
||||
long_byline_text: Text;
|
||||
thumbnail_overlays: ObservedArray<ThumbnailOverlayBottomPanel> | null;
|
||||
author: Author;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, ShowCustomThumbnail);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.long_byline_text = new Text(data.longBylineText);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays, ThumbnailOverlayBottomPanel);
|
||||
this.author = new Author(data.shortBylineText, undefined);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Author from './misc/Author.js';
|
||||
|
||||
import type Menu from './menus/Menu.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
@@ -24,21 +24,21 @@ class GridVideo extends YTNode {
|
||||
endpoint: NavigationEndpoint;
|
||||
menu: Menu | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
const length_alt = data.thumbnailOverlays.find((overlay: any) => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer;
|
||||
this.id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.rich_thumbnail = data.richThumbnail && Parser.parse(data.richThumbnail);
|
||||
this.rich_thumbnail = data.richThumbnail && Parser.parseItem(data.richThumbnail);
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
this.duration = data.lengthText ? new Text(data.lengthText) : length_alt?.text ? new Text(length_alt.text) : null;
|
||||
this.author = data.shortBylineText && new Author(data.shortBylineText, data.ownerBadges);
|
||||
this.views = new Text(data.viewCountText);
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parser from '../index.js';
|
||||
import type HeatMarker from './HeatMarker.js';
|
||||
import HeatMarker from './HeatMarker.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
@@ -17,7 +17,7 @@ class Heatmap extends YTNode {
|
||||
this.max_height_dp = data.maxHeightDp;
|
||||
this.min_height_dp = data.minHeightDp;
|
||||
this.show_hide_animation_duration_millis = data.showHideAnimationDurationMillis;
|
||||
this.heat_markers = Parser.parseArray<HeatMarker>(data.heatMarkers);
|
||||
this.heat_markers = Parser.parseArray(data.heatMarkers, HeatMarker);
|
||||
this.heat_markers_decorations = Parser.parseArray(data.heatMarkersDecorations);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { YTNode } from '../helpers.js';
|
||||
import SearchRefinementCard from './SearchRefinementCard.js';
|
||||
import Button from './Button.js';
|
||||
import MacroMarkersListItem from './MacroMarkersListItem.js';
|
||||
import GameCard from './GameCard.js';
|
||||
import VideoCard from './VideoCard.js';
|
||||
|
||||
class HorizontalCardList extends YTNode {
|
||||
static type = 'HorizontalCardList';
|
||||
@@ -14,10 +16,10 @@ class HorizontalCardList extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.cards = Parser.parseArray<SearchRefinementCard | MacroMarkersListItem>(data.cards);
|
||||
this.cards = Parser.parseArray(data.cards, [ SearchRefinementCard, MacroMarkersListItem, GameCard, VideoCard ]);
|
||||
this.header = Parser.parseItem(data.header);
|
||||
this.previous_button = Parser.parseItem<Button>(data.previousButton, Button);
|
||||
this.next_button = Parser.parseItem<Button>(data.nextButton, Button);
|
||||
this.previous_button = Parser.parseItem(data.previousButton, Button);
|
||||
this.next_button = Parser.parseItem(data.nextButton, Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
src/parser/classes/HorizontalMovieList.ts
Normal file
25
src/parser/classes/HorizontalMovieList.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
class HorizontalMovieList extends YTNode {
|
||||
static type = 'HorizontalMovieList';
|
||||
|
||||
items;
|
||||
previous_button: Button | null;
|
||||
next_button: Button | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.items = Parser.parseArray(data.items);
|
||||
this.previous_button = Parser.parseItem<Button>(data.previousButton, Button);
|
||||
this.next_button = Parser.parseItem<Button>(data.nextButton, Button);
|
||||
}
|
||||
|
||||
// XXX: alias for consistency
|
||||
get contents() {
|
||||
return this.items;
|
||||
}
|
||||
}
|
||||
|
||||
export default HorizontalMovieList;
|
||||
27
src/parser/classes/InfoPanelContainer.ts
Normal file
27
src/parser/classes/InfoPanelContainer.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import InfoPanelContent from './InfoPanelContent.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class InfoPanelContainer extends YTNode {
|
||||
static type = 'InfoPanelContainer';
|
||||
|
||||
icon_type?: string;
|
||||
title: Text;
|
||||
menu: Menu | null;
|
||||
content: YTNode | null;
|
||||
background: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
this.content = Parser.parseItem(data.content, InfoPanelContent);
|
||||
this.background = data.background;
|
||||
|
||||
if (data.icon?.iconType) {
|
||||
this.icon_type = data.icon.iconType;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/parser/classes/InfoPanelContent.ts
Normal file
33
src/parser/classes/InfoPanelContent.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
export default class InfoPanelContent extends YTNode {
|
||||
static type = 'InfoPanelContent';
|
||||
|
||||
title: Text;
|
||||
inline_link_icon_type?: string;
|
||||
source: Text;
|
||||
paragraphs: Text[];
|
||||
thumbnail: Thumbnail[];
|
||||
source_endpoint: NavigationEndpoint;
|
||||
truncate_paragraphs: boolean;
|
||||
background: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.source = new Text(data.source);
|
||||
this.paragraphs = data.paragraphs.map((p: RawNode) => new Text(p));
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.source_endpoint = new NavigationEndpoint(data.sourceEndpoint);
|
||||
this.truncate_paragraphs = !!data.truncateParagraphs;
|
||||
this.background = data.background;
|
||||
|
||||
if (data.inlineLinkIcon?.iconType) {
|
||||
this.inline_link_icon_type = data.inlineLinkIcon.iconType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class InteractiveTabbedHeader extends YTNode {
|
||||
this.badges = Parser.parseArray<MetadataBadge>(data.badges, MetadataBadge);
|
||||
this.box_art = Thumbnail.fromResponse(data.boxArt);
|
||||
this.banner = Thumbnail.fromResponse(data.banner);
|
||||
this.buttons = Parser.parseArray<SubscribeButton | Button>(data.buttons, [ SubscribeButton, Button ]);
|
||||
this.buttons = Parser.parseArray(data.buttons, [ SubscribeButton, Button ]);
|
||||
this.auto_generated = new Text(data.autoGenerated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class ItemSection extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.header = Parser.parseItem<CommentsHeader | ItemSectionHeader | ItemSectionTabbedHeader>(data.header);
|
||||
this.header = Parser.parseItem(data.header, [ CommentsHeader, ItemSectionHeader, ItemSectionTabbedHeader ]);
|
||||
this.contents = Parser.parse(data.contents, true);
|
||||
|
||||
if (data.targetId || data.sectionIdentifier) {
|
||||
|
||||
@@ -13,7 +13,7 @@ class ItemSectionTabbedHeader extends YTNode {
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.tabs = Parser.parseArray<ItemSectionTab>(data.tabs, ItemSectionTab);
|
||||
this.tabs = Parser.parseArray(data.tabs, ItemSectionTab);
|
||||
if (data.endItems) {
|
||||
this.end_items = Parser.parseArray(data.endItems);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
class LiveChat extends YTNode {
|
||||
static type = 'LiveChat';
|
||||
@@ -19,9 +19,9 @@ class LiveChat extends YTNode {
|
||||
|
||||
is_replay: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.header = Parser.parse(data.header);
|
||||
this.header = Parser.parseItem(data.header);
|
||||
this.initial_display_state = data.initialDisplayState;
|
||||
this.continuation = data.continuations[0]?.reloadContinuationData?.continuation;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Parser from '../index.js';
|
||||
import type Menu from './menus/Menu.js';
|
||||
import type Button from './Button.js';
|
||||
import type SortFilterSubMenu from './SortFilterSubMenu.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Button from './Button.js';
|
||||
import SortFilterSubMenu from './SortFilterSubMenu.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class LiveChatHeader extends YTNode {
|
||||
@@ -13,9 +13,9 @@ class LiveChatHeader extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.overflow_menu = Parser.parseItem<Menu>(data.overflowMenu);
|
||||
this.collapse_button = Parser.parseItem<Button>(data.collapseButton);
|
||||
this.view_selector = Parser.parseItem<SortFilterSubMenu>(data.viewSelector);
|
||||
this.overflow_menu = Parser.parseItem(data.overflowMenu, Menu);
|
||||
this.collapse_button = Parser.parseItem(data.collapseButton, Button);
|
||||
this.view_selector = Parser.parseItem(data.viewSelector, SortFilterSubMenu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type Button from './Button.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
class LiveChatItemList extends YTNode {
|
||||
static type = 'LiveChatItemList';
|
||||
@@ -11,7 +11,7 @@ class LiveChatItemList extends YTNode {
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.max_items_to_display = data.maxItemsToDisplay;
|
||||
this.more_comments_below_button = Parser.parseItem<Button>(data.moreCommentsBelowButton);
|
||||
this.more_comments_below_button = Parser.parseItem(data.moreCommentsBelowButton, Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Text from './misc/Text.js';
|
||||
import Parser from '../index.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import type Button from './Button.js';
|
||||
import Button from './Button.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class LiveChatMessageInput extends YTNode {
|
||||
@@ -16,7 +16,7 @@ class LiveChatMessageInput extends YTNode {
|
||||
super();
|
||||
this.author_name = new Text(data.authorName);
|
||||
this.author_photo = Thumbnail.fromResponse(data.authorPhoto);
|
||||
this.send_button = Parser.parseItem<Button>(data.sendButton);
|
||||
this.send_button = Parser.parseItem(data.sendButton, Button);
|
||||
this.target_id = data.targetId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import { ObservedArray, YTNode } from '../helpers.js';
|
||||
import type LiveChatParticipant from './LiveChatParticipant.js';
|
||||
import LiveChatParticipant from './LiveChatParticipant.js';
|
||||
|
||||
class LiveChatParticipantsList extends YTNode {
|
||||
static type = 'LiveChatParticipantsList';
|
||||
@@ -12,7 +12,7 @@ class LiveChatParticipantsList extends YTNode {
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.participants = Parser.parseArray<LiveChatParticipant>(data.participants);
|
||||
this.participants = Parser.parseArray(data.participants, LiveChatParticipant);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
|
||||
class MerchandiseShelf extends YTNode {
|
||||
static type = 'MerchandiseShelf';
|
||||
@@ -8,11 +8,11 @@ class MerchandiseShelf extends YTNode {
|
||||
menu;
|
||||
items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = data.title;
|
||||
this.menu = Parser.parse(data.actionButton);
|
||||
this.items = Parser.parse(data.items);
|
||||
this.menu = Parser.parseItem(data.actionButton);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
|
||||
// XXX: alias for consistency
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
@@ -28,7 +28,7 @@ class Movie extends YTNode {
|
||||
show_action_menu: boolean;
|
||||
menu;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
const overlay_time_status = data.thumbnailOverlays
|
||||
.find((overlay: any) => overlay.thumbnailOverlayTimeStatusRenderer)
|
||||
@@ -39,19 +39,19 @@ class Movie extends YTNode {
|
||||
this.description_snippet = data.descriptionSnippet ? new Text(data.descriptionSnippet) : null;
|
||||
this.top_metadata_items = new Text(data.topMetadataItems);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.author = new Author(data.longBylineText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail);
|
||||
|
||||
this.duration = {
|
||||
text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text,
|
||||
seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text)
|
||||
text: data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString(),
|
||||
seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString())
|
||||
};
|
||||
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.badges = Parser.parse(data.badges);
|
||||
this.use_vertical_poster = data.useVerticalPoster;
|
||||
this.show_action_menu = data.showActionMenu;
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.menu = Parser.parseItem(data.menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index.js';
|
||||
import type Chapter from './Chapter.js';
|
||||
import type Heatmap from './Heatmap.js';
|
||||
import Chapter from './Chapter.js';
|
||||
import Heatmap from './Heatmap.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
import { observe, ObservedArray, YTNode } from '../helpers.js';
|
||||
|
||||
@@ -13,18 +14,18 @@ class Marker extends YTNode {
|
||||
chapters?: Chapter[];
|
||||
};
|
||||
|
||||
constructor (data: any) {
|
||||
constructor (data: RawNode) {
|
||||
super();
|
||||
this.marker_key = data.key;
|
||||
|
||||
this.value = {};
|
||||
|
||||
if (data.value.heatmap) {
|
||||
this.value.heatmap = Parser.parseItem<Heatmap>(data.value.heatmap);
|
||||
this.value.heatmap = Parser.parseItem(data.value.heatmap, Heatmap);
|
||||
}
|
||||
|
||||
if (data.value.chapters) {
|
||||
this.value.chapters = Parser.parseArray<Chapter>(data.value.chapters);
|
||||
this.value.chapters = Parser.parseArray(data.value.chapters, Chapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,9 +35,12 @@ class MultiMarkersPlayerBar extends YTNode {
|
||||
|
||||
markers_map: ObservedArray<Marker>;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.markers_map = observe(data.markersMap?.map((marker: { key: string; value: { [key: string ]: any }}) => new Marker(marker)));
|
||||
this.markers_map = observe(data.markersMap?.map((marker: {
|
||||
key: string;
|
||||
value: { [key: string ]: any
|
||||
}}) => new Marker(marker)) || []);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
48
src/parser/classes/MusicCardShelf.ts
Normal file
48
src/parser/classes/MusicCardShelf.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ObservedArray, YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Text from './misc/Text.js';
|
||||
import MusicCardShelfHeaderBasic from './MusicCardShelfHeaderBasic.js';
|
||||
import MusicInlineBadge from './MusicInlineBadge.js';
|
||||
import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay.js';
|
||||
import MusicThumbnail from './MusicThumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
export default class MusicCardShelf extends YTNode {
|
||||
static type = 'MusicCardShelf';
|
||||
|
||||
thumbnail: MusicThumbnail | null;
|
||||
title: Text;
|
||||
subtitle: Text;
|
||||
buttons: ObservedArray<Button> | null;
|
||||
menu: Menu | null;
|
||||
on_tap: NavigationEndpoint;
|
||||
header: MusicCardShelfHeaderBasic | null;
|
||||
end_icon_type?: string;
|
||||
subtitle_badges: ObservedArray<MusicInlineBadge>;
|
||||
thumbnail_overlay: MusicItemThumbnailOverlay | null;
|
||||
contents?: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
|
||||
this.title = new Text(data.title);
|
||||
this.subtitle = new Text(data.subtitle);
|
||||
this.buttons = Parser.parseArray(data.buttons, Button);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
this.on_tap = new NavigationEndpoint(data.onTap);
|
||||
this.header = Parser.parseItem(data.header, MusicCardShelfHeaderBasic);
|
||||
|
||||
if (Reflect.has(data, 'endIcon') && Reflect.has(data.endIcon, 'iconType')) {
|
||||
this.end_icon_type = data.endIcon.iconType;
|
||||
}
|
||||
|
||||
this.subtitle_badges = Parser.parseArray(data.subtitleBadges, MusicInlineBadge);
|
||||
this.thumbnail_overlay = Parser.parseItem(data.thumbnailOverlay, MusicItemThumbnailOverlay);
|
||||
|
||||
if (Reflect.has(data, 'contents')) {
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/parser/classes/MusicCardShelfHeaderBasic.ts
Normal file
14
src/parser/classes/MusicCardShelfHeaderBasic.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class MusicCardShelfHeaderBasic extends YTNode {
|
||||
static type = 'MusicCardShelfHeaderBasic';
|
||||
|
||||
title: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,19 @@ import MusicResponsiveListItem from './MusicResponsiveListItem.js';
|
||||
import MusicCarouselShelfBasicHeader from './MusicCarouselShelfBasicHeader.js';
|
||||
import MusicNavigationButton from './MusicNavigationButton.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { ObservedArray, YTNode } from '../helpers.js';
|
||||
|
||||
class MusicCarouselShelf extends YTNode {
|
||||
static type = 'MusicCarouselShelf';
|
||||
|
||||
header: MusicCarouselShelfBasicHeader | null;
|
||||
contents: Array<MusicTwoRowItem | MusicResponsiveListItem | MusicNavigationButton>;
|
||||
contents: ObservedArray<MusicTwoRowItem | MusicResponsiveListItem | MusicNavigationButton>;
|
||||
num_items_per_column: number | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.header = Parser.parseItem<MusicCarouselShelfBasicHeader>(data.header, MusicCarouselShelfBasicHeader);
|
||||
this.contents = Parser.parseArray<MusicTwoRowItem | MusicResponsiveListItem | MusicNavigationButton>(data.contents, [ MusicTwoRowItem, MusicResponsiveListItem, MusicNavigationButton ]);
|
||||
this.contents = Parser.parseArray(data.contents, [ MusicTwoRowItem, MusicResponsiveListItem, MusicNavigationButton ]);
|
||||
this.num_items_per_column = Reflect.has(data, 'numItemsPerColumn') ? parseInt(data.numItemsPerColumn) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import TextRun from './misc/TextRun.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class MusicDetailHeader extends YTNode {
|
||||
static type = 'MusicDetailHeader';
|
||||
@@ -24,7 +24,7 @@ class MusicDetailHeader extends YTNode {
|
||||
};
|
||||
menu;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.description = new Text(data.description);
|
||||
@@ -46,7 +46,7 @@ class MusicDetailHeader extends YTNode {
|
||||
};
|
||||
}
|
||||
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.menu = Parser.parseItem(data.menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class MusicEditablePlaylistDetailHeader extends YTNode {
|
||||
@@ -6,9 +6,9 @@ class MusicEditablePlaylistDetailHeader extends YTNode {
|
||||
|
||||
header;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.header = Parser.parse(data.header);
|
||||
this.header = Parser.parseItem(data.header);
|
||||
|
||||
// TODO: Should we also parse data.editHeader.musicPlaylistEditHeaderRenderer?
|
||||
// It doesn't seem practical to do so...
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
@@ -8,11 +8,11 @@ class MusicHeader extends YTNode {
|
||||
header?;
|
||||
title?: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
if (data.header) {
|
||||
this.header = Parser.parse(data.header);
|
||||
this.header = Parser.parseItem(data.header);
|
||||
}
|
||||
|
||||
if (data.title) {
|
||||
|
||||
@@ -37,7 +37,7 @@ class MusicShelf extends YTNode {
|
||||
}
|
||||
|
||||
if (data.bottomButton) {
|
||||
this.bottom_button = Parser.parseItem<Button>(data.bottomButton);
|
||||
this.bottom_button = Parser.parseItem(data.bottomButton, Button);
|
||||
}
|
||||
|
||||
if (data.subheaders) {
|
||||
|
||||
@@ -14,7 +14,7 @@ class MusicSortFilterButton extends YTNode {
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.title = new Text(data.title).text;
|
||||
this.title = new Text(data.title).toString();
|
||||
this.icon_type = data.icon?.icon_type || null;
|
||||
this.menu = Parser.parseItem(data.menu, MusicMultiSelectMenu);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class Notification extends YTNode {
|
||||
static type = 'Notification';
|
||||
@@ -11,13 +11,13 @@ class Notification extends YTNode {
|
||||
video_thumbnails: Thumbnail[];
|
||||
short_message: Text;
|
||||
sent_time: Text;
|
||||
notification_id: any;
|
||||
notification_id: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
record_click_endpoint: NavigationEndpoint;
|
||||
menu;
|
||||
read: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.video_thumbnails = Thumbnail.fromResponse(data.videoThumbnail);
|
||||
@@ -26,7 +26,7 @@ class Notification extends YTNode {
|
||||
this.notification_id = data.notificationId;
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.record_click_endpoint = new NavigationEndpoint(data.recordClickEndpoint);
|
||||
this.menu = Parser.parse(data.contextualMenu);
|
||||
this.menu = Parser.parseItem(data.contextualMenu);
|
||||
this.read = data.read;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode, SuperParsedResult } from '../helpers.js';
|
||||
|
||||
class PlayerAnnotationsExpanded extends YTNode {
|
||||
static type = 'PlayerAnnotationsExpanded';
|
||||
|
||||
featured_channel: {
|
||||
featured_channel?: {
|
||||
start_time_ms: number;
|
||||
end_time_ms: number;
|
||||
watermark: Thumbnail[];
|
||||
channel_name: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
subscribe_button: SuperParsedResult<YTNode>;
|
||||
subscribe_button: YTNode | null;
|
||||
};
|
||||
|
||||
allow_swipe_dismiss: boolean;
|
||||
annotation_id: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.featured_channel = {
|
||||
start_time_ms: data.featuredChannel.startTimeMs,
|
||||
end_time_ms: data.featuredChannel.endTimeMs,
|
||||
watermark: Thumbnail.fromResponse(data.featuredChannel.watermark),
|
||||
channel_name: data.featuredChannel.channelName,
|
||||
endpoint: new NavigationEndpoint(data.featuredChannel.navigationEndpoint),
|
||||
subscribe_button: Parser.parse(data.featuredChannel.subscribeButton)
|
||||
};
|
||||
if (Reflect.has(data, 'featuredChannel')) {
|
||||
this.featured_channel = {
|
||||
start_time_ms: data.featuredChannel.startTimeMs,
|
||||
end_time_ms: data.featuredChannel.endTimeMs,
|
||||
watermark: Thumbnail.fromResponse(data.featuredChannel.watermark),
|
||||
channel_name: data.featuredChannel.channelName,
|
||||
endpoint: new NavigationEndpoint(data.featuredChannel.navigationEndpoint),
|
||||
subscribe_button: Parser.parseItem(data.featuredChannel.subscribeButton)
|
||||
};
|
||||
}
|
||||
|
||||
this.allow_swipe_dismiss = data.allowSwipeDismiss;
|
||||
this.annotation_id = data.annotationId;
|
||||
|
||||
32
src/parser/classes/PlayerLegacyDesktopYpcTrailer.ts
Normal file
32
src/parser/classes/PlayerLegacyDesktopYpcTrailer.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, RawNode } from '../index.js';
|
||||
import YpcTrailer from './YpcTrailer.js';
|
||||
|
||||
class PlayerLegacyDesktopYpcTrailer extends YTNode {
|
||||
static type = 'PlayerLegacyDesktopYpcTrailer';
|
||||
|
||||
video_id: string;
|
||||
title: string;
|
||||
thumbnail: string;
|
||||
offer_headline: string;
|
||||
offer_description: string;
|
||||
offer_id: string;
|
||||
offer_button_text: string;
|
||||
video_message: string;
|
||||
trailer: YpcTrailer | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.video_id = data.trailerVideoId;
|
||||
this.title = data.itemTitle;
|
||||
this.thumbnail = data.itemThumbnail;
|
||||
this.offer_headline = data.offerHeadline;
|
||||
this.offer_description = data.offerDescription;
|
||||
this.offer_id = data.offerId;
|
||||
this.offer_button_text = data.offerButtonText;
|
||||
this.video_message = data.fullVideoMessage;
|
||||
this.trailer = Parser.parseItem<YpcTrailer>(data.ypcTrailer, YpcTrailer);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlayerLegacyDesktopYpcTrailer;
|
||||
@@ -1,34 +1,34 @@
|
||||
import Parser from '../index.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import WatchNextEndScreen from './WatchNextEndScreen.js';
|
||||
import DecoratedPlayerBar from './DecoratedPlayerBar.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import PlayerOverlayAutoplay from './PlayerOverlayAutoplay.js';
|
||||
import type DecoratedPlayerBar from './DecoratedPlayerBar.js';
|
||||
import WatchNextEndScreen from './WatchNextEndScreen.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class PlayerOverlay extends YTNode {
|
||||
static type = 'PlayerOverlay';
|
||||
|
||||
end_screen;
|
||||
autoplay;
|
||||
share_button;
|
||||
add_to_menu;
|
||||
end_screen: WatchNextEndScreen | null;
|
||||
autoplay: PlayerOverlayAutoplay | null;
|
||||
share_button: Button | null;
|
||||
add_to_menu: Menu | null;
|
||||
fullscreen_engagement;
|
||||
actions;
|
||||
browser_media_session;
|
||||
decorated_player_bar;
|
||||
decorated_player_bar: DecoratedPlayerBar | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.end_screen = Parser.parseItem<WatchNextEndScreen>(data.endScreen, WatchNextEndScreen);
|
||||
this.autoplay = Parser.parseItem<PlayerOverlayAutoplay>(data.autoplay, PlayerOverlayAutoplay);
|
||||
this.share_button = Parser.parseItem<Button>(data.shareButton, Button);
|
||||
this.add_to_menu = Parser.parseItem<Menu>(data.addToMenu, Menu);
|
||||
this.fullscreen_engagement = Parser.parse(data.fullscreenEngagement);
|
||||
this.end_screen = Parser.parseItem(data.endScreen, WatchNextEndScreen);
|
||||
this.autoplay = Parser.parseItem(data.autoplay, PlayerOverlayAutoplay);
|
||||
this.share_button = Parser.parseItem(data.shareButton, Button);
|
||||
this.add_to_menu = Parser.parseItem(data.addToMenu, Menu);
|
||||
this.fullscreen_engagement = Parser.parseItem(data.fullscreenEngagement);
|
||||
this.actions = Parser.parseArray(data.actions);
|
||||
this.browser_media_session = Parser.parseItem(data.browserMediaSession);
|
||||
this.decorated_player_bar = Parser.parseItem<DecoratedPlayerBar>(data.decoratedPlayerBarRenderer);
|
||||
this.decorated_player_bar = Parser.parseItem(data.decoratedPlayerBarRenderer, DecoratedPlayerBar);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
@@ -18,11 +18,11 @@ class PlayerOverlayAutoplay extends YTNode {
|
||||
background: Thumbnail[];
|
||||
thumbnail_overlays;
|
||||
author: Author;
|
||||
cancel_button;
|
||||
next_button;
|
||||
close_button;
|
||||
cancel_button: Button | null;
|
||||
next_button: Button | null;
|
||||
close_button: Button | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.video_id = data.videoId;
|
||||
@@ -32,11 +32,11 @@ class PlayerOverlayAutoplay extends YTNode {
|
||||
this.count_down_secs_for_fullscreen = data.countDownSecsForFullscreen;
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
this.background = Thumbnail.fromResponse(data.background);
|
||||
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.author = new Author(data.byline);
|
||||
this.cancel_button = Parser.parseItem<Button>(data.cancelButton, Button);
|
||||
this.next_button = Parser.parseItem<Button>(data.nextButton, Button);
|
||||
this.close_button = Parser.parseItem<Button>(data.closeButton, Button);
|
||||
this.cancel_button = Parser.parseItem(data.cancelButton, Button);
|
||||
this.next_button = Parser.parseItem(data.nextButton, Button);
|
||||
this.close_button = Parser.parseItem(data.closeButton, Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Text from './misc/Text.js';
|
||||
import Parser from '../index.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import PlaylistAuthor from './misc/PlaylistAuthor.js';
|
||||
import Author from './misc/Author.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class Playlist extends YTNode {
|
||||
@@ -10,7 +10,7 @@ class Playlist extends YTNode {
|
||||
|
||||
id: string;
|
||||
title: Text;
|
||||
author: Text | PlaylistAuthor;
|
||||
author: Text | Author;
|
||||
thumbnails: Thumbnail[];
|
||||
video_count: Text;
|
||||
video_count_short: Text;
|
||||
@@ -20,6 +20,7 @@ class Playlist extends YTNode {
|
||||
badges;
|
||||
endpoint: NavigationEndpoint;
|
||||
thumbnail_overlays;
|
||||
view_playlist?: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -28,7 +29,7 @@ class Playlist extends YTNode {
|
||||
|
||||
this.author = data.shortBylineText?.simpleText ?
|
||||
new Text(data.shortBylineText) :
|
||||
new PlaylistAuthor(data.longBylineText, data.ownerBadges, null);
|
||||
new Author(data.longBylineText, data.ownerBadges, null);
|
||||
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail || { thumbnails: data.thumbnails.map((th: any) => th.thumbnails).flat(1) });
|
||||
this.video_count = new Text(data.thumbnailText);
|
||||
@@ -39,6 +40,10 @@ class Playlist extends YTNode {
|
||||
this.badges = Parser.parseArray(data.ownerBadges);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
|
||||
if (data.viewPlaylistText) {
|
||||
this.view_playlist = new Text(data.viewPlaylistText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Text from './misc/Text.js';
|
||||
import PlaylistAuthor from './misc/PlaylistAuthor.js';
|
||||
import Parser from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class PlaylistHeader extends YTNode {
|
||||
@@ -10,7 +10,7 @@ class PlaylistHeader extends YTNode {
|
||||
title: Text;
|
||||
stats: Text[];
|
||||
brief_stats: Text[];
|
||||
author: PlaylistAuthor;
|
||||
author: Author;
|
||||
description: Text;
|
||||
num_videos: Text;
|
||||
view_count: Text;
|
||||
@@ -23,13 +23,13 @@ class PlaylistHeader extends YTNode {
|
||||
menu;
|
||||
banner;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.id = data.playlistId;
|
||||
this.title = new Text(data.title);
|
||||
this.stats = data.stats.map((stat: any) => new Text(stat));
|
||||
this.brief_stats = data.briefStats.map((stat: any) => new Text(stat));
|
||||
this.author = new PlaylistAuthor({ ...data.ownerText, navigationEndpoint: data.ownerEndpoint }, data.ownerBadges, null);
|
||||
this.author = new Author({ ...data.ownerText, navigationEndpoint: data.ownerEndpoint }, data.ownerBadges, null);
|
||||
this.description = new Text(data.descriptionText);
|
||||
this.num_videos = new Text(data.numVideosText);
|
||||
this.view_count = new Text(data.viewCountText);
|
||||
@@ -37,9 +37,9 @@ class PlaylistHeader extends YTNode {
|
||||
this.can_delete = data.editableDetails.canDelete;
|
||||
this.is_editable = data.isEditable;
|
||||
this.privacy = data.privacy;
|
||||
this.save_button = Parser.parse(data.saveButton);
|
||||
this.shuffle_play_button = Parser.parse(data.shufflePlayButton);
|
||||
this.menu = Parser.parse(data.moreActionsMenu);
|
||||
this.save_button = Parser.parseItem(data.saveButton);
|
||||
this.shuffle_play_button = Parser.parseItem(data.shufflePlayButton);
|
||||
this.menu = Parser.parseItem(data.moreActionsMenu);
|
||||
this.banner = Parser.parseItem(data.playlistHeaderBanner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class PlaylistPanel extends YTNode {
|
||||
super();
|
||||
this.title = data.title;
|
||||
this.title_text = new Text(data.titleText);
|
||||
this.contents = Parser.parseArray<PlaylistPanelVideoWrapper | PlaylistPanelVideo | AutomixPreviewVideo>(data.contents);
|
||||
this.contents = Parser.parseArray(data.contents, [ PlaylistPanelVideoWrapper, PlaylistPanelVideo, AutomixPreviewVideo ]);
|
||||
this.playlist_id = data.playlistId;
|
||||
this.is_infinite = data.isInfinite;
|
||||
this.continuation = data.continuations?.[0]?.nextRadioContinuationData?.continuation || data.continuations?.[0]?.nextContinuationData?.continuation;
|
||||
|
||||
@@ -10,8 +10,8 @@ class PlaylistPanelVideoWrapper extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.primary = Parser.parseItem<PlaylistPanelVideo>(data.primaryRenderer);
|
||||
this.counterpart = data.counterpart?.map((item: any) => Parser.parseItem<PlaylistPanelVideo>(item.counterpartRenderer)) || [];
|
||||
this.primary = Parser.parseItem(data.primaryRenderer, PlaylistPanelVideo);
|
||||
this.counterpart = data.counterpart?.map((item: any) => Parser.parseItem(item.counterpartRenderer, PlaylistPanelVideo)) || [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class PlaylistSidebar extends YTNode {
|
||||
@@ -6,9 +6,9 @@ class PlaylistSidebar extends YTNode {
|
||||
|
||||
items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parse(data.items);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
|
||||
// XXX: alias for consistency
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
@@ -14,10 +14,10 @@ class PlaylistSidebarPrimaryInfo extends YTNode {
|
||||
endpoint: NavigationEndpoint;
|
||||
description: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.stats = data.stats.map((stat: any) => new Text(stat));
|
||||
this.thumbnail_renderer = Parser.parse(data.thumbnailRenderer);
|
||||
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer);
|
||||
this.title = new Text(data.title);
|
||||
this.menu = Parser.parseItem(data.menu);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
|
||||
class PlaylistSidebarSecondaryInfo extends YTNode {
|
||||
static type = 'PlaylistSidebarSecondaryInfo';
|
||||
@@ -7,10 +7,10 @@ class PlaylistSidebarSecondaryInfo extends YTNode {
|
||||
owner;
|
||||
button;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.owner = Parser.parse(data.videoOwner) || null;
|
||||
this.button = Parser.parse(data.button) || null;
|
||||
this.owner = Parser.parseItem(data.videoOwner);
|
||||
this.button = Parser.parseItem(data.button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Text from './misc/Text.js';
|
||||
import Parser from '../index.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import PlaylistAuthor from './misc/PlaylistAuthor.js';
|
||||
import Author from './misc/Author.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import ThumbnailOverlayTimeStatus from './ThumbnailOverlayTimeStatus.js';
|
||||
import type Menu from './menus/Menu.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
@@ -14,7 +14,7 @@ class PlaylistVideo extends YTNode {
|
||||
id: string;
|
||||
index: Text;
|
||||
title: Text;
|
||||
author: PlaylistAuthor;
|
||||
author: Author;
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_overlays;
|
||||
set_video_id: string | undefined;
|
||||
@@ -33,13 +33,13 @@ class PlaylistVideo extends YTNode {
|
||||
this.id = data.videoId;
|
||||
this.index = new Text(data.index);
|
||||
this.title = new Text(data.title);
|
||||
this.author = new PlaylistAuthor(data.shortBylineText);
|
||||
this.author = new Author(data.shortBylineText);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.set_video_id = data?.setVideoId;
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.is_playable = data.isPlayable;
|
||||
this.menu = Parser.parseItem<Menu>(data.menu);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
|
||||
const upcoming = data.upcomingEventData && Number(`${data.upcomingEventData.startTime}000`);
|
||||
if (upcoming) {
|
||||
@@ -47,7 +47,7 @@ class PlaylistVideo extends YTNode {
|
||||
}
|
||||
|
||||
this.duration = {
|
||||
text: new Text(data.lengthText).text,
|
||||
text: new Text(data.lengthText).toString(),
|
||||
seconds: parseInt(data.lengthSeconds)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ProfileColumn extends YTNode {
|
||||
@@ -6,9 +6,9 @@ class ProfileColumn extends YTNode {
|
||||
|
||||
items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parse(data.items);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
|
||||
// XXX: alias for consistency
|
||||
|
||||
@@ -13,7 +13,7 @@ class ReelShelf extends YTNode {
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.items = Parser.parse(data.items);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
this.endpoint = data.endpoint ? new NavigationEndpoint(data.endpoint) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ class SearchBox extends YTNode {
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.endpoint = new NavigationEndpoint(data.endpoint);
|
||||
this.search_button = Parser.parse(data.searchButton);
|
||||
this.clear_button = Parser.parse(data.clearButton);
|
||||
this.search_button = Parser.parseItem(data.searchButton);
|
||||
this.clear_button = Parser.parseItem(data.clearButton);
|
||||
this.placeholder_text = new Text(data.placeholderText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,26 @@ class SearchFilter extends YTNode {
|
||||
label: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
tooltip: string;
|
||||
status?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.label = new Text(data.label);
|
||||
this.endpoint = new NavigationEndpoint(data.endpoint);
|
||||
this.endpoint = new NavigationEndpoint(data.endpoint || data.navigationEndpoint);
|
||||
this.tooltip = data.tooltip;
|
||||
|
||||
if (data.status) {
|
||||
this.status = data.status;
|
||||
}
|
||||
}
|
||||
|
||||
get disabled(): boolean {
|
||||
return this.status === 'FILTER_STATUS_DISABLED';
|
||||
}
|
||||
|
||||
get selected(): boolean {
|
||||
return this.status === 'FILTER_STATUS_SELECTED';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
|
||||
class SectionList extends YTNode {
|
||||
static type = 'SectionList';
|
||||
@@ -10,13 +10,12 @@ class SectionList extends YTNode {
|
||||
header?;
|
||||
sub_menu?;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
if (data.targetId) {
|
||||
this.target_id = data.targetId;
|
||||
}
|
||||
|
||||
// TODO: this should be Parser#parseArray
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
|
||||
if (data.continuations) {
|
||||
@@ -28,7 +27,7 @@ class SectionList extends YTNode {
|
||||
}
|
||||
|
||||
if (data.header) {
|
||||
this.header = Parser.parse(data.header);
|
||||
this.header = Parser.parseItem(data.header);
|
||||
}
|
||||
|
||||
if (data.subMenu) {
|
||||
|
||||
@@ -12,8 +12,8 @@ class SegmentedLikeDislikeButton extends YTNode {
|
||||
|
||||
constructor (data: RawNode) {
|
||||
super();
|
||||
this.like_button = Parser.parseItem<ToggleButton | Button>(data.likeButton, [ ToggleButton, Button ]);
|
||||
this.dislike_button = Parser.parseItem<ToggleButton | Button>(data.dislikeButton, [ ToggleButton, Button ]);
|
||||
this.like_button = Parser.parseItem(data.likeButton, [ ToggleButton, Button ]);
|
||||
this.dislike_button = Parser.parseItem(data.dislikeButton, [ ToggleButton, Button ]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class SettingsOptions extends YTNode {
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'options')) {
|
||||
this.options = Parser.parseArray<SettingsSwitch | Dropdown | CopyLink | SettingsCheckbox | ChannelOptions>(data.options, [
|
||||
this.options = Parser.parseArray(data.options, [
|
||||
SettingsSwitch, Dropdown, CopyLink,
|
||||
SettingsCheckbox, ChannelOptions
|
||||
]);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Menu } from '../map.js';
|
||||
import Parser from '../parser.js';
|
||||
import BackstagePost from './BackstagePost.js';
|
||||
import Button from './Button.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
@@ -2,6 +2,7 @@ import Text from './misc/Text.js';
|
||||
import Parser from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
class Shelf extends YTNode {
|
||||
static type = 'Shelf';
|
||||
@@ -11,6 +12,7 @@ class Shelf extends YTNode {
|
||||
content: YTNode | null;
|
||||
icon_type?: string;
|
||||
menu?: YTNode | null;
|
||||
play_all_button?: Button | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -29,6 +31,10 @@ class Shelf extends YTNode {
|
||||
if (data.menu) {
|
||||
this.menu = Parser.parseItem(data.menu);
|
||||
}
|
||||
|
||||
if (data.playAllButton) {
|
||||
this.play_all_button = Parser.parseItem(data.playAllButton, Button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
src/parser/classes/ShowCustomThumbnail.ts
Normal file
14
src/parser/classes/ShowCustomThumbnail.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { RawNode } from '../index.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
export default class ShowCustomThumbnail extends YTNode {
|
||||
static type = 'ShowCustomThumbnail';
|
||||
|
||||
thumbnail: Thumbnail[];
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class Tab extends YTNode {
|
||||
this.title = data.title || 'N/A';
|
||||
this.selected = data.selected || false;
|
||||
this.endpoint = new NavigationEndpoint(data.endpoint);
|
||||
this.content = Parser.parseItem<SectionList | MusicQueue | RichGrid>(data.content, [ SectionList, MusicQueue, RichGrid ]);
|
||||
this.content = Parser.parseItem(data.content, [ SectionList, MusicQueue, RichGrid ]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
class ThumbnailOverlayBottomPanel extends YTNode {
|
||||
static type = 'ThumbnailOverlayBottomPanel';
|
||||
|
||||
icon_type: string;
|
||||
text?: Text;
|
||||
icon_type?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.icon_type = data.icon.iconType;
|
||||
if (Reflect.has(data, 'text')) {
|
||||
this.text = new Text(data.text);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType')) {
|
||||
this.icon_type = data.icon.iconType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Author from './misc/Author.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import Menu from './menus/Menu.js';
|
||||
|
||||
type AutoplaySet = {
|
||||
autoplay_video: NavigationEndpoint,
|
||||
next_button_video?: NavigationEndpoint
|
||||
};
|
||||
|
||||
class TwoColumnWatchNextResults extends YTNode {
|
||||
static type = 'TwoColumnWatchNextResults';
|
||||
@@ -7,12 +17,66 @@ class TwoColumnWatchNextResults extends YTNode {
|
||||
results;
|
||||
secondary_results;
|
||||
conversation_bar;
|
||||
playlist?: {
|
||||
id: string,
|
||||
title: string,
|
||||
author: Text | Author,
|
||||
contents: YTNode[],
|
||||
current_index: number,
|
||||
is_infinite: boolean,
|
||||
menu: Menu | null
|
||||
};
|
||||
autoplay?: {
|
||||
sets: AutoplaySet[],
|
||||
modified_sets?: AutoplaySet[],
|
||||
count_down_secs?: number
|
||||
};
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.results = Parser.parseArray(data.results?.results.contents);
|
||||
this.secondary_results = Parser.parseArray(data.secondaryResults?.secondaryResults.results);
|
||||
this.conversation_bar = Parser.parseItem(data?.conversationBar);
|
||||
|
||||
const playlistData = data.playlist?.playlist;
|
||||
if (playlistData) {
|
||||
this.playlist = {
|
||||
id: playlistData.playlistId,
|
||||
title: playlistData.title,
|
||||
author: playlistData.shortBylineText?.simpleText ?
|
||||
new Text(playlistData.shortBylineText) :
|
||||
new Author(playlistData.longBylineText),
|
||||
contents: Parser.parseArray(playlistData.contents),
|
||||
current_index: playlistData.currentIndex,
|
||||
is_infinite: !!playlistData.isInfinite,
|
||||
menu: Parser.parseItem(playlistData.menu, Menu)
|
||||
};
|
||||
}
|
||||
|
||||
const autoplayData = data.autoplay?.autoplay;
|
||||
if (autoplayData) {
|
||||
this.autoplay = {
|
||||
sets: autoplayData.sets.map((set: any) => this.#parseAutoplaySet(set))
|
||||
};
|
||||
if (autoplayData.modifiedSets) {
|
||||
this.autoplay.modified_sets = autoplayData.modifiedSets.map((set: any) => this.#parseAutoplaySet(set));
|
||||
}
|
||||
if (autoplayData.countDownSecs) {
|
||||
this.autoplay.count_down_secs = autoplayData.countDownSecs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#parseAutoplaySet(data: any): AutoplaySet {
|
||||
const result = {
|
||||
autoplay_video: new NavigationEndpoint(data.autoplayVideo)
|
||||
} as AutoplaySet;
|
||||
|
||||
if (data.nextButtonVideo) {
|
||||
result.next_button_video = new NavigationEndpoint(data.nextButtonVideo);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type Button from './Button.js';
|
||||
import Button from './Button.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
class UpsellDialog extends YTNode {
|
||||
@@ -16,8 +16,8 @@ class UpsellDialog extends YTNode {
|
||||
super();
|
||||
this.message_title = new Text(data.dialogMessageTitle);
|
||||
this.message_text = new Text(data.dialogMessageText);
|
||||
this.action_button = Parser.parseItem<Button>(data.actionButton);
|
||||
this.dismiss_button = Parser.parseItem<Button>(data.dismissButton);
|
||||
this.action_button = Parser.parseItem(data.actionButton, Button);
|
||||
this.dismiss_button = Parser.parseItem(data.dismissButton, Button);
|
||||
this.is_visible = data.isVisible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class VerticalList extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.items = Parser.parse(data.items);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
this.collapsed_item_count = data.collapsedItemCount;
|
||||
this.collapsed_state_button_text = new Text(data.collapsedStateButtonText);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class Video extends YTNode {
|
||||
hover_text: new Text(snippet.snippetHoverText)
|
||||
})) || [];
|
||||
|
||||
this.expandable_metadata = Parser.parseItem<ExpandableMetadata>(data.expandableMetadata);
|
||||
this.expandable_metadata = Parser.parseItem(data.expandableMetadata, ExpandableMetadata);
|
||||
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
@@ -77,8 +77,8 @@ class Video extends YTNode {
|
||||
}
|
||||
|
||||
this.duration = {
|
||||
text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text,
|
||||
seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text)
|
||||
text: data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString(),
|
||||
seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString())
|
||||
};
|
||||
|
||||
this.show_action_menu = data.showActionMenu;
|
||||
|
||||
@@ -20,14 +20,14 @@ class VideoSecondaryInfo extends YTNode {
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.owner = Parser.parseItem<VideoOwner>(data.owner);
|
||||
this.owner = Parser.parseItem(data.owner, VideoOwner);
|
||||
this.description = new Text(data.description);
|
||||
|
||||
if (Reflect.has(data, 'attributedDescription')) {
|
||||
this.description = new Text(this.#convertAttributedDescriptionToRuns(data.attributedDescription));
|
||||
}
|
||||
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton | Button>(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
this.metadata = Parser.parseItem<MetadataRowContainer>(data.metadataRowContainer, MetadataRowContainer);
|
||||
this.show_more_text = data.showMoreText;
|
||||
this.show_less_text = data.showLessText;
|
||||
|
||||
@@ -12,7 +12,7 @@ class WatchNextEndScreen extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.results = Parser.parseArray<EndScreenVideo | EndScreenPlaylist>(data.results, [ EndScreenVideo, EndScreenPlaylist ]);
|
||||
this.results = Parser.parseArray(data.results, [ EndScreenVideo, EndScreenPlaylist ]);
|
||||
this.title = new Text(data.title).toString();
|
||||
}
|
||||
}
|
||||
|
||||
17
src/parser/classes/YpcTrailer.ts
Normal file
17
src/parser/classes/YpcTrailer.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { RawNode } from '../index.js';
|
||||
|
||||
class YpcTrailer extends YTNode {
|
||||
static type = 'YpcTrailer';
|
||||
|
||||
video_message: string;
|
||||
player_response;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.video_message = data.fullVideoMessage;
|
||||
this.player_response = data.unserializedPlayerResponse;
|
||||
}
|
||||
}
|
||||
|
||||
export default YpcTrailer;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user