Compare commits

..

19 Commits

Author SHA1 Message Date
LuanRT
ac9341c769 chore(release): v2.6.0 2022-12-19 04:07:48 -03:00
LuanRT
cac762569a feat(Session): allow overriding geolocation (#260)
* Allow overriding geolocation

* Fix some inconsistencies (unrelated)
2022-12-19 03:55:38 -03:00
LuanRT
9978ebf085 refactor(Parser): reduce reliance on localised strings (#258) 2022-12-17 00:54:08 -03:00
LuanRT
b036e2fcdc feat(Channel): parse subscribe button
This way one can subscribe to a given channel simply by calling the button's endpoint.
2022-12-16 17:13:13 -03:00
LuanRT
e37f42f41b feat: bring back Video#is_live and add ExpandableMetadata (#256)
* bring back `Video#is_live`

* add ExpandableMetadata
2022-12-15 19:01:42 -03:00
absidue
883a023624 feat(TextRun): add support for formatting (#254) 2022-12-14 22:48:35 -03:00
LuanRT
506834b253 docs: fix formatting (oops) 2022-12-12 01:18:42 -03:00
LuanRT
87e7ef77eb chore(release): v2.5.2 2022-12-12 00:21:32 -03:00
LuanRT
27fdd8268a docs: update ToC 2022-12-12 00:14:55 -03:00
LuanRT
d4ea87b8b0 chore(docs): fix typo 2022-12-12 00:13:24 -03:00
LuanRT
ec87eea20d chore: update deps 2022-12-12 00:10:56 -03:00
LuanRT
e43ad202f4 chore: update examples 2022-12-12 00:10:33 -03:00
LuanRT
104c36b450 docs: reword a few things 2022-12-11 23:58:26 -03:00
absidue
f5d61d70f2 fix: author and thumbnails for autogenerated playlists (#251) 2022-12-07 20:34:53 -03:00
LuanRT
c76f5f478d 2.5.1 2022-11-30 19:11:40 -03:00
LuanRT
49d1432b5a chore: fix a few inconsistencies 2022-11-30 19:02:49 -03:00
LuanRT
be157ef016 fix: signature decipher extraction failing (#249) 2022-11-30 18:39:37 -03:00
LuanRT
9f703203b6 chore(docs): update readme 2022-11-29 05:49:15 -03:00
LuanRT
516eeeff45 refactor: improve Search parser (#247)
* refactor: improve Search parser

* chore: lint
2022-11-29 03:50:17 -03:00
61 changed files with 692 additions and 501 deletions

135
README.md
View File

@@ -1,5 +1,3 @@
<!-- Hi there, fellow coder :) -->
<!-- BADGE LINKS -->
[npm]: https://www.npmjs.com/package/youtubei.js
[versions]: https://www.npmjs.com/package/youtubei.js?activeTab=versions
@@ -10,45 +8,27 @@
<!-- OTHER LINKS -->
[project]: https://github.com/LuanRT/YouTube.js
[twitter]: https://twitter.com/lrt_nooneknows
[twitter]: https://twitter.com/thesciencephile
[discord]: https://discord.gg/syDu7Yks54
[nodejs]: https://nodejs.org
<!-- INTRODUCTION -->
<h1 align=center>
YouTube.js
</h1>
<h1 align=center>YouTube.js</h1>
<p align=center>
<i>
A full-featured wrapper around the InnerTube API, which is what YouTube itself uses.
</i>
</p>
<p align=center>A full-featured wrapper around the InnerTube API, which is what YouTube itself uses</p>
<p align="center">
<a href="https://github.com/LuanRT/YouTube.js/issues">
Report Bug
</a>
·
<a href="https://github.com/LuanRT/YouTube.js/issues">
Request Feature
</a>
</p>
<!-- BADGES -->
<div align="center">
[![Tests](https://github.com/LuanRT/YouTube.js/actions/workflows/node.js.yml/badge.svg)][actions]
[![Latest version](https://img.shields.io/npm/v/youtubei.js?color=%2335C757)][versions]
[![NPM Version](https://img.shields.io/npm/v/youtubei.js?color=%2335C757)][versions]
[![Codefactor](https://www.codefactor.io/repository/github/luanrt/youtube.js/badge)][codefactor]
[![Downloads](https://img.shields.io/npm/dt/youtubei.js)][npm]
[![Discord](https://img.shields.io/badge/discord-online-brightgreen.svg)][discord]
[![Say thanks](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)][say-thanks]
<br>
[![Donate](https://img.shields.io/badge/donate-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#white)][github-sponsors]
</div>
<!-- SPONSORS -->
<p align="center">
<a><sub>Special thanks to:<sub></a>
</p>
@@ -57,7 +37,7 @@
<body>
<tr>
<td align="center">
<a href="https://serpapi.com/">
<a href="https://serpapi.com/" target="_blank">
<img width="80" alt="SerpApi" src="https://luanrt.is-a.dev/assets/img/serpapi.svg" />
<br>
<b>
@@ -73,7 +53,6 @@
___
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
@@ -95,23 +74,20 @@ ___
<li><a href="#api">API</a></li>
</ul>
</li>
<li><a href="#implementing-custom-functionality">Implementing custom functionality </a></li>
<li><a href="#extending-the-library">Extending the library</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#contributors">Contributors</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>
<!-- ABOUT THE PROJECT -->
## About
## Description
InnerTube is an API used across all YouTube clients, it was created to simplify[^1] the internal structure of the platform in a way that updates, tweaks, and experiments can be easily made. This library handles all the low-level communication with InnerTube, providing a simple, fast, and efficient way to interact with YouTube programmatically.
InnerTube is an API used across 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.
If you have any questions or need help, feel free to contact us on our chat server [here](https://discord.gg/syDu7Yks54).
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).
<!-- GETTING STARTED -->
## Getting Started
### Prerequisites
@@ -138,7 +114,6 @@ npm install github:LuanRT/YouTube.js
**TODO:** Deno install instructions (esm.sh possibly?)
<!-- USAGE -->
## Usage
Create an InnerTube instance:
```ts
@@ -245,7 +220,7 @@ const yt = await Innertube.create({
* `Innertube`
<details>
<summary>objects</summary>
<summary>Objects</summary>
<p>
* [.session](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/session.md)
@@ -260,7 +235,7 @@ const yt = await Innertube.create({
<details>
<summary>methods</summary>
<summary>Methods</summary>
<p>
* [.getInfo(video_id, client?)](#getinfo)
@@ -597,48 +572,60 @@ Utility to call navigation endpoints.
| --- | --- | --- |
| endpoint | `NavigationEndpoint` | The target endpoint |
| args? | `object` | Additional payload arguments |
## Extending the library
## Implementing custom functionality
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.
Something cool about YouTube.js is that it is completely modular and easy to tinker with. 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, you may want to call an endpoint directly, that can be achieved with the `Actions` class:
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:
```ts
// ...
import { Innertube } from 'youtubei.js';
const payload = {
// ...
videoId: 'jLTOuvBTLxA',
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.
};
(async () => {
const yt = await Innertube.create();
const response = await yt.actions.execute('/player', payload);
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.
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.
});
console.info(response);
return videoInfo;
}
const videoInfo = await getVideoInfo('jLTOuvBTLxA');
console.info(videoInfo);
})();
```
Or maybe there's an interesting `NavigationEndpoint` in a parsed response and we want to call it to see what happens:
Or perhaps there's a `NavigationEndpoint` in a parsed response and we want to call it to see what happens:
```ts
// ...
const artist = await yt.music.getArtist('UC52ZqHVQz5OoGhvbWiRal6g');
const albums = artist.sections[1].as(YTNodes.MusicCarouselShelf);
import { Innertube, YTNodes } from 'youtubei.js';
// Say we have a button and want to “click” it
const button = albums.as(YTNodes.MusicCarouselShelf).header?.more_content;
if (button) {
// To do that, we can call its navigation endpoint:
const page = await button.endpoint.call(yt.actions, { parse: true, client: 'YTMUSIC' });
console.info(page);
}
(async () => {
const yt = await Innertube.create();
const artist = await yt.music.getArtist('UC52ZqHVQz5OoGhvbWiRal6g');
const albums = artist.sections[1].as(YTNodes.MusicCarouselShelf);
// Say we want to click 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:
const page = await button.endpoint.call(yt.actions);
console.info(page);
}
})();
```
### Parser
If you're working on an extension for the library or just want to have nicely typed and sanitized InnerTube responses for a project then have a look at our powerful 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.
Example:
```ts
@@ -656,10 +643,12 @@ 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 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.
*/
const tab = page.contents.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
@@ -674,23 +663,21 @@ const sections = tab.content?.as(YTNodes.SectionList).contents.array().as(YTNode
console.info('Sections:', sections);
```
Detailed documentation can be found [here](https://github.com/LuanRT/YouTube.js/blob/main/src/parser).
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.
<!-- CONTRIBUTORS -->
## Contributors
Thank you to all the wonderful people who have contributed to this project:
<a href="https://github.com/LuanRT/YouTube.js/graphs/contributors">
<img src="https://contrib.rocks/image?repo=LuanRT/YouTube.js" />
</a>
<!-- CONTACT -->
## Contact
LuanRT - [@lrt_nooneknows][twitter] - luan.lrt4@gmail.com
LuanRT - [@thesciencephile][twitter] - luan.lrt4@gmail.com
Project Link: [https://github.com/LuanRT/YouTube.js][project]
@@ -700,10 +687,8 @@ All trademarks, logos, and brand names are the property of their respective owne
Should you have any questions or concerns please contact me directly via email.
<!-- Footnotes -->
[^1]: https://gizmodo.com/how-project-innertube-helped-pull-youtube-out-of-the-gu-1704946491
<!-- LICENSE -->
## License
Distributed under the [MIT](https://choosealicense.com/licenses/mit/) License.

View File

@@ -4,42 +4,42 @@ import { streamToIterable } from 'youtubei.js/dist/src/utils/Utils';
(async () => {
const yt = await Innertube.create({ cache: new UniversalCache() });
const search = await yt.music.search('No Copyright Background Music', { type: 'album' });
if (!search.results)
throw new Error('Filter "type" must be used');
const album = await yt.music.getAlbum(search.results[0].id as string);
if (!album.contents)
throw new Error('Album appears to be empty');
console.info(`Album "${album.header.title.toString()}" by ${album.header.author?.name}`, '\n');
console.info(`Album "${album.header?.title.toString()}" by ${album.header?.author?.name}`, '\n');
for (const song of album.contents) {
const stream = await yt.download(song.id as string, {
type: 'audio', // audio, video or audio+video
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on.
format: 'mp4' // media container format
});
console.info(`Downloading ${song.title} (${song.id})`);
const dir = `./${album.header.title.toString()}`;
const dir = `./${album.header?.title.toString()}`;
if (!existsSync(dir)) {
mkdirSync(dir);
}
const file = createWriteStream(`${dir}/${song.title?.replace(/\//g, '')}.m4a`);
for await (const chunk of streamToIterable(stream)) {
file.write(chunk);
}
console.info(`${song.id} - Done!`, '\n');
}
console.info(`Downloaded ${album.header.song_count}!`);
console.info(`Downloaded ${album.header?.song_count}!`);
})();

412
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "youtubei.js",
"version": "2.5.0",
"version": "2.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "youtubei.js",
"version": "2.5.0",
"version": "2.6.0",
"funding": [
"https://github.com/sponsors/LuanRT"
],
@@ -58,30 +58,30 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz",
"integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz",
"integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz",
"integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz",
"integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.2",
"@babel/generator": "^7.20.5",
"@babel/helper-compilation-targets": "^7.20.0",
"@babel/helper-module-transforms": "^7.20.2",
"@babel/helpers": "^7.20.1",
"@babel/parser": "^7.20.2",
"@babel/helpers": "^7.20.5",
"@babel/parser": "^7.20.5",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.2",
"@babel/traverse": "^7.20.5",
"@babel/types": "^7.20.5",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -106,12 +106,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.20.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz",
"integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
"integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==",
"dev": true,
"dependencies": {
"@babel/types": "^7.20.2",
"@babel/types": "^7.20.5",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
},
@@ -286,14 +286,14 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
"integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz",
"integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==",
"dev": true,
"dependencies": {
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.0"
"@babel/traverse": "^7.20.5",
"@babel/types": "^7.20.5"
},
"engines": {
"node": ">=6.9.0"
@@ -385,9 +385,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.20.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz",
"integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
"integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -573,19 +573,19 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
"integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz",
"integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.1",
"@babel/generator": "^7.20.5",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.1",
"@babel/types": "^7.20.0",
"@babel/parser": "^7.20.5",
"@babel/types": "^7.20.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -603,9 +603,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz",
"integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.19.4",
@@ -1284,9 +1284,9 @@
"dev": true
},
"node_modules/@sinonjs/commons": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.5.tgz",
"integrity": "sha512-rTpCA0wG1wUxglBSFdMMY0oTrKYvgf4fNgv/sXbfCVAdf+FnPBdKJR/7XbpTCwbCrvCbdPYnlWaUUYz4V2fPDA==",
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
"integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
@@ -1334,9 +1334,9 @@
}
},
"node_modules/@types/babel__traverse": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz",
"integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==",
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
"integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
"dev": true,
"dependencies": {
"@babel/types": "^7.3.0"
@@ -1416,9 +1416,9 @@
"dev": true
},
"node_modules/@types/yargs": {
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.14.tgz",
"integrity": "sha512-9Pj7abXoW1RSTcZaL2Hk6G2XyLMlp5ECdVC/Zf2p/KBjC3srijLGgRAXOBjtFrJoIrvxdTKyKDA14bEcbxBaWw==",
"version": "17.0.17",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz",
"integrity": "sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==",
"dev": true,
"dependencies": {
"@types/yargs-parser": "*"
@@ -1431,14 +1431,14 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.44.0.tgz",
"integrity": "sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.0.tgz",
"integrity": "sha512-QrZqaIOzJAjv0sfjY4EjbXUi3ZOFpKfzntx22gPGr9pmFcTjcFw/1sS1LJhEubfAGwuLjNrPV0rH+D1/XZFy7Q==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.44.0",
"@typescript-eslint/type-utils": "5.44.0",
"@typescript-eslint/utils": "5.44.0",
"@typescript-eslint/scope-manager": "5.46.0",
"@typescript-eslint/type-utils": "5.46.0",
"@typescript-eslint/utils": "5.46.0",
"debug": "^4.3.4",
"ignore": "^5.2.0",
"natural-compare-lite": "^1.4.0",
@@ -1464,14 +1464,14 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.44.0.tgz",
"integrity": "sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.46.0.tgz",
"integrity": "sha512-joNO6zMGUZg+C73vwrKXCd8usnsmOYmgW/w5ZW0pG0RGvqeznjtGDk61EqqTpNrFLUYBW2RSBFrxdAZMqA4OZA==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.44.0",
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/typescript-estree": "5.44.0",
"@typescript-eslint/scope-manager": "5.46.0",
"@typescript-eslint/types": "5.46.0",
"@typescript-eslint/typescript-estree": "5.46.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1491,13 +1491,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.44.0.tgz",
"integrity": "sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.0.tgz",
"integrity": "sha512-7wWBq9d/GbPiIM6SqPK9tfynNxVbfpihoY5cSFMer19OYUA3l4powA2uv0AV2eAZV6KoAh6lkzxv4PoxOLh1oA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/visitor-keys": "5.44.0"
"@typescript-eslint/types": "5.46.0",
"@typescript-eslint/visitor-keys": "5.46.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1508,13 +1508,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.44.0.tgz",
"integrity": "sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.0.tgz",
"integrity": "sha512-dwv4nimVIAsVS2dTA0MekkWaRnoYNXY26dKz8AN5W3cBFYwYGFQEqm/cG+TOoooKlncJS4RTbFKgcFY/pOiBCg==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "5.44.0",
"@typescript-eslint/utils": "5.44.0",
"@typescript-eslint/typescript-estree": "5.46.0",
"@typescript-eslint/utils": "5.46.0",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
},
@@ -1535,9 +1535,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.44.0.tgz",
"integrity": "sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.0.tgz",
"integrity": "sha512-wHWgQHFB+qh6bu0IAPAJCdeCdI0wwzZnnWThlmHNY01XJ9Z97oKqKOzWYpR2I83QmshhQJl6LDM9TqMiMwJBTw==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -1548,13 +1548,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.44.0.tgz",
"integrity": "sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.0.tgz",
"integrity": "sha512-kDLNn/tQP+Yp8Ro2dUpyyVV0Ksn2rmpPpB0/3MO874RNmXtypMwSeazjEN/Q6CTp8D7ExXAAekPEcCEB/vtJkw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/visitor-keys": "5.44.0",
"@typescript-eslint/types": "5.46.0",
"@typescript-eslint/visitor-keys": "5.46.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1575,16 +1575,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.44.0.tgz",
"integrity": "sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.0.tgz",
"integrity": "sha512-4O+Ps1CRDw+D+R40JYh5GlKLQERXRKW5yIQoNDpmXPJ+C7kaPF9R7GWl+PxGgXjB3PQCqsaaZUpZ9dG4U6DO7g==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.44.0",
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/typescript-estree": "5.44.0",
"@typescript-eslint/scope-manager": "5.46.0",
"@typescript-eslint/types": "5.46.0",
"@typescript-eslint/typescript-estree": "5.46.0",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0",
"semver": "^7.3.7"
@@ -1601,12 +1601,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.44.0.tgz",
"integrity": "sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.0.tgz",
"integrity": "sha512-E13gBoIXmaNhwjipuvQg1ByqSAu/GbEpP/qzFihugJ+MomtoJtFAJG/+2DRPByf57B863m0/q7Zt16V9ohhANw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/types": "5.46.0",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
@@ -1941,9 +1941,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001434",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
"integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==",
"version": "1.0.30001439",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz",
"integrity": "sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==",
"dev": true,
"funding": [
{
@@ -2654,9 +2654,9 @@
}
},
"node_modules/eslint": {
"version": "8.28.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.28.0.tgz",
"integrity": "sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==",
"version": "8.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz",
"integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==",
"dev": true,
"dependencies": {
"@eslint/eslintrc": "^1.3.3",
@@ -2975,9 +2975,9 @@
"dev": true
},
"node_modules/fastq": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz",
"integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
@@ -3157,9 +3157,9 @@
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz",
"integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -3270,9 +3270,9 @@
}
},
"node_modules/ignore": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
"integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==",
"dev": true,
"engines": {
"node": ">= 4"
@@ -5314,9 +5314,9 @@
}
},
"node_modules/typescript": {
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -5332,9 +5332,9 @@
"integrity": "sha512-o0QVGuFg24FK765Qdd5kk0zU/U4dEsCtN/GSiwNI9i8xsSVtjIAOdTaVhLwZ1nrbWxFVMxNDDl+9fednsOMsBw=="
},
"node_modules/undici": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.12.0.tgz",
"integrity": "sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg==",
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz",
"integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==",
"dependencies": {
"busboy": "^1.6.0"
},
@@ -5536,27 +5536,27 @@
}
},
"@babel/compat-data": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz",
"integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz",
"integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==",
"dev": true
},
"@babel/core": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz",
"integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz",
"integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==",
"dev": true,
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.2",
"@babel/generator": "^7.20.5",
"@babel/helper-compilation-targets": "^7.20.0",
"@babel/helper-module-transforms": "^7.20.2",
"@babel/helpers": "^7.20.1",
"@babel/parser": "^7.20.2",
"@babel/helpers": "^7.20.5",
"@babel/parser": "^7.20.5",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.2",
"@babel/traverse": "^7.20.5",
"@babel/types": "^7.20.5",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -5573,12 +5573,12 @@
}
},
"@babel/generator": {
"version": "7.20.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz",
"integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz",
"integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==",
"dev": true,
"requires": {
"@babel/types": "^7.20.2",
"@babel/types": "^7.20.5",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
},
@@ -5709,14 +5709,14 @@
"dev": true
},
"@babel/helpers": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
"integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz",
"integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==",
"dev": true,
"requires": {
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.0"
"@babel/traverse": "^7.20.5",
"@babel/types": "^7.20.5"
}
},
"@babel/highlight": {
@@ -5789,9 +5789,9 @@
}
},
"@babel/parser": {
"version": "7.20.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz",
"integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz",
"integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==",
"dev": true
},
"@babel/plugin-syntax-async-generators": {
@@ -5923,19 +5923,19 @@
}
},
"@babel/traverse": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
"integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz",
"integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.1",
"@babel/generator": "^7.20.5",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.1",
"@babel/types": "^7.20.0",
"@babel/parser": "^7.20.5",
"@babel/types": "^7.20.5",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -5949,9 +5949,9 @@
}
},
"@babel/types": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz",
"integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==",
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz",
"integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.19.4",
@@ -6480,9 +6480,9 @@
"dev": true
},
"@sinonjs/commons": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.5.tgz",
"integrity": "sha512-rTpCA0wG1wUxglBSFdMMY0oTrKYvgf4fNgv/sXbfCVAdf+FnPBdKJR/7XbpTCwbCrvCbdPYnlWaUUYz4V2fPDA==",
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
"integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
@@ -6530,9 +6530,9 @@
}
},
"@types/babel__traverse": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz",
"integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==",
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
"integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
"dev": true,
"requires": {
"@babel/types": "^7.3.0"
@@ -6612,9 +6612,9 @@
"dev": true
},
"@types/yargs": {
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.14.tgz",
"integrity": "sha512-9Pj7abXoW1RSTcZaL2Hk6G2XyLMlp5ECdVC/Zf2p/KBjC3srijLGgRAXOBjtFrJoIrvxdTKyKDA14bEcbxBaWw==",
"version": "17.0.17",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz",
"integrity": "sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
@@ -6627,14 +6627,14 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.44.0.tgz",
"integrity": "sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.0.tgz",
"integrity": "sha512-QrZqaIOzJAjv0sfjY4EjbXUi3ZOFpKfzntx22gPGr9pmFcTjcFw/1sS1LJhEubfAGwuLjNrPV0rH+D1/XZFy7Q==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "5.44.0",
"@typescript-eslint/type-utils": "5.44.0",
"@typescript-eslint/utils": "5.44.0",
"@typescript-eslint/scope-manager": "5.46.0",
"@typescript-eslint/type-utils": "5.46.0",
"@typescript-eslint/utils": "5.46.0",
"debug": "^4.3.4",
"ignore": "^5.2.0",
"natural-compare-lite": "^1.4.0",
@@ -6644,53 +6644,53 @@
}
},
"@typescript-eslint/parser": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.44.0.tgz",
"integrity": "sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.46.0.tgz",
"integrity": "sha512-joNO6zMGUZg+C73vwrKXCd8usnsmOYmgW/w5ZW0pG0RGvqeznjtGDk61EqqTpNrFLUYBW2RSBFrxdAZMqA4OZA==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "5.44.0",
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/typescript-estree": "5.44.0",
"@typescript-eslint/scope-manager": "5.46.0",
"@typescript-eslint/types": "5.46.0",
"@typescript-eslint/typescript-estree": "5.46.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.44.0.tgz",
"integrity": "sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.0.tgz",
"integrity": "sha512-7wWBq9d/GbPiIM6SqPK9tfynNxVbfpihoY5cSFMer19OYUA3l4powA2uv0AV2eAZV6KoAh6lkzxv4PoxOLh1oA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/visitor-keys": "5.44.0"
"@typescript-eslint/types": "5.46.0",
"@typescript-eslint/visitor-keys": "5.46.0"
}
},
"@typescript-eslint/type-utils": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.44.0.tgz",
"integrity": "sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.0.tgz",
"integrity": "sha512-dwv4nimVIAsVS2dTA0MekkWaRnoYNXY26dKz8AN5W3cBFYwYGFQEqm/cG+TOoooKlncJS4RTbFKgcFY/pOiBCg==",
"dev": true,
"requires": {
"@typescript-eslint/typescript-estree": "5.44.0",
"@typescript-eslint/utils": "5.44.0",
"@typescript-eslint/typescript-estree": "5.46.0",
"@typescript-eslint/utils": "5.46.0",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
}
},
"@typescript-eslint/types": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.44.0.tgz",
"integrity": "sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.0.tgz",
"integrity": "sha512-wHWgQHFB+qh6bu0IAPAJCdeCdI0wwzZnnWThlmHNY01XJ9Z97oKqKOzWYpR2I83QmshhQJl6LDM9TqMiMwJBTw==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.44.0.tgz",
"integrity": "sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.0.tgz",
"integrity": "sha512-kDLNn/tQP+Yp8Ro2dUpyyVV0Ksn2rmpPpB0/3MO874RNmXtypMwSeazjEN/Q6CTp8D7ExXAAekPEcCEB/vtJkw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/visitor-keys": "5.44.0",
"@typescript-eslint/types": "5.46.0",
"@typescript-eslint/visitor-keys": "5.46.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -6699,28 +6699,28 @@
}
},
"@typescript-eslint/utils": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.44.0.tgz",
"integrity": "sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.0.tgz",
"integrity": "sha512-4O+Ps1CRDw+D+R40JYh5GlKLQERXRKW5yIQoNDpmXPJ+C7kaPF9R7GWl+PxGgXjB3PQCqsaaZUpZ9dG4U6DO7g==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.44.0",
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/typescript-estree": "5.44.0",
"@typescript-eslint/scope-manager": "5.46.0",
"@typescript-eslint/types": "5.46.0",
"@typescript-eslint/typescript-estree": "5.46.0",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0",
"semver": "^7.3.7"
}
},
"@typescript-eslint/visitor-keys": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.44.0.tgz",
"integrity": "sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==",
"version": "5.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.0.tgz",
"integrity": "sha512-E13gBoIXmaNhwjipuvQg1ByqSAu/GbEpP/qzFihugJ+MomtoJtFAJG/+2DRPByf57B863m0/q7Zt16V9ohhANw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.44.0",
"@typescript-eslint/types": "5.46.0",
"eslint-visitor-keys": "^3.3.0"
}
},
@@ -6959,9 +6959,9 @@
"dev": true
},
"caniuse-lite": {
"version": "1.0.30001434",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
"integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==",
"version": "1.0.30001439",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz",
"integrity": "sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==",
"dev": true
},
"chalk": {
@@ -7379,9 +7379,9 @@
"dev": true
},
"eslint": {
"version": "8.28.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.28.0.tgz",
"integrity": "sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==",
"version": "8.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz",
"integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==",
"dev": true,
"requires": {
"@eslint/eslintrc": "^1.3.3",
@@ -7628,9 +7628,9 @@
"dev": true
},
"fastq": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
"integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz",
"integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==",
"dev": true,
"requires": {
"reusify": "^1.0.4"
@@ -7755,9 +7755,9 @@
}
},
"minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz",
"integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
@@ -7848,9 +7848,9 @@
"dev": true
},
"ignore": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz",
"integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==",
"dev": true
},
"import-fresh": {
@@ -9349,9 +9349,9 @@
"dev": true
},
"typescript": {
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz",
"integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==",
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true
},
"uhyphen": {
@@ -9360,9 +9360,9 @@
"integrity": "sha512-o0QVGuFg24FK765Qdd5kk0zU/U4dEsCtN/GSiwNI9i8xsSVtjIAOdTaVhLwZ1nrbWxFVMxNDDl+9fednsOMsBw=="
},
"undici": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.12.0.tgz",
"integrity": "sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg==",
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz",
"integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==",
"requires": {
"busboy": "^1.6.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "2.5.0",
"version": "2.6.0",
"description": "Full-featured wrapper around YouTube's private API. Supports YouTube, YouTube Music and YouTube Studio (WIP).",
"main": "./dist/index.js",
"browser": "./bundle/browser.js",

View File

@@ -11,9 +11,6 @@ import Playlist from '../parser/ytmusic/Playlist';
import Recap from '../parser/ytmusic/Recap';
import Tab from '../parser/classes/Tab';
import Tabbed from '../parser/classes/Tabbed';
import SingleColumnMusicWatchNextResults from '../parser/classes/SingleColumnMusicWatchNextResults';
import WatchNextTabbedResults from '../parser/classes/WatchNextTabbedResults';
import SectionList from '../parser/classes/SectionList';
import Message from '../parser/classes/Message';
@@ -235,13 +232,9 @@ class Music {
parse: true
});
const tabs = data.contents.item()
.as(SingleColumnMusicWatchNextResults).contents.item()
.as(Tabbed).contents.item()
.as(WatchNextTabbedResults)
.tabs.array().as(Tab);
const tabs = data.contents_memo.getType(Tab);
const tab = tabs.get({ title: 'Up next' });
const tab = tabs?.[0];
if (!tab)
throw new InnertubeError('Could not find target tab.');
@@ -287,20 +280,16 @@ class Music {
parse: true
});
const tabs = data.contents.item()
.as(SingleColumnMusicWatchNextResults).contents.item()
.as(Tabbed).contents.item()
.as(WatchNextTabbedResults)
.tabs.array().as(Tab);
const tabs = data.contents_memo.getType(Tab);
const tab = tabs.get({ title: 'Related' });
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
if (!tab)
throw new InnertubeError('Could not find target tab.');
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
const shelves = page.contents.item().as(SectionList).contents.array().as(MusicCarouselShelf, MusicDescriptionShelf);
const shelves = page.contents.item().as(SectionList).contents.as(MusicCarouselShelf, MusicDescriptionShelf);
return shelves;
}
@@ -318,13 +307,9 @@ class Music {
parse: true
});
const tabs = data.contents.item()
.as(SingleColumnMusicWatchNextResults).contents.item()
.as(Tabbed).contents.item()
.as(WatchNextTabbedResults)
.tabs.array().as(Tab);
const tabs = data.contents_memo.getType(Tab);
const tab = tabs.get({ title: 'Lyrics' });
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
if (!tab)
throw new InnertubeError('Could not find target tab.');
@@ -334,7 +319,7 @@ class Music {
if (page.contents.item().key('type').string() === 'Message')
throw new InnertubeError(page.contents.item().as(Message).text, video_id);
const section_list = page.contents.item().as(SectionList).contents.array();
const section_list = page.contents.item().as(SectionList).contents;
return section_list.firstOfType(MusicDescriptionShelf);
}

View File

@@ -167,13 +167,13 @@ export default class Player {
static extractSigSourceCode(data: string) {
const calls = getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
const obj_name = calls?.split('.')?.[0]?.replace(';', '');
const functions = getStringBetweenStrings(data, `var ${obj_name}=`, '};');
const obj_name = calls?.split(/\.|\[/)?.[0]?.replace(';', '')?.trim();
const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};');
if (!functions || !calls)
console.warn(new PlayerError('Failed to extract signature decipher algorithm'));
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}=${functions}}${calls} return a.join("") } descramble_sig(sig);`;
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`;
}
static extractNSigSourceCode(data: string) {

View File

@@ -56,6 +56,7 @@ export interface Context {
export interface SessionOptions {
lang?: string;
location?: string;
account_index?: number;
device_category?: DeviceCategory;
client_type?: ClientType;
@@ -112,6 +113,7 @@ export default class Session extends EventEmitterLike {
static async create(options: SessionOptions = {}) {
const { context, api_key, api_version, account_index } = await Session.getSessionData(
options.lang,
options.location,
options.account_index,
options.device_category,
options.client_type,
@@ -123,6 +125,7 @@ export default class Session extends EventEmitterLike {
static async getSessionData(
lang = 'en-US',
location = '',
account_index = 0,
device_category: DeviceCategory = 'desktop',
client_name: ClientType = ClientType.WEB,
@@ -157,7 +160,7 @@ export default class Session extends EventEmitterLike {
const context: Context = {
client: {
hl: device_info[0],
gl: device_info[2],
gl: location || device_info[2],
remoteHost: device_info[3],
screenDensityFloat: 1,
screenHeightPoints: 720,

View File

@@ -17,7 +17,7 @@ class TabbedFeed extends Feed {
return this.#tabs.map((tab) => tab.title.toString());
}
async getTab(title: string) {
async getTabByName(title: string) {
const tab = this.#tabs.find((tab) => tab.title.toLowerCase() === title.toLowerCase());
if (!tab)
@@ -28,8 +28,19 @@ class TabbedFeed extends Feed {
const response = await tab.endpoint.call(this.#actions);
if (!response)
throw new InnertubeError('Failed to call endpoint');
return new TabbedFeed(this.#actions, response.data, false);
}
async getTabByURL(url: string) {
const tab = this.#tabs.find((tab) => tab.endpoint.metadata.url?.split('/').pop() === url);
if (!tab)
throw new InnertubeError(`Tab "${url}" not found`);
if (tab.selected)
return this;
const response = await tab.endpoint.call(this.#actions);
return new TabbedFeed(this.#actions, response.data, false);
}

View File

@@ -6,17 +6,21 @@ import { YTNode } from '../helpers';
class Button extends YTNode {
static type = 'Button';
text: string;
text?: string;
label;
tooltip;
icon_type;
label?: string;
tooltip?: string;
icon_type?: string;
is_disabled?: boolean;
endpoint: NavigationEndpoint;
constructor(data: any) {
super();
this.text = new Text(data.text).toString();
if (data.text) {
this.text = new Text(data.text).toString();
}
if (data.accessibility?.label) {
this.label = data.accessibility?.label;
@@ -30,6 +34,10 @@ class Button extends YTNode {
this.icon_type = data.icon?.iconType;
}
if (Reflect.has(data, 'isDisabled')) {
this.is_disabled = data.isDisabled;
}
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command);
}
}

View File

@@ -1,20 +1,28 @@
import Parser from '../index';
import Author from './misc/Author';
import Thumbnail from './misc/Thumbnail';
import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import type Button from './Button';
import type ChannelHeaderLinks from './ChannelHeaderLinks';
import type SubscribeButton from './SubscribeButton';
import { YTNode } from '../helpers';
class C4TabbedHeader extends YTNode {
static type = 'C4TabbedHeader';
author;
banner;
tv_banner;
mobile_banner;
subscribers;
sponsor_button;
subscribe_button;
header_links;
author: Author;
banner: Thumbnail[];
tv_banner: Thumbnail[];
mobile_banner: Thumbnail[];
subscribers: Text;
videos_count: Text;
sponsor_button: Button | null;
subscribe_button: SubscribeButton | null;
header_links: ChannelHeaderLinks | null;
channel_handle: Text;
channel_id: string;
constructor(data: any) {
super();
@@ -23,13 +31,16 @@ class C4TabbedHeader extends YTNode {
navigationEndpoint: data.navigationEndpoint
}, data.badges, data.avatar);
this.banner = data.banner ? Thumbnail.fromResponse(data.banner) : [];
this.tv_banner = data.tvBanner ? Thumbnail.fromResponse(data.tvBanner) : [];
this.mobile_banner = data.mobileBanner ? Thumbnail.fromResponse(data.mobileBanner) : [];
this.banner = Thumbnail.fromResponse(data.banner);
this.tv_banner = Thumbnail.fromResponse(data.tvBanner);
this.mobile_banner = Thumbnail.fromResponse(data.mobileBanner);
this.subscribers = new Text(data.subscriberCountText);
this.sponsor_button = data.sponsorButton ? Parser.parseItem(data.sponsorButton) : undefined;
this.subscribe_button = data.subscribeButton ? Parser.parseItem(data.subscribeButton) : undefined;
this.header_links = data.headerLinks ? Parser.parse(data.headerLinks) : undefined;
this.videos_count = new Text(data.videosCountText);
this.sponsor_button = Parser.parseItem<Button>(data.sponsorButton);
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
this.header_links = Parser.parseItem<ChannelHeaderLinks>(data.headerLinks);
this.channel_handle = new Text(data.channelHandleText);
this.channel_id = data.channelId;
}
}

View File

@@ -1,7 +1,7 @@
import Parser from '..';
import { YTNode } from '../helpers';
export default class CarouselHeader extends YTNode {
class CarouselHeader extends YTNode {
static type = 'CarouselHeader';
contents: YTNode[];
@@ -10,4 +10,6 @@ export default class CarouselHeader extends YTNode {
super();
this.contents = Parser.parseArray(data.contents);
}
}
}
export default CarouselHeader;

View File

@@ -3,7 +3,7 @@ import { YTNode } from '../helpers';
import Thumbnail from './misc/Thumbnail';
export default class CarouselItem extends YTNode {
class CarouselItem extends YTNode {
static type = 'CarouselItem';
items: YTNode[];
@@ -20,4 +20,6 @@ export default class CarouselItem extends YTNode {
this.pagination_thumbnails = Thumbnail.fromResponse(data.paginationThumbnails);
this.paginator_alignment = data.paginatorAlignment;
}
}
}
export default CarouselItem;

View File

@@ -1,6 +1,11 @@
import Parser from '..';
import Text from './misc/Text';
import Author from './misc/Author';
import NavigationEndpoint from './NavigationEndpoint';
import Text from './misc/Text';
import type SubscribeButton from './SubscribeButton';
import { YTNode } from '../helpers';
class Channel extends YTNode {
@@ -10,7 +15,10 @@ class Channel extends YTNode {
author: Author;
subscribers: Text;
videos: Text;
long_byline: Text;
short_byline: Text;
endpoint: NavigationEndpoint;
subscribe_button: SubscribeButton | null;
description_snippet: Text;
constructor(data: any) {
@@ -22,9 +30,13 @@ class Channel extends YTNode {
navigationEndpoint: data.navigationEndpoint
}, data.ownerBadges, data.thumbnail);
// TODO: subscriberCountText is now the channel's handle and videoCountText is the subscriber count. Why haven't they renamed the properties?
this.subscribers = new Text(data.subscriberCountText);
this.videos = new Text(data.videoCountText);
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.description_snippet = new Text(data.descriptionSnippet);
}
}

View File

@@ -1,7 +1,11 @@
import Parser from '../index';
import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import Text from './misc/Text';
import Parser from '../index';
import type Button from './Button';
import { YTNode } from '../helpers';
class ChannelAboutFullMetadata extends YTNode {
@@ -11,13 +15,20 @@ class ChannelAboutFullMetadata extends YTNode {
name: Text;
avatar: Thumbnail[];
canonical_channel_url: string;
primary_links: {
endpoint: NavigationEndpoint;
icon: Thumbnail[];
title: Text;
}[];
views: Text;
joined: Text;
description: Text;
email_reveal: NavigationEndpoint;
can_reveal_email: boolean;
country: Text;
buttons;
buttons: Button[];
constructor(data: any) {
super();
@@ -25,13 +36,20 @@ class ChannelAboutFullMetadata extends YTNode {
this.name = new Text(data.title);
this.avatar = Thumbnail.fromResponse(data.avatar);
this.canonical_channel_url = data.canonicalChannelUrl;
this.primary_links = data.primaryLinks.map((link: any) => ({
endpoint: new NavigationEndpoint(link.navigationEndpoint),
icon: Thumbnail.fromResponse(link.icon),
title: new Text(link.title)
}));
this.views = new Text(data.viewCountText);
this.joined = new Text(data.joinedDateText);
this.description = new Text(data.description);
this.email_reveal = new NavigationEndpoint(data.onBusinessEmailRevealClickCommand);
this.can_reveal_email = !data.signInForBusinessEmail;
this.country = new Text(data.country);
this.buttons = Parser.parse(data.actionButtons);
this.buttons = Parser.parseArray<Button>(data.actionButtons);
}
}

View File

@@ -4,7 +4,7 @@ import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
export default class CompactStation extends YTNode {
class CompactStation extends YTNode {
static type = 'CompactStation';
title: Text;
@@ -22,4 +22,6 @@ export default class CompactStation extends YTNode {
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
}
}
}
export default CompactStation;

View File

@@ -4,6 +4,8 @@ import Author from './misc/Author';
import { timeToSeconds } from '../../utils/Utils';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import type Menu from './menus/Menu';
import { YTNode } from '../helpers';
class CompactVideo extends YTNode {
@@ -25,7 +27,7 @@ class CompactVideo extends YTNode {
thumbnail_overlays;
endpoint: NavigationEndpoint;
menu;
menu: Menu | null;
constructor(data: any) {
super();
@@ -43,9 +45,9 @@ class CompactVideo extends YTNode {
seconds: timeToSeconds(new Text(data.lengthText).toString())
};
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.menu = Parser.parse(data.menu);
this.menu = Parser.parseItem<Menu>(data.menu);
}
get best_thumbnail() {

View File

@@ -4,7 +4,7 @@ import { YTNode } from '../helpers';
import Text from './misc/Text';
import NavigationEndpoint from './NavigationEndpoint';
export default class DefaultPromoPanel extends YTNode {
class DefaultPromoPanel extends YTNode {
static type = 'DefaultPromoPanel';
title: Text;
@@ -33,4 +33,6 @@ export default class DefaultPromoPanel extends YTNode {
this.metadata_order = data.metadataOrder;
this.panel_layout = data.panelLayout;
}
}
}
export default DefaultPromoPanel;

View File

@@ -0,0 +1,41 @@
import Parser from '..';
import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import Button from './Button';
import HorizontalCardList from './HorizontalCardList';
import { YTNode } from '../helpers';
class ExpandableMetadata extends YTNode {
static type = 'ExpandableMetadata';
header: {
collapsed_title: Text;
collapsed_thumbnail: Thumbnail[];
collapsed_label: Text;
expanded_title: Text;
};
expanded_content: HorizontalCardList | null;
expand_button: Button | null;
collapse_button: Button | null;
constructor(data: any) {
super();
this.header = {
collapsed_title: new Text(data.header.collapsedTitle),
collapsed_thumbnail: Thumbnail.fromResponse(data.header.collapsedThumbnail),
collapsed_label: new Text(data.header.collapsedLabel),
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);
}
}
export default ExpandableMetadata;

View File

@@ -2,7 +2,7 @@ import Parser from '../index';
import { YTNode } from '../helpers';
import ChipCloudChip from './ChipCloudChip';
export default class FeedFilterChipBar extends YTNode {
class FeedFilterChipBar extends YTNode {
static type = 'FeedFilterChipBar';
contents;
@@ -11,4 +11,6 @@ export default class FeedFilterChipBar extends YTNode {
super();
this.contents = Parser.parseArray<ChipCloudChip>(data.contents, ChipCloudChip);
}
}
}
export default FeedFilterChipBar;

View File

@@ -1,7 +1,7 @@
import Parser from '..';
import { YTNode } from '../helpers';
export default class GameCard extends YTNode {
class GameCard extends YTNode {
static type = 'GameCard';
game;
@@ -10,4 +10,6 @@ export default class GameCard extends YTNode {
super();
this.game = Parser.parseItem(data.game);
}
}
}
export default GameCard;

View File

@@ -4,7 +4,7 @@ import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
export default class GameDetails extends YTNode {
class GameDetails extends YTNode {
static type = 'GameDetails';
title: Text;
@@ -21,4 +21,6 @@ export default class GameDetails extends YTNode {
this.endpoint = new NavigationEndpoint(data.endpoint);
this.is_official_box_art = data.isOfficialBoxArt;
}
}
}
export default GameDetails;

View File

@@ -3,6 +3,9 @@ import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import Author from './misc/Author';
import type Menu from './menus/Menu';
import { YTNode } from '../helpers';
class GridVideo extends YTNode {
@@ -14,12 +17,12 @@ class GridVideo extends YTNode {
thumbnail_overlays;
rich_thumbnail;
published: Text;
duration: Text | string;
duration: Text | null;
author: Author;
views: Text;
short_view_count: Text;
endpoint: NavigationEndpoint;
menu;
menu: Menu | null;
constructor(data: any) {
super();
@@ -27,15 +30,15 @@ class GridVideo extends YTNode {
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.rich_thumbnail = data.richThumbnail && Parser.parse(data.richThumbnail);
this.published = new Text(data.publishedTimeText);
this.duration = data.lengthText ? new Text(data.lengthText) : length_alt?.text ? new Text(length_alt.text) : '';
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.parse(data.menu);
this.menu = Parser.parseItem<Menu>(data.menu);
}
}

View File

@@ -4,7 +4,7 @@ import { YTNode } from '../helpers';
class Panel {
static type = 'Panel';
thumbnail: {
thumbnail?: {
image: {
url: string;
width: number;
@@ -43,13 +43,15 @@ class Panel {
};
constructor(data: any) {
this.thumbnail = {
image: data.thumbnail.image.sources,
endpoint: new NavigationEndpoint(data.thumbnail.onTap),
on_long_press_endpoint: new NavigationEndpoint(data.thumbnail.onLongPress),
content_mode: data.thumbnail.contentMode,
crop_options: data.thumbnail.cropOptions
};
if (data.thumbnail) {
this.thumbnail = {
image: data.thumbnail.image.sources,
endpoint: new NavigationEndpoint(data.thumbnail.onTap),
on_long_press_endpoint: new NavigationEndpoint(data.thumbnail.onLongPress),
content_mode: data.thumbnail.contentMode,
crop_options: data.thumbnail.cropOptions
};
}
this.background_image = {
image: data.backgroundImage.image.sources,

View File

@@ -1,5 +1,8 @@
import Parser from '../index';
import { YTNode } from '../helpers';
import SearchRefinementCard from './SearchRefinementCard';
import Button from './Button';
import MacroMarkersListItem from './MacroMarkersListItem';
class HorizontalCardList extends YTNode {
static type = 'HorizontalCardList';
@@ -11,10 +14,10 @@ class HorizontalCardList extends YTNode {
constructor(data: any) {
super();
this.cards = Parser.parse(data.cards);
this.header = Parser.parse(data.header);
this.previous_button = Parser.parse(data.previousButton);
this.next_button = Parser.parse(data.nextButton);
this.cards = Parser.parseArray<SearchRefinementCard | MacroMarkersListItem>(data.cards);
this.header = Parser.parseItem(data.header);
this.previous_button = Parser.parseItem<Button>(data.previousButton, Button);
this.next_button = Parser.parseItem<Button>(data.nextButton, Button);
}
}

View File

@@ -7,7 +7,7 @@ import SubscribeButton from './SubscribeButton';
import MetadataBadge from './MetadataBadge';
import Button from './Button';
export default class InteractiveTabbedHeader extends YTNode {
class InteractiveTabbedHeader extends YTNode {
static type = 'InteractiveTabbedHeader';
header_type: string;
@@ -33,4 +33,6 @@ export default class InteractiveTabbedHeader extends YTNode {
this.buttons = Parser.parseArray<SubscribeButton | Button>(data.buttons, [ SubscribeButton, Button ]);
this.auto_generated = new Text(data.autoGenerated);
}
}
}
export default InteractiveTabbedHeader;

View File

@@ -0,0 +1,29 @@
import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import { YTNode } from '../helpers';
class MacroMarkersListItem extends YTNode {
static type = 'MacroMarkersListItem';
title: Text;
time_description: Text;
thumbnail: Thumbnail[];
on_tap_endpoint: NavigationEndpoint;
layout: string;
is_highlighted: boolean;
constructor(data: any) {
super();
this.title = new Text(data.title);
this.time_description = new Text(data.timeDescription);
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
this.on_tap_endpoint = new NavigationEndpoint(data.onTap);
this.layout = data.layout;
this.is_highlighted = data.isHighlighted;
}
}
export default MacroMarkersListItem;

View File

@@ -5,7 +5,8 @@ class MetadataBadge extends YTNode {
icon_type?: string;
style?: string;
tooltip: string | null;
label?: string;
tooltip?: string;
constructor(data: any) {
super();
@@ -18,7 +19,13 @@ class MetadataBadge extends YTNode {
this.style = data.style;
}
this.tooltip = data?.tooltip || data?.iconTooltip || null;
if (data?.label) {
this.style = data.label;
}
if (data?.tooltip || data?.iconTooltip) {
this.tooltip = data.tooltip || data.iconTooltip;
}
}
}

View File

@@ -1,7 +1,7 @@
import { YTNode } from '../helpers';
import Thumbnail from './misc/Thumbnail';
export default class PlaylistCustomThumbnail extends YTNode {
class PlaylistCustomThumbnail extends YTNode {
static type = 'PlaylistCustomThumbnail';
thumbnail: Thumbnail[];
@@ -10,4 +10,6 @@ export default class PlaylistCustomThumbnail extends YTNode {
super();
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
}
}
}
export default PlaylistCustomThumbnail;

View File

@@ -76,8 +76,8 @@ class PlaylistPanelVideo extends YTNode {
}));
}
this.badges = Parser.parse(data.badges);
this.menu = Parser.parse(data.menu);
this.badges = Parser.parseArray(data.badges);
this.menu = Parser.parseItem(data.menu);
this.set_video_id = data.playlistSetVideoId;
}
}

View File

@@ -3,6 +3,8 @@ import Parser from '../index';
import Thumbnail from './misc/Thumbnail';
import PlaylistAuthor from './misc/PlaylistAuthor';
import NavigationEndpoint from './NavigationEndpoint';
import type Menu from './menus/Menu';
import { YTNode } from '../helpers';
class PlaylistVideo extends YTNode {
@@ -17,7 +19,7 @@ class PlaylistVideo extends YTNode {
set_video_id: string | undefined;
endpoint: NavigationEndpoint;
is_playable: boolean;
menu;
menu: Menu | null;
duration: {
text: string;
@@ -35,7 +37,7 @@ class PlaylistVideo extends YTNode {
this.set_video_id = data?.setVideoId;
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.is_playable = data.isPlayable;
this.menu = Parser.parse(data.menu);
this.menu = Parser.parseItem<Menu>(data.menu);
this.duration = {
text: new Text(data.lengthText).text,
seconds: parseInt(data.lengthSeconds)

View File

@@ -5,7 +5,7 @@ import Button from './Button';
import Text from './misc/Text';
import Thumbnail from './misc/Thumbnail';
export default class RecognitionShelf extends YTNode {
class RecognitionShelf extends YTNode {
static type = 'RecognitionShelf';
title: Text;
@@ -23,4 +23,6 @@ export default class RecognitionShelf extends YTNode {
this.button = Parser.parseItem<Button>(data.button, Button);
this.surface = data.surface;
}
}
}
export default RecognitionShelf;

View File

@@ -16,7 +16,7 @@ class SectionList extends YTNode {
}
// TODO: this should be Parser#parseArray
this.contents = Parser.parse(data.contents);
this.contents = Parser.parseArray(data.contents);
if (data.continuations) {
if (data.continuations[0].nextContinuationData) {

View File

@@ -20,14 +20,14 @@ class Shelf extends YTNode {
this.endpoint = new NavigationEndpoint(data.endpoint);
}
this.content = Parser.parse(data.content) || null;
this.content = Parser.parseItem(data.content) || null;
if (data.icon?.iconType) {
this.icon_type = data.icon?.iconType;
}
if (data.menu) {
this.menu = Parser.parse(data.menu);
this.menu = Parser.parseItem(data.menu);
}
}
}

View File

@@ -1,7 +1,7 @@
import { YTNode } from '../helpers';
import Thumbnail from './misc/Thumbnail';
export default class ThumbnailLandscapePortrait extends YTNode {
class ThumbnailLandscapePortrait extends YTNode {
static type = 'ThumbnailLandscapePortrait';
landscape: Thumbnail[];
@@ -12,4 +12,6 @@ export default class ThumbnailLandscapePortrait extends YTNode {
this.landscape = Thumbnail.fromResponse(data.landscape);
this.portrait = Thumbnail.fromResponse(data.portrait);
}
}
}
export default ThumbnailLandscapePortrait;

View File

@@ -6,7 +6,7 @@ import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import SubscribeButton from './SubscribeButton';
export default class TopicChannelDetails extends YTNode {
class TopicChannelDetails extends YTNode {
static type = 'TopicChannelDetails';
title: Text;
@@ -24,4 +24,6 @@ export default class TopicChannelDetails extends YTNode {
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton, SubscribeButton);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
}
}
}
export default TopicChannelDetails;

View File

@@ -1,4 +1,5 @@
import Parser from '../index';
import Text from './misc/Text';
import { YTNode } from '../helpers';
class UniversalWatchCard extends YTNode {
@@ -7,13 +8,17 @@ class UniversalWatchCard extends YTNode {
header;
call_to_action;
sections;
collapsed_label?: Text;
constructor(data: any) {
super();
// TODO: use parseItem / parseArray for these
this.header = Parser.parse(data.header);
this.call_to_action = Parser.parse(data.callToAction);
this.sections = Parser.parse(data.sections);
this.header = Parser.parseItem(data.header);
this.call_to_action = Parser.parseItem(data.callToAction);
this.sections = Parser.parseArray(data.sections);
if (data.collapsedLabel) {
this.collapsed_label = new Text(data.collapsedLabel);
}
}
}

View File

@@ -13,7 +13,7 @@ class VerticalWatchCardList extends YTNode {
constructor(data: any) {
super();
this.items = Parser.parse(data.items);
this.items = Parser.parseArray(data.items);
this.contents = this.items; // XXX: alias for consistency
this.view_all_text = new Text(data.viewAllText);
this.view_all_endpoint = new NavigationEndpoint(data.viewAllEndpoint);

View File

@@ -1,9 +1,12 @@
import Parser from '../index';
import Parser from '..';
import Text from './misc/Text';
import Author from './misc/Author';
import Menu from './menus/Menu';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import MetadataBadge from './MetadataBadge';
import ExpandableMetadata from './ExpandableMetadata';
import { timeToSeconds } from '../../utils/Utils';
import { YTNode } from '../helpers';
@@ -17,11 +20,13 @@ class Video extends YTNode {
text: Text;
hover_text: Text;
}[];
expandable_metadata: ExpandableMetadata | null;
thumbnails: Thumbnail[];
thumbnail_overlays;
rich_thumbnail;
author: Author;
badges: MetadataBadge[];
endpoint: NavigationEndpoint;
published: Text;
view_count: Text;
@@ -35,7 +40,8 @@ class Video extends YTNode {
show_action_menu: boolean;
is_watched: boolean;
menu;
menu: Menu | null;
search_video_result_entity_key: string;
constructor(data: any) {
super();
@@ -53,10 +59,13 @@ class Video extends YTNode {
hover_text: new Text(snippet.snippetHoverText)
})) || [];
this.expandable_metadata = Parser.parseItem<ExpandableMetadata>(data.expandableMetadata);
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
this.rich_thumbnail = data.richThumbnail ? Parser.parse(data.richThumbnail) : null;
this.rich_thumbnail = data.richThumbnail ? Parser.parseItem(data.richThumbnail) : null;
this.author = new Author(data.ownerText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail);
this.badges = Parser.parseArray(data.badges, MetadataBadge);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.published = new Text(data.publishedTimeText);
this.view_count = new Text(data.viewCountText);
@@ -75,6 +84,7 @@ class Video extends YTNode {
this.show_action_menu = data.showActionMenu;
this.is_watched = data.isWatched || false;
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
this.search_video_result_entity_key = data.searchVideoResultEntityKey;
}
get description(): string {
@@ -84,22 +94,30 @@ class Video extends YTNode {
return this.description_snippet?.toString() || '';
}
/*
Get is_live() {
return this.badges.some((badge) => badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW');
get is_live(): boolean {
return this.badges.some((badge) => {
if (badge.label === 'BADGE_STYLE_TYPE_LIVE_NOW' || badge.style === 'LIVE')
return true;
});
}
*/
get is_upcoming(): boolean | undefined {
return this.upcoming && this.upcoming > new Date();
}
/*
Get has_captions() {
return this.badges.some((badge) => badge.label === 'CC');
}*/
get is_premiere(): boolean {
return this.badges.some((badge) => badge.style === 'PREMIERE');
}
get best_thumbnail(): Thumbnail | undefined{
get is_4k(): boolean {
return this.badges.some((badge) => badge.style === '4K');
}
get has_captions(): boolean {
return this.badges.some((badge) => badge.style === 'CC');
}
get best_thumbnail(): Thumbnail | undefined {
return this.thumbnails[0];
}
}

View File

@@ -1,9 +1,11 @@
import Video from './Video';
export default class VideoCard extends Video {
class VideoCard extends Video {
static type = 'VideoCard';
constructor(data: any) {
super(data);
}
}
}
export default VideoCard;

View File

@@ -13,9 +13,9 @@ class WatchCardHeroVideo extends YTNode {
constructor(data: any) {
super();
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.call_to_action_button = Parser.parse(data.callToActionButton);
this.hero_image = Parser.parse(data.heroImage);
this.label = data.lengthText.accessibility.accessibilityData.label;
this.call_to_action_button = Parser.parseItem(data.callToActionButton);
this.hero_image = Parser.parseItem(data.heroImage);
this.label = data.lengthText?.accessibility.accessibilityData.label || '';
}
}

View File

@@ -8,7 +8,7 @@ class WatchCardSectionSequence extends YTNode {
constructor(data: any) {
super();
this.lists = Parser.parse(data.lists);
this.lists = Parser.parseArray(data.lists);
}
}

View File

@@ -3,9 +3,15 @@ import NavigationEndpoint from '../NavigationEndpoint';
class TextRun {
text: string;
endpoint: NavigationEndpoint | undefined;
bold: boolean;
italics: boolean;
strikethrough: boolean;
constructor(data: any) {
this.text = data.text;
this.bold = Boolean(data.bold);
this.italics = Boolean(data.italics);
this.strikethrough = Boolean(data.strikethrough);
this.endpoint = data.navigationEndpoint ? new NavigationEndpoint(data.navigationEndpoint) : undefined;
}
}

View File

@@ -359,6 +359,10 @@ export type ObservedArray<T extends YTNode = YTNode> = Array<T> & {
* Returns all objects that match the rule.
*/
getAll: (rule: object, del_items?: boolean) => T[];
/**
* Returns the first object to match the condition.
*/
matchCondition: (condition: (node: T) => boolean) => T | undefined;
/**
* Removes the item at the given index.
*/
@@ -412,6 +416,14 @@ export function observe<T extends YTNode>(obj: Array<T>) {
);
}
if (prop == 'matchCondition') {
return (condition: (node: T) => boolean) => (
target.find((obj) => {
return condition(obj);
})
);
}
if (prop == 'filterType') {
return (...types: YTNodeConstructor<YTNode>[]) => {
return observe(target.filter((node: YTNode) => {

View File

@@ -73,6 +73,7 @@ import { default as Endscreen } from './classes/Endscreen';
import { default as EndscreenElement } from './classes/EndscreenElement';
import { default as EndScreenPlaylist } from './classes/EndScreenPlaylist';
import { default as EndScreenVideo } from './classes/EndScreenVideo';
import { default as ExpandableMetadata } from './classes/ExpandableMetadata';
import { default as ExpandableTab } from './classes/ExpandableTab';
import { default as ExpandedShelfContents } from './classes/ExpandedShelfContents';
import { default as FeedFilterChipBar } from './classes/FeedFilterChipBar';
@@ -137,6 +138,7 @@ import { default as LiveChatItemList } from './classes/LiveChatItemList';
import { default as LiveChatMessageInput } from './classes/LiveChatMessageInput';
import { default as LiveChatParticipant } from './classes/LiveChatParticipant';
import { default as LiveChatParticipantsList } from './classes/LiveChatParticipantsList';
import { default as MacroMarkersListItem } from './classes/MacroMarkersListItem';
import { default as Menu } from './classes/menus/Menu';
import { default as MenuNavigationItem } from './classes/menus/MenuNavigationItem';
import { default as MenuServiceItem } from './classes/menus/MenuServiceItem';
@@ -363,6 +365,7 @@ export const YTNodes = {
EndscreenElement,
EndScreenPlaylist,
EndScreenVideo,
ExpandableMetadata,
ExpandableTab,
ExpandedShelfContents,
FeedFilterChipBar,
@@ -427,6 +430,7 @@ export const YTNodes = {
LiveChatMessageInput,
LiveChatParticipant,
LiveChatParticipantsList,
MacroMarkersListItem,
Menu,
MenuNavigationItem,
MenuServiceItem,

View File

@@ -9,12 +9,13 @@ import MicroformatData from '../classes/MicroformatData';
import SubscribeButton from '../classes/SubscribeButton';
import Tab from '../classes/Tab';
import { InnertubeError } from '../../utils/Utils';
import FeedFilterChipBar from '../classes/FeedFilterChipBar';
import ChipCloudChip from '../classes/ChipCloudChip';
import FilterableFeed from '../../core/FilterableFeed';
import Feed from '../../core/Feed';
import { InnertubeError } from '../../utils/Utils';
export default class Channel extends TabbedFeed {
header;
metadata;
@@ -70,37 +71,37 @@ export default class Channel extends TabbedFeed {
}
async getHome() {
const tab = await this.getTab('Home');
const tab = await this.getTabByURL('featured');
return new Channel(this.actions, tab.page, true);
}
async getVideos() {
const tab = await this.getTab('Videos');
const tab = await this.getTabByURL('videos');
return new Channel(this.actions, tab.page, true);
}
async getShorts() {
const tab = await this.getTab('Shorts');
const tab = await this.getTabByURL('shorts');
return new Channel(this.actions, tab.page, true);
}
async getLiveStreams() {
const tab = await this.getTab('Live');
const tab = await this.getTabByURL('streams');
return new Channel(this.actions, tab.page, true);
}
async getPlaylists() {
const tab = await this.getTab('Playlists');
const tab = await this.getTabByURL('playlists');
return new Channel(this.actions, tab.page, true);
}
async getCommunity() {
const tab = await this.getTab('Community');
const tab = await this.getTabByURL('community');
return new Channel(this.actions, tab.page, true);
}
async getChannels() {
const tab = await this.getTab('Channels');
const tab = await this.getTabByURL('channels');
return new Channel(this.actions, tab.page, true);
}
@@ -109,7 +110,7 @@ export default class Channel extends TabbedFeed {
* Note that this does not return a new {@link Channel} object.
*/
async getAbout() {
const tab = await this.getTab('About');
const tab = await this.getTabByURL('about');
return tab.memo.getType(ChannelAboutFullMetadata)?.[0];
}

View File

@@ -39,10 +39,10 @@ class Library {
}
async #getAll(shelf: Shelf): Promise<Playlist | History | Feed> {
if (!shelf.menu?.item().as(Menu).hasKey('top_level_buttons'))
if (!shelf.menu?.as(Menu).hasKey('top_level_buttons'))
throw new InnertubeError(`The ${shelf.title.text} shelf doesn't have more items`);
const button = shelf.menu.item().as(Menu).top_level_buttons.get({ text: 'See all' });
const button = shelf.menu.as(Menu).top_level_buttons.get({ text: 'See all' });
if (!button)
throw new InnertubeError('Did not find target button.');

View File

@@ -7,6 +7,7 @@ import VideoOwner from '../classes/VideoOwner';
import PlaylistMetadata from '../classes/PlaylistMetadata';
import PlaylistSidebarPrimaryInfo from '../classes/PlaylistSidebarPrimaryInfo';
import PlaylistSidebarSecondaryInfo from '../classes/PlaylistSidebarSecondaryInfo';
import PlaylistCustomThumbnail from '../classes/PlaylistCustomThumbnail';
import PlaylistVideoThumbnail from '../classes/PlaylistVideoThumbnail';
import PlaylistHeader from '../classes/PlaylistHeader';
@@ -30,8 +31,8 @@ class Playlist extends Feed {
this.info = {
...this.page.metadata.item().as(PlaylistMetadata),
...{
author: secondary_info?.owner.item().as(VideoOwner).author,
thumbnails: primary_info?.thumbnail_renderer.item().as(PlaylistVideoThumbnail).thumbnail as Thumbnail[],
author: secondary_info?.owner.item().as(VideoOwner).author ?? header?.author,
thumbnails: primary_info?.thumbnail_renderer.item().as(PlaylistVideoThumbnail, PlaylistCustomThumbnail).thumbnail as Thumbnail[],
total_items: this.#getStat(0, primary_info),
views: this.#getStat(1, primary_info),
last_updated: this.#getStat(2, primary_info),

View File

@@ -1,19 +1,15 @@
import Actions from '../../core/Actions';
import { observe, ObservedArray, YTNode } from '../helpers';
import { ObservedArray, YTNode } from '../helpers';
import { InnertubeError } from '../../utils/Utils';
import Feed from '../../core/Feed';
import SectionList from '../classes/SectionList';
import ItemSection from '../classes/ItemSection';
import HorizontalCardList from '../classes/HorizontalCardList';
import RichListHeader from '../classes/RichListHeader';
import SearchRefinementCard from '../classes/SearchRefinementCard';
import TwoColumnSearchResults from '../classes/TwoColumnSearchResults';
import UniversalWatchCard from '../classes/UniversalWatchCard';
import WatchCardHeroVideo from '../classes/WatchCardHeroVideo';
import WatchCardSectionSequence from '../classes/WatchCardSectionSequence';
class Search extends Feed {
export default class Search extends Feed {
results: ObservedArray<YTNode> | null | undefined;
refinements;
estimated_results;
@@ -24,30 +20,16 @@ class Search extends Feed {
super(actions, data, already_parsed);
const contents =
this.page.contents?.item().as(TwoColumnSearchResults).primary_contents?.item().as(SectionList).contents.array() ||
this.page.contents_memo.getType(SectionList)?.[0]?.contents ||
this.page.on_response_received_commands?.[0].contents;
const secondary_contents_maybe = this.page.contents?.item().key('secondary_contents');
const secondary_contents = secondary_contents_maybe?.isParsed() ? secondary_contents_maybe.parsed().item().key('contents').parsed().array() : undefined;
this.results = contents.firstOfType(ItemSection)?.contents;
const card_list = this.results?.get({ type: 'HorizontalCardList' }, true)?.as(HorizontalCardList);
const universal_watch_card = secondary_contents?.firstOfType(UniversalWatchCard);
this.refinements = this.page.refinements || [];
this.estimated_results = this.page.estimated_results;
this.watch_card = {
header: universal_watch_card?.header.item() || null,
call_to_action: universal_watch_card?.call_to_action?.item()?.as(WatchCardHeroVideo) || null,
sections: universal_watch_card?.sections?.array()?.filterType(WatchCardSectionSequence) || []
};
this.refinement_cards = {
header: card_list?.header.item().as(RichListHeader) || null,
cards: card_list?.cards.array().filterType(SearchRefinementCard) || observe([] as SearchRefinementCard[])
};
this.watch_card = this.page?.contents_memo.getType(UniversalWatchCard)?.[0];
this.refinement_cards = this.results?.get({ type: 'HorizontalCardList' }, true)?.as(HorizontalCardList);
}
/**
@@ -57,7 +39,8 @@ class Search extends Feed {
let target_card: SearchRefinementCard | undefined;
if (typeof card === 'string') {
target_card = this.refinement_cards.cards.get({ query: card });
if (!this.refinement_cards) throw new InnertubeError('No refinement cards found.');
target_card = this.refinement_cards?.cards.get({ query: card })?.as(SearchRefinementCard);
if (!target_card)
throw new InnertubeError(`Refinement card "${card}" not found`, { available_cards: this.refinement_card_queries });
} else if (card.type === 'SearchRefinementCard') {
@@ -71,8 +54,11 @@ class Search extends Feed {
return new Search(this.actions, page, true);
}
/**
* Returns a list of refinement card queries.
*/
get refinement_card_queries() {
return this.refinement_cards.cards.map((card) => card.query);
return this.refinement_cards?.cards.as(SearchRefinementCard).map((card) => card.query);
}
/**
@@ -82,6 +68,4 @@ class Search extends Feed {
const continuation = await this.getContinuationData();
return new Search(this.actions, continuation, true);
}
}
export default Search;
}

View File

@@ -31,7 +31,7 @@ class Settings {
if (!tab)
throw new InnertubeError('Target tab not found');
const contents = tab.content?.as(SectionList).contents.array().as(ItemSection);
const contents = tab.content?.as(SectionList).contents.as(ItemSection);
this.introduction = contents?.shift()?.contents?.get({ type: 'PageIntroduction' })?.as(PageIntroduction);

View File

@@ -19,7 +19,7 @@ class TimeWatched {
if (!tab)
throw new InnertubeError('Could not find target tab.');
this.contents = tab.content?.as(SectionList).contents.array().as(ItemSection);
this.contents = tab.content?.as(SectionList).contents.as(ItemSection);
}
get page(): ParsedResponse {

View File

@@ -28,8 +28,8 @@ class Explore {
if (!section_list)
throw new InnertubeError('Target tab did not have any content.');
this.top_buttons = section_list.contents.array().firstOfType(Grid)?.items.as(MusicNavigationButton) || ([] as MusicNavigationButton[]);
this.sections = section_list.contents.array().getAll({ type: 'MusicCarouselShelf' }) as MusicCarouselShelf[];
this.top_buttons = section_list.contents.firstOfType(Grid)?.items.as(MusicNavigationButton) || ([] as MusicNavigationButton[]);
this.sections = section_list.contents.getAll({ type: 'MusicCarouselShelf' }) as MusicCarouselShelf[];
}
get page(): ParsedResponse {

View File

@@ -33,7 +33,7 @@ class HomeFeed {
}
this.#continuation = tab.content?.as(SectionList).continuation;
this.sections = tab.content?.as(SectionList).contents.array().as(MusicCarouselShelf);
this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf);
}
/**

View File

@@ -30,7 +30,7 @@ class Library {
const section_list = this.#page.contents_memo.getType(SectionList)?.[0];
this.header = section_list?.header?.item().as(MusicSideAlignedItem);
this.contents = section_list?.contents?.array().as(Grid, MusicShelf);
this.contents = section_list?.contents?.as(Grid, MusicShelf);
this.#continuation = this.contents?.find((list: Grid | MusicShelf) => list.continuation)?.continuation;
}

View File

@@ -61,7 +61,7 @@ class Playlist {
/**
* Retrieves related playlists
*/
async getRelated() {
async getRelated(): Promise<MusicCarouselShelf> {
let section_continuation = this.#page.contents_memo.get('SectionList')?.[0].as(SectionList).continuation;
while (section_continuation) {
@@ -74,19 +74,15 @@ class Playlist {
const section_list = data.continuation_contents?.as(SectionListContinuation);
const sections = section_list?.contents?.as(MusicCarouselShelf, MusicShelf);
const related = sections?.filter(
(section) =>
section.is(MusicCarouselShelf) ? section.header?.title.toString() === 'Related playlists' :
section.title.toString() === 'Related playlists'
)[0];
const related = sections?.matchCondition((section) => section.is(MusicCarouselShelf))?.as(MusicCarouselShelf);
if (related)
return related.contents || [];
return related;
section_continuation = section_list?.continuation;
}
return [];
throw new InnertubeError('Target section not found.');
}
async getSuggestions(refresh = true) {
@@ -115,9 +111,7 @@ class Playlist {
const section_list = page.continuation_contents?.as(SectionListContinuation);
const sections = section_list?.contents?.as(MusicCarouselShelf, MusicShelf);
const suggestions = sections?.filter(
(section) => section.is(MusicShelf) && section.title.toString() === 'Suggestions'
)[0] as MusicShelf | undefined;
const suggestions = sections?.matchCondition((section) => section.is(MusicShelf))?.as(MusicShelf);
return {
items: suggestions?.contents || [],

View File

@@ -37,7 +37,7 @@ class Recap {
if (!tab)
throw new InnertubeError('Target tab not found');
this.sections = tab.content?.as(SectionList).contents.array().as(ItemSection, MusicCarouselShelf, Message);
this.sections = tab.content?.as(SectionList).contents.as(ItemSection, MusicCarouselShelf, Message);
}
/**

View File

@@ -49,7 +49,7 @@ class Search {
this.header = tab_content.hasKey('header') ? tab_content.header?.item().as(ChipCloud) : null;
const shelves = tab_content.contents.array().as(MusicShelf, ItemSection);
const shelves = tab_content.contents.as(MusicShelf, ItemSection);
const item_section = shelves.firstOfType(ItemSection);
this.did_you_mean = item_section?.contents?.firstOfType(DidYouMean) || null;

View File

@@ -4,9 +4,7 @@ import Constants from '../../utils/Constants';
import { InnertubeError } from '../../utils/Utils';
import Tab from '../classes/Tab';
import Tabbed from '../classes/Tabbed';
import WatchNextTabbedResults from '../classes/WatchNextTabbedResults';
import SingleColumnMusicWatchNextResults from '../classes/SingleColumnMusicWatchNextResults';
import MicroformatData from '../classes/MicroformatData';
import PlayerOverlay from '../classes/PlayerOverlay';
import PlaylistPanel from '../classes/PlaylistPanel';
@@ -70,8 +68,7 @@ class TrackInfo {
this.#playback_tracking = info.playback_tracking;
if (next) {
const single_col = next.contents.item().as(SingleColumnMusicWatchNextResults);
const tabbed_results = single_col.contents.item().as(Tabbed).contents.item().as(WatchNextTabbedResults);
const tabbed_results = next.contents_memo.getType(WatchNextTabbedResults)?.[0];
this.tabs = tabbed_results.tabs.array().as(Tab);
this.current_video_endpoint = next.current_video_endpoint;
@@ -84,14 +81,17 @@ class TrackInfo {
/**
* Retrieves contents of the given tab.
*/
async getTab(title: string) {
async getTab(title_or_page_type: string) {
if (!this.tabs)
throw new InnertubeError('Could not find any tab');
const target_tab = this.tabs.get({ title });
const target_tab =
this.tabs.get({ title: title_or_page_type }) ||
this.tabs.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === title_or_page_type) ||
this.tabs?.[0];
if (!target_tab)
throw new InnertubeError(`Tab "${title}" not found`, { available_tabs: this.available_tabs });
throw new InnertubeError(`Tab "${title_or_page_type}" not found`, { available_tabs: this.available_tabs });
if (target_tab.content)
return target_tab.content;
@@ -101,7 +101,7 @@ class TrackInfo {
if (page.contents.item().key('type').string() === 'Message')
return page.contents.item().as(Message);
return page.contents.item().as(SectionList).contents.array();
return page.contents.item().as(SectionList).contents;
}
/**
@@ -140,7 +140,7 @@ class TrackInfo {
* Retrieves related content.
*/
async getRelated(): Promise<ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>> {
const tab = await this.getTab('Related') as ObservedArray<MusicDescriptionShelf | MusicDescriptionShelf>;
const tab = await this.getTab('MUSIC_PAGE_TYPE_TRACK_RELATED') as ObservedArray<MusicDescriptionShelf | MusicDescriptionShelf>;
return tab;
}
@@ -148,7 +148,7 @@ class TrackInfo {
* Retrieves lyrics.
*/
async getLyrics(): Promise<MusicDescriptionShelf | undefined> {
const tab = await this.getTab('Lyrics') as ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>;
const tab = await this.getTab('MUSIC_PAGE_TYPE_TRACK_LYRICS') as ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>;
return tab.firstOfType(MusicDescriptionShelf);
}

View File

@@ -48,7 +48,7 @@ export const CLIENTS = Object.freeze({
},
YTMUSIC_ANDROID: {
NAME: 'ANDROID_MUSIC',
VERSION: '5.17.51'
VERSION: '5.34.51'
},
TV_EMBEDDED: {
NAME: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',

View File

@@ -48,7 +48,7 @@ export default class HTTPClient {
const request_headers = new Headers(headers);
request_headers.set('Accept', '*/*');
request_headers.set('Accept-Language', `en-${this.#session.context.client.gl || 'US'}`);
request_headers.set('Accept-Language', `${this.#session.context.client.hl}-${this.#session.context.client.gl}`);
request_headers.set('x-goog-visitor-id', this.#session.context.client.visitorData || '');
request_headers.set('x-origin', request_url.origin);
request_headers.set('x-youtube-client-version', this.#session.context.client.clientVersion || '');

View File

@@ -10,6 +10,10 @@ export const VIDEOS = [
{
ID: 'I1qsF0WQy8c',
QUERY: 'mkbhd',
},
{
ID: 'OqiXFXlYFi8',
QUERY: 'formatted comment text'
}
];

View File

@@ -2,6 +2,7 @@ import fs from 'fs';
import Innertube from '..';
import { CHANNELS, VIDEOS } from './constants';
import { streamToIterable } from '../src/utils/Utils';
import TextRun from '../src/parser/classes/misc/TextRun';
describe('YouTube.js Tests', () => {
let yt: Innertube;
@@ -68,6 +69,19 @@ describe('YouTube.js Tests', () => {
threads = await yt.getComments(VIDEOS[1].ID);
expect(threads.contents.length).toBeGreaterThan(0);
});
it('should parse formatted comments', async () => {
const threads = await yt.getComments(VIDEOS[3].ID);
const authorComment = threads.contents.find(t => t.comment?.author_is_channel_owner)
expect(authorComment).not.toBeUndefined();
expect(authorComment!.comment?.content.runs?.length).toBeGreaterThan(0)
const runs = authorComment!.comment!.content.runs! as TextRun[]
expect(runs[0].bold).toBeTruthy()
expect(runs[2].italics).toBeTruthy()
expect(runs[4].strikethrough).toBeTruthy()
})
it('should retrieve next batch of comments', async () => {
const next = await threads.getContinuation();