mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f66f0bd656 | ||
|
|
05de3ec97a | ||
|
|
a0566969ba | ||
|
|
a9cad49333 | ||
|
|
096bf362c9 | ||
|
|
ec9c0979f5 | ||
|
|
342d1d95e9 | ||
|
|
dbfc569602 | ||
|
|
c16a967987 | ||
|
|
a07375eb20 | ||
|
|
ce9d9c56b4 | ||
|
|
f50ce1a06b | ||
|
|
3b6ccfa3d8 | ||
|
|
878488d1b3 | ||
|
|
3c94c9da4b | ||
|
|
0b301de6a1 | ||
|
|
c9135e66d3 | ||
|
|
e82c843928 | ||
|
|
be71d7c937 | ||
|
|
470d8d9406 | ||
|
|
2c5907f80f | ||
|
|
ade5feb31c | ||
|
|
13ebf0a039 | ||
|
|
cb8fafe94b | ||
|
|
bd35faa597 | ||
|
|
a8b507ee65 | ||
|
|
e7eacd9742 | ||
|
|
1c72a41675 | ||
|
|
62a68b207c | ||
|
|
1d9587e8c1 |
33
.github/pull_request_template.md
vendored
33
.github/pull_request_template.md
vendored
@@ -1,27 +1,6 @@
|
||||
# Pull Request Template
|
||||
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] I have checked my code and corrected any misspellings
|
||||
<!-- Thank you for submitting a Pull Request! Please:
|
||||
* Read our contributing guidelines: https://github.com/LuanRT/YouTube.js/blob/main/CONTRIBUTING.md
|
||||
* Add "Fixes #<issue_number>" to the PR description if you are fixing an issue.
|
||||
* Ensure that the code is up-to-date with the `main` branch.
|
||||
* Include a description of the proposed changes and how to test them.
|
||||
-->
|
||||
44
CHANGELOG.md
44
CHANGELOG.md
@@ -1,5 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
## [4.3.0](https://github.com/LuanRT/YouTube.js/compare/v4.2.0...v4.3.0) (2023-04-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **GridVideo:** add `upcoming`, `upcoming_text`, `is_reminder_set` and `buttons` ([05de3ec](https://github.com/LuanRT/YouTube.js/commit/05de3ec97a1fea92543b5e5f84933b86a07ab830)), closes [#380](https://github.com/LuanRT/YouTube.js/issues/380)
|
||||
* **MusicResponsiveListItem:** make flex/fixed cols public ([#382](https://github.com/LuanRT/YouTube.js/issues/382)) ([096bf36](https://github.com/LuanRT/YouTube.js/commit/096bf362c9bd46a510ecb0d01623c70841e26e26))
|
||||
* **ToggleMenuServiceItem:** parse default nav endpoint ([a056696](https://github.com/LuanRT/YouTube.js/commit/a0566969ba436f31ca3722d09442a0c6302235d7))
|
||||
* **ytmusic:** add taste builder nodes ([#383](https://github.com/LuanRT/YouTube.js/issues/383)) ([a9cad49](https://github.com/LuanRT/YouTube.js/commit/a9cad49333aa85c98bbb96e5f2d5b57d9beeb0c7))
|
||||
|
||||
## [4.2.0](https://github.com/LuanRT/YouTube.js/compare/v4.1.1...v4.2.0) (2023-04-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Enable importHelpers in tsconfig to reduce output size ([#378](https://github.com/LuanRT/YouTube.js/issues/378)) ([0b301de](https://github.com/LuanRT/YouTube.js/commit/0b301de6a1e1352a64881c1751a84360922a77cd))
|
||||
* **parser:** ignore PrimetimePromo node ([ce9d9c5](https://github.com/LuanRT/YouTube.js/commit/ce9d9c56b4f45c0139d74edc95c295ecfd1ee4b1))
|
||||
* **PlaylistVideo:** Extract video_info and accessibility_label texts ([#376](https://github.com/LuanRT/YouTube.js/issues/376)) ([c9135e6](https://github.com/LuanRT/YouTube.js/commit/c9135e66d3c9c72b8d063eedcf3cc2123800946d))
|
||||
|
||||
## [4.1.1](https://github.com/LuanRT/YouTube.js/compare/v4.1.0...v4.1.1) (2023-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **PlayerCaptionsTracklist:** parse props only if they exist in the node ([470d8d9](https://github.com/LuanRT/YouTube.js/commit/470d8d94063f0159cd005c9eb15fd1a4a175bea0)), closes [#372](https://github.com/LuanRT/YouTube.js/issues/372)
|
||||
* **Search:** Return search results even if there are ads ([#373](https://github.com/LuanRT/YouTube.js/issues/373)) ([2c5907f](https://github.com/LuanRT/YouTube.js/commit/2c5907f80fd76452afe87d1722fe35a4f45a22e0))
|
||||
|
||||
## [4.1.0](https://github.com/LuanRT/YouTube.js/compare/v4.0.1...v4.1.0) (2023-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Session:** allow setting a custom visitor data token ([#371](https://github.com/LuanRT/YouTube.js/issues/371)) ([13ebf0a](https://github.com/LuanRT/YouTube.js/commit/13ebf0a03973e7ba7b65e9f72c4333927e4254f6))
|
||||
* **ShowingResultsFor:** parse all props ([1d9587e](https://github.com/LuanRT/YouTube.js/commit/1d9587e8c1ee0b11bb0e444c3d1e98162e9e1059))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **http:** android tv http client missing `clientName` ([#370](https://github.com/LuanRT/YouTube.js/issues/370)) ([cb8fafe](https://github.com/LuanRT/YouTube.js/commit/cb8fafe94b8ab330ae58211a892923321d65d890))
|
||||
* **node:** Electron apps crashing ([#367](https://github.com/LuanRT/YouTube.js/issues/367)) ([e7eacd9](https://github.com/LuanRT/YouTube.js/commit/e7eacd974211c90e7fbddfbf8019388cda3dfa5a))
|
||||
* **parser:** Make Video.is_live work on channel pages ([#368](https://github.com/LuanRT/YouTube.js/issues/368)) ([bd35faa](https://github.com/LuanRT/YouTube.js/commit/bd35faa5978f0b822e98d019523be1303374ddc0))
|
||||
* **toDash:** Generate unique Representation ids ([#366](https://github.com/LuanRT/YouTube.js/issues/366)) ([a8b507e](https://github.com/LuanRT/YouTube.js/commit/a8b507ee65ccd8edc9aea2aef8a908fa272bb23c))
|
||||
* **Utils:** Properly parse timestamps with thousands separators ([#363](https://github.com/LuanRT/YouTube.js/issues/363)) ([1c72a41](https://github.com/LuanRT/YouTube.js/commit/1c72a41675e47f711bd61ebb898bca5527406a79))
|
||||
|
||||
## [4.0.1](https://github.com/LuanRT/YouTube.js/compare/v4.0.0...v4.0.1) (2023-03-16)
|
||||
|
||||
|
||||
|
||||
84
README.md
84
README.md
@@ -3,14 +3,12 @@
|
||||
[versions]: https://www.npmjs.com/package/youtubei.js?activeTab=versions
|
||||
[codefactor]: https://www.codefactor.io/repository/github/luanrt/youtube.js
|
||||
[actions]: https://github.com/LuanRT/YouTube.js/actions
|
||||
[say-thanks]: https://saythanks.io/to/LuanRT
|
||||
[collaborators]: https://github.com/LuanRT/YouTube.js/blob/main/COLLABORATORS.md
|
||||
|
||||
<!-- OTHER LINKS -->
|
||||
[project]: https://github.com/LuanRT/YouTube.js
|
||||
[twitter]: https://twitter.com/thesciencephile
|
||||
[discord]: https://discord.gg/syDu7Yks54
|
||||
[nodejs]: https://nodejs.org
|
||||
|
||||
<h1 align=center>YouTube.js</h1>
|
||||
|
||||
@@ -23,33 +21,28 @@
|
||||
[][codefactor]
|
||||
[][npm]
|
||||
[][discord]
|
||||
[][say-thanks]
|
||||
<br>
|
||||
[][collaborators]
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a><sub>Special thanks to:<sub></a>
|
||||
</p>
|
||||
|
||||
<table align="center">
|
||||
<body>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://serpapi.com/" target="_blank">
|
||||
<img width="80" alt="SerpApi" src="https://luanrt.is-a.dev/assets/img/serpapi.svg" />
|
||||
<br>
|
||||
<b>
|
||||
<sub>
|
||||
Scrape Google and other search engines from a fast, easy and complete API.
|
||||
</sub>
|
||||
</b>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</body>
|
||||
</table>
|
||||
<div align="center">
|
||||
<p>
|
||||
<sup>Special thanks to:</sup>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://serpapi.com" target="_blank">
|
||||
<img width="80" alt="SerpApi" src="https://luanrt.is-a.dev/assets/img/serpapi.svg" />
|
||||
<br>
|
||||
<sub>
|
||||
API to get search engine results with ease.
|
||||
</sub>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
## Table of Contents
|
||||
<ol>
|
||||
@@ -130,6 +123,7 @@ const youtube = await Innertube.create(/* options */);
|
||||
| `lang` | `string` | Language. | `en` |
|
||||
| `location` | `string` | Geolocation. | `US` |
|
||||
| `account_index` | `number` | The account index to use. This is useful if you have multiple accounts logged in. **NOTE:** Only works if you are signed in with cookies. | `0` |
|
||||
| `visitor_data` | `string` | Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in. A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint. | `undefined` |
|
||||
| `retrieve_player` | `boolean` | Specifies whether to retrieve the JS player. Disabling this will make session creation faster. **NOTE:** Deciphering formats is not possible without the JS player. | `true` |
|
||||
| `enable_safety_mode` | `boolean` | Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content. | `false` |
|
||||
| `generate_session_locally` | `boolean` | Specifies whether to generate the session data locally or retrieve it from YouTube. This can be useful if you need more performance. | `false` |
|
||||
@@ -248,7 +242,7 @@ const yt = await Innertube.create({
|
||||
* `Innertube`
|
||||
|
||||
<details>
|
||||
<summary>Objects</summary>
|
||||
<summary>Properties</summary>
|
||||
<p>
|
||||
|
||||
* [.session](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/session.md)
|
||||
@@ -292,7 +286,7 @@ const yt = await Innertube.create({
|
||||
</details>
|
||||
|
||||
<a name="getinfo"></a>
|
||||
### getInfo(target, client?)
|
||||
### `getInfo(target, client?)`
|
||||
|
||||
Retrieves video info.
|
||||
|
||||
@@ -356,7 +350,7 @@ Retrieves video info.
|
||||
</details>
|
||||
|
||||
<a name="getbasicinfo"></a>
|
||||
### getBasicInfo(video_id, client?)
|
||||
### `getBasicInfo(video_id, client?)`
|
||||
|
||||
Suitable for cases where you only need basic video metadata. Also, it is faster than [`getInfo()`](#getinfo).
|
||||
|
||||
@@ -368,7 +362,7 @@ Suitable for cases where you only need basic video metadata. Also, it is faster
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC_ANDROID`, `YTMUSIC`, `TV_EMBEDDED` |
|
||||
|
||||
<a name="search"></a>
|
||||
### search(query, filters?)
|
||||
### `search(query, filters?)`
|
||||
|
||||
Searches the given query on YouTube.
|
||||
|
||||
@@ -413,7 +407,7 @@ Searches the given query on YouTube.
|
||||
</details>
|
||||
|
||||
<a name="getsearchsuggestions"></a>
|
||||
### getSearchSuggestions(query)
|
||||
### `getSearchSuggestions(query)`
|
||||
Retrieves search suggestions for given query.
|
||||
|
||||
**Returns**: `Promise<string[]>`
|
||||
@@ -423,7 +417,7 @@ Retrieves search suggestions for given query.
|
||||
| query | `string` | The search query |
|
||||
|
||||
<a name="getcomments"></a>
|
||||
### getComments(video_id, sort_by?)
|
||||
### `getComments(video_id, sort_by?)`
|
||||
Retrieves comments for given video.
|
||||
|
||||
**Returns**: `Promise<Comments>`
|
||||
@@ -436,7 +430,7 @@ Retrieves comments for given video.
|
||||
See [`./examples/comments`](https://github.com/LuanRT/YouTube.js/blob/main/examples/comments) for examples.
|
||||
|
||||
<a name="gethomefeed"></a>
|
||||
### getHomeFeed()
|
||||
### `getHomeFeed()`
|
||||
Retrieves YouTube's home feed.
|
||||
|
||||
**Returns**: `Promise<HomeFeed>`
|
||||
@@ -470,13 +464,13 @@ Retrieves YouTube's home feed.
|
||||
</details>
|
||||
|
||||
<a name="getguide"></a>
|
||||
### getGuide()
|
||||
### `getGuide()`
|
||||
Retrieves YouTube's content guide.
|
||||
|
||||
**Returns**: `Promise<Guide>`
|
||||
|
||||
<a name="getlibrary"></a>
|
||||
### getLibrary()
|
||||
### `getLibrary()`
|
||||
Retrieves the account's library.
|
||||
|
||||
**Returns**: `Promise<Library>`
|
||||
@@ -498,7 +492,7 @@ Retrieves the account's library.
|
||||
</details>
|
||||
|
||||
<a name="gethistory"></a>
|
||||
### getHistory()
|
||||
### `getHistory()`
|
||||
Retrieves watch history.
|
||||
|
||||
**Returns**: `Promise<History>`
|
||||
@@ -517,19 +511,19 @@ Retrieves watch history.
|
||||
</details>
|
||||
|
||||
<a name="gettrending"></a>
|
||||
### getTrending()
|
||||
### `getTrending()`
|
||||
Retrieves trending content.
|
||||
|
||||
**Returns**: `Promise<TabbedFeed<IBrowseResponse>>`
|
||||
|
||||
<a name="getsubscriptionsfeed"></a>
|
||||
### getSubscriptionsFeed()
|
||||
### `getSubscriptionsFeed()`
|
||||
Retrieves the subscriptions feed.
|
||||
|
||||
**Returns**: `Promise<Feed<IBrowseResponse>>`
|
||||
|
||||
<a name="getchannel"></a>
|
||||
### getChannel(id)
|
||||
### `getChannel(id)`
|
||||
Retrieves contents for a given channel.
|
||||
|
||||
**Returns**: `Promise<Channel>`
|
||||
@@ -569,7 +563,7 @@ Retrieves contents for a given channel.
|
||||
See [`./examples/channel`](https://github.com/LuanRT/YouTube.js/blob/main/examples/channel) for examples.
|
||||
|
||||
<a name="getnotifications"></a>
|
||||
### getNotifications()
|
||||
### `getNotifications()`
|
||||
Retrieves notifications.
|
||||
|
||||
**Returns**: `Promise<NotificationsMenu>`
|
||||
@@ -585,13 +579,13 @@ Retrieves notifications.
|
||||
</details>
|
||||
|
||||
<a name="getunseennotificationscount"></a>
|
||||
### getUnseenNotificationsCount()
|
||||
### `getUnseenNotificationsCount()`
|
||||
Retrieves unseen notifications count.
|
||||
|
||||
**Returns**: `Promise<number>`
|
||||
|
||||
<a name="getplaylist"></a>
|
||||
### getPlaylist(id)
|
||||
### `getPlaylist(id)`
|
||||
Retrieves playlist contents.
|
||||
|
||||
**Returns**: `Promise<Playlist>`
|
||||
@@ -614,7 +608,7 @@ Retrieves playlist contents.
|
||||
</details>
|
||||
|
||||
<a name="gethashtag"></a>
|
||||
### getHashtag(hashtag)
|
||||
### `getHashtag(hashtag)`
|
||||
Retrieves a given hashtag's page.
|
||||
|
||||
**Returns**: `Promise<HashtagFeed>`
|
||||
@@ -639,7 +633,7 @@ Retrieves a given hashtag's page.
|
||||
</details>
|
||||
|
||||
<a name="getstreamingdata"></a>
|
||||
### getStreamingData(video_id, options)
|
||||
### `getStreamingData(video_id, options)`
|
||||
Returns deciphered streaming data.
|
||||
|
||||
> **Note**
|
||||
@@ -665,7 +659,7 @@ console.info('Playback url:', url);
|
||||
| options | `FormatOptions` | Format options |
|
||||
|
||||
<a name="download"></a>
|
||||
### download(video_id, options?)
|
||||
### `download(video_id, options?)`
|
||||
Downloads a given video.
|
||||
|
||||
**Returns**: `Promise<ReadableStream<Uint8Array>>`
|
||||
@@ -678,7 +672,7 @@ Downloads a given video.
|
||||
See [`./examples/download`](https://github.com/LuanRT/YouTube.js/blob/main/examples/download) for examples.
|
||||
|
||||
<a name="resolveurl"></a>
|
||||
### resolveURL(url)
|
||||
### `resolveURL(url)`
|
||||
Resolves a given url.
|
||||
|
||||
**Returns**: `Promise<NavigationEndpoint>`
|
||||
@@ -688,7 +682,7 @@ Resolves a given url.
|
||||
| url | `string` | Url to resolve |
|
||||
|
||||
<a name="call"></a>
|
||||
### call(endpoint, args?)
|
||||
### `call(endpoint, args?)`
|
||||
Utility to call navigation endpoints.
|
||||
|
||||
**Returns**: `Promise<T extends IParsedResponse | IParsedResponse | ApiResponse>`
|
||||
|
||||
75
package-lock.json
generated
75
package-lock.json
generated
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "4.0.1",
|
||||
"version": "4.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "4.0.1",
|
||||
"version": "4.3.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jintr": "^0.4.1",
|
||||
"jintr": "^1.0.0",
|
||||
"linkedom": "^0.14.12",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1574,6 +1575,12 @@
|
||||
"node": ">=12 <14 || 14.2 - 14.9 || >14.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/fslib/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@yarnpkg/libzip": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.2.4.tgz",
|
||||
@@ -1587,6 +1594,12 @@
|
||||
"node": ">=12 <14 || 14.2 - 14.9 || >14.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/libzip/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
|
||||
@@ -4394,9 +4407,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jintr": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.4.1.tgz",
|
||||
"integrity": "sha512-R42VuIoTjsGbZuEmtT7WqyErd9JQuuV17Cg05wQwRWkQbmQNm2zO519Af1Ib7P7SBATqSMbhyu2/VcTnb3TcOg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-1.0.0.tgz",
|
||||
"integrity": "sha512-Kbyb5jiIzmTrbhbdjQGt+jjVzn9BPluvL3mZU5ihFQIEGjCHUA4+rsXE2PNDKmg1UlfdTn3947aSwWOVnc5UIw==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
@@ -6193,10 +6206,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||
},
|
||||
"node_modules/tsutils": {
|
||||
"version": "3.21.0",
|
||||
@@ -6213,6 +6225,12 @@
|
||||
"typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
|
||||
}
|
||||
},
|
||||
"node_modules/tsutils/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -7641,6 +7659,14 @@
|
||||
"requires": {
|
||||
"@yarnpkg/libzip": "^2.2.4",
|
||||
"tslib": "^1.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@yarnpkg/libzip": {
|
||||
@@ -7651,6 +7677,14 @@
|
||||
"requires": {
|
||||
"@types/emscripten": "^1.38.0",
|
||||
"tslib": "^1.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
@@ -9619,9 +9653,9 @@
|
||||
}
|
||||
},
|
||||
"jintr": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.4.1.tgz",
|
||||
"integrity": "sha512-R42VuIoTjsGbZuEmtT7WqyErd9JQuuV17Cg05wQwRWkQbmQNm2zO519Af1Ib7P7SBATqSMbhyu2/VcTnb3TcOg==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-1.0.0.tgz",
|
||||
"integrity": "sha512-Kbyb5jiIzmTrbhbdjQGt+jjVzn9BPluvL3mZU5ihFQIEGjCHUA4+rsXE2PNDKmg1UlfdTn3947aSwWOVnc5UIw==",
|
||||
"requires": {
|
||||
"acorn": "^8.8.0"
|
||||
}
|
||||
@@ -10930,10 +10964,9 @@
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||
},
|
||||
"tsutils": {
|
||||
"version": "3.21.0",
|
||||
@@ -10942,6 +10975,14 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"type-check": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "4.0.1",
|
||||
"version": "4.3.0",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
@@ -72,7 +72,7 @@
|
||||
"build:proto": "npx pb-gen-ts --entry-path=\"src/proto\" --out-dir=\"src/proto/generated\" --ext-in-import=\".js\"",
|
||||
"build:esm": "npx tsc",
|
||||
"build:deno": "npx cpy ./src ./deno && npx cpy ./package.json ./deno && npx replace \".js';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'linkedom';\" \"'https://esm.sh/linkedom';\" ./deno -r && npx replace \"'jintr';\" \"'https://esm.sh/jintr';\" ./deno -r && npx replace \"new Jinter.default\" \"new Jinter\" ./deno -r",
|
||||
"bundle:node": "npx esbuild ./dist/src/platform/node.js --bundle --target=node10 --keep-names --format=cjs --platform=node --outfile=./bundle/node.cjs --external:jintr --external:undici --external:linkedom --sourcemap --banner:js=\"/* eslint-disable */\"",
|
||||
"bundle:node": "npx esbuild ./dist/src/platform/node.js --bundle --target=node10 --keep-names --format=cjs --platform=node --outfile=./bundle/node.cjs --external:jintr --external:undici --external:linkedom --external:tslib --sourcemap --banner:js=\"/* eslint-disable */\"",
|
||||
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser",
|
||||
"bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify",
|
||||
"prepare": "npm run build",
|
||||
@@ -84,8 +84,9 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jintr": "^0.4.1",
|
||||
"jintr": "^1.0.0",
|
||||
"linkedom": "^0.14.12",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,12 +3,12 @@ import EventEmitterLike from '../utils/EventEmitterLike.js';
|
||||
import Actions from './Actions.js';
|
||||
import Player from './Player.js';
|
||||
|
||||
import HTTPClient from '../utils/HTTPClient.js';
|
||||
import { Platform, DeviceCategory, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils.js';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
|
||||
import Proto from '../proto/index.js';
|
||||
import { ICache } from '../types/Cache.js';
|
||||
import { FetchFunction } from '../types/PlatformShim.js';
|
||||
import HTTPClient from '../utils/HTTPClient.js';
|
||||
import { DeviceCategory, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.js';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
|
||||
|
||||
export enum ClientType {
|
||||
WEB = 'WEB',
|
||||
@@ -118,6 +118,11 @@ export interface SessionOptions {
|
||||
* YouTube cookies.
|
||||
*/
|
||||
cookie?: string;
|
||||
/**
|
||||
* Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in.
|
||||
* A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint.
|
||||
*/
|
||||
visitor_data?: string;
|
||||
/**
|
||||
* Fetch function to use.
|
||||
*/
|
||||
@@ -179,6 +184,7 @@ export default class Session extends EventEmitterLike {
|
||||
options.lang,
|
||||
options.location,
|
||||
options.account_index,
|
||||
options.visitor_data,
|
||||
options.enable_safety_mode,
|
||||
options.generate_session_locally,
|
||||
options.device_category,
|
||||
@@ -198,6 +204,7 @@ export default class Session extends EventEmitterLike {
|
||||
lang = '',
|
||||
location = '',
|
||||
account_index = 0,
|
||||
visitor_data = '',
|
||||
enable_safety_mode = false,
|
||||
generate_session_locally = false,
|
||||
device_category: DeviceCategory = 'desktop',
|
||||
@@ -208,9 +215,9 @@ export default class Session extends EventEmitterLike {
|
||||
let session_data: SessionData;
|
||||
|
||||
if (generate_session_locally) {
|
||||
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode });
|
||||
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data });
|
||||
} else {
|
||||
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode }, fetch);
|
||||
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data }, fetch);
|
||||
}
|
||||
|
||||
return { ...session_data, account_index };
|
||||
@@ -223,16 +230,24 @@ export default class Session extends EventEmitterLike {
|
||||
device_category: string;
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
|
||||
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
|
||||
|
||||
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
visitor_id = decoded_visitor_data.id;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'accept-language': options.lang || 'en-US',
|
||||
'user-agent': getRandomUserAgent('desktop'),
|
||||
'accept': '*/*',
|
||||
'referer': 'https://www.youtube.com/sw.js',
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${Constants.CLIENTS.WEB.STATIC_VISITOR_ID};`
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${visitor_id};`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -292,10 +307,15 @@ export default class Session extends EventEmitterLike {
|
||||
time_zone: string;
|
||||
device_category: DeviceCategory;
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
}): SessionData {
|
||||
const id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
visitor_id = decoded_visitor_data.id;
|
||||
}
|
||||
|
||||
const context: Context = {
|
||||
client: {
|
||||
@@ -305,7 +325,7 @@ export default class Session extends EventEmitterLike {
|
||||
screenHeightPoints: 1080,
|
||||
screenPixelDensity: 1,
|
||||
screenWidthPoints: 1920,
|
||||
visitorData: Proto.encodeVisitorData(id, timestamp),
|
||||
visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)),
|
||||
userAgent: getRandomUserAgent('desktop'),
|
||||
clientName: options.client_name,
|
||||
clientVersion: CLIENTS.WEB.VERSION,
|
||||
|
||||
@@ -32,7 +32,7 @@ The parser is responsible for sanitizing and standardizing InnerTube responses w
|
||||
* [`helpers.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/helpers.ts) - Helper functions/classes for the parser.
|
||||
* [`parser.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/parser.ts) - The core of the parser.
|
||||
* [`nodes.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/nodes.ts) - Contains a list of all the InnerTube nodes, which is used to determine the appropriate node for a given renderer. It's important to note that this file is automatically generated and should not be edited manually.
|
||||
* [`misc.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/misc.ts) - Miscellaneous classes for. Also automatically generated.
|
||||
* [`misc.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/misc.ts) - Miscellaneous classes. Also automatically generated.
|
||||
|
||||
### Clients
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class Button extends YTNode {
|
||||
export default class Button extends YTNode {
|
||||
static type = 'Button';
|
||||
|
||||
text?: string;
|
||||
@@ -15,7 +16,7 @@ class Button extends YTNode {
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
if (data.text) {
|
||||
@@ -40,6 +41,4 @@ class Button extends YTNode {
|
||||
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command);
|
||||
}
|
||||
}
|
||||
|
||||
export default Button;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import Menu from './menus/Menu.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class GridVideo extends YTNode {
|
||||
export default class GridVideo extends YTNode {
|
||||
static type = 'GridVideo';
|
||||
|
||||
id: string;
|
||||
@@ -23,10 +23,15 @@ class GridVideo extends YTNode {
|
||||
short_view_count: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
menu: Menu | null;
|
||||
buttons?;
|
||||
upcoming?: Date;
|
||||
upcoming_text?: Text;
|
||||
is_reminder_set?: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
const length_alt = data.thumbnailOverlays.find((overlay: any) => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer;
|
||||
|
||||
this.id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
@@ -39,7 +44,19 @@ class GridVideo extends YTNode {
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
}
|
||||
}
|
||||
|
||||
export default GridVideo;
|
||||
if (Reflect.has(data, 'buttons')) {
|
||||
this.buttons = Parser.parseArray(data.buttons);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'upcomingEventData')) {
|
||||
this.upcoming = new Date(Number(`${data.upcomingEventData.startTime}000`));
|
||||
this.upcoming_text = new Text(data.upcomingEventData.upcomingEventText);
|
||||
this.is_reminder_set = !!data.upcomingEventData?.isReminderSet;
|
||||
}
|
||||
}
|
||||
|
||||
get is_upcoming(): boolean {
|
||||
return Boolean(this.upcoming && this.upcoming > new Date());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,19 @@
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser from '../parser.js';
|
||||
import GuideEntry from './GuideEntry.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class GuideCollapsibleEntry extends YTNode {
|
||||
export default class GuideCollapsibleEntry extends YTNode {
|
||||
static type = 'GuideCollapsibleEntry';
|
||||
|
||||
expander_item: {
|
||||
title: string,
|
||||
icon_type: string
|
||||
};
|
||||
collapser_item: {
|
||||
title: string,
|
||||
icon_type: string
|
||||
};
|
||||
expander_item: GuideEntry | null;
|
||||
collapser_item: GuideEntry | null;
|
||||
expandable_items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.expander_item = {
|
||||
title: new Text(data.expanderItem.guideEntryRenderer.formattedTitle).toString(),
|
||||
icon_type: data.expanderItem.guideEntryRenderer.icon.iconType
|
||||
};
|
||||
|
||||
this.collapser_item = {
|
||||
title: new Text(data.collapserItem.guideEntryRenderer.formattedTitle).toString(),
|
||||
icon_type: data.collapserItem.guideEntryRenderer.icon.iconType
|
||||
};
|
||||
|
||||
this.expander_item = Parser.parseItem(data.expanderItem, GuideEntry);
|
||||
this.collapser_item = Parser.parseItem(data.collapserItem, GuideEntry);
|
||||
this.expandable_items = Parser.parseArray(data.expandableItems);
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideCollapsibleEntry;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser from '../parser.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class GuideCollapsibleSectionEntry extends YTNode {
|
||||
export default class GuideCollapsibleSectionEntry extends YTNode {
|
||||
static type = 'GuideCollapsibleSectionEntry';
|
||||
|
||||
header_entry;
|
||||
@@ -9,15 +10,11 @@ class GuideCollapsibleSectionEntry extends YTNode {
|
||||
collapser_icon: string;
|
||||
section_items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.header_entry = Parser.parseItem(data.headerEntry);
|
||||
this.expander_icon = data.expanderIcon.iconType;
|
||||
this.collapser_icon = data.collapserIcon.iconType;
|
||||
this.section_items = Parser.parseArray(data.sectionItems);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideCollapsibleSectionEntry;
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import GuideEntry from './GuideEntry.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class GuideDownloadsEntry extends GuideEntry {
|
||||
export default class GuideDownloadsEntry extends GuideEntry {
|
||||
static type = 'GuideDownloadsEntry';
|
||||
|
||||
always_show: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super(data.entryRenderer.guideEntryRenderer);
|
||||
this.always_show = !!data.alwaysShow;
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideDownloadsEntry;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
class GuideEntry extends YTNode {
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class GuideEntry extends YTNode {
|
||||
static type = 'GuideEntry';
|
||||
|
||||
title: Text;
|
||||
@@ -13,21 +15,24 @@ class GuideEntry extends YTNode {
|
||||
badges?: any;
|
||||
is_primary: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.formattedTitle);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint);
|
||||
if (data.icon?.iconType) {
|
||||
|
||||
if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType')) {
|
||||
this.icon_type = data.icon.iconType;
|
||||
}
|
||||
if (data.thumbnail) {
|
||||
|
||||
if (Reflect.has(data, 'thumbnail')) {
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
}
|
||||
if (data.badges) {
|
||||
|
||||
// (LuanRT) XXX: Check this property's data and parse it.
|
||||
if (Reflect.has(data, 'badges')) {
|
||||
this.badges = data.badges;
|
||||
}
|
||||
|
||||
this.is_primary = !!data.isPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideEntry;
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser from '../parser.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class GuideSection extends YTNode {
|
||||
export default class GuideSection extends YTNode {
|
||||
static type = 'GuideSection';
|
||||
|
||||
title?: Text;
|
||||
items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
if (data.formattedTitle) {
|
||||
if (Reflect.has(data, 'formattedTitle')) {
|
||||
this.title = new Text(data.formattedTitle);
|
||||
}
|
||||
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideSection;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import GuideSection from './GuideSection.js';
|
||||
|
||||
class GuideSubscriptionsSection extends GuideSection {
|
||||
export default class GuideSubscriptionsSection extends GuideSection {
|
||||
static type = 'GuideSubscriptionsSection';
|
||||
}
|
||||
|
||||
export default GuideSubscriptionsSection;
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
// TODO: this needs a refactor
|
||||
// Seems like a mess to use
|
||||
// TODO: Clean up and refactor this.
|
||||
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import TextRun from './misc/TextRun.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay.js';
|
||||
import MusicResponsiveListItemFlexColumn from './MusicResponsiveListItemFlexColumn.js';
|
||||
import MusicResponsiveListItemFixedColumn from './MusicResponsiveListItemFixedColumn.js';
|
||||
import MusicResponsiveListItemFlexColumn from './MusicResponsiveListItemFlexColumn.js';
|
||||
import MusicThumbnail from './MusicThumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
import { isTextRun, timeToSeconds } from '../../utils/Utils.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class MusicResponsiveListItem extends YTNode {
|
||||
export default class MusicResponsiveListItem extends YTNode {
|
||||
static type = 'MusicResponsiveListItem';
|
||||
|
||||
#flex_columns;
|
||||
#fixed_columns;
|
||||
flex_columns: ObservedArray<MusicResponsiveListItemFlexColumn>;
|
||||
fixed_columns: ObservedArray<MusicResponsiveListItemFixedColumn>;
|
||||
#playlist_item_data;
|
||||
|
||||
endpoint;
|
||||
item_type;
|
||||
index;
|
||||
thumbnails;
|
||||
endpoint: NavigationEndpoint | null;
|
||||
item_type: 'album' | 'playlist' | 'artist' | 'library_artist' | 'video' | 'song' | 'endpoint' | 'unknown' | undefined;
|
||||
index?: Text;
|
||||
thumbnail?: MusicThumbnail | null;
|
||||
badges;
|
||||
menu;
|
||||
overlay;
|
||||
menu?: Menu | null;
|
||||
overlay?: MusicItemThumbnailOverlay | null;
|
||||
|
||||
id?: string;
|
||||
title?: string;
|
||||
@@ -59,19 +59,20 @@ class MusicResponsiveListItem extends YTNode {
|
||||
subtitle?: Text;
|
||||
subscribers?: string;
|
||||
song_count?: string;
|
||||
|
||||
// TODO: these might be replaceable with Author class
|
||||
author?: {
|
||||
name: string,
|
||||
channel_id?: string
|
||||
endpoint?: NavigationEndpoint
|
||||
};
|
||||
item_count?: string | undefined;
|
||||
item_count?: string;
|
||||
year?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.#flex_columns = Parser.parseArray<MusicResponsiveListItemFlexColumn>(data.flexColumns, MusicResponsiveListItemFlexColumn);
|
||||
this.#fixed_columns = Parser.parseArray<MusicResponsiveListItemFixedColumn>(data.fixedColumns, MusicResponsiveListItemFixedColumn);
|
||||
this.flex_columns = Parser.parseArray(data.flexColumns, MusicResponsiveListItemFlexColumn);
|
||||
this.fixed_columns = Parser.parseArray(data.fixedColumns, MusicResponsiveListItemFixedColumn);
|
||||
|
||||
this.#playlist_item_data = {
|
||||
video_id: data?.playlistItemData?.videoId || null,
|
||||
@@ -101,7 +102,7 @@ class MusicResponsiveListItem extends YTNode {
|
||||
this.#parseLibraryArtist();
|
||||
break;
|
||||
default:
|
||||
if (this.#flex_columns[1]) {
|
||||
if (this.flex_columns[1]) {
|
||||
this.#parseVideoOrSong();
|
||||
} else {
|
||||
this.#parseOther();
|
||||
@@ -113,14 +114,14 @@ class MusicResponsiveListItem extends YTNode {
|
||||
this.index = new Text(data.index);
|
||||
}
|
||||
|
||||
this.thumbnails = data.thumbnail ? Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer?.thumbnail) : [];
|
||||
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
|
||||
this.badges = Parser.parseArray(data.badges);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
|
||||
this.overlay = Parser.parseItem<MusicItemThumbnailOverlay>(data.overlay, MusicItemThumbnailOverlay);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
this.overlay = Parser.parseItem(data.overlay, MusicItemThumbnailOverlay);
|
||||
}
|
||||
|
||||
#parseOther() {
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.title = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
|
||||
if (this.endpoint) {
|
||||
this.item_type = 'endpoint';
|
||||
@@ -130,7 +131,7 @@ class MusicResponsiveListItem extends YTNode {
|
||||
}
|
||||
|
||||
#parseVideoOrSong() {
|
||||
const is_video = this.#flex_columns[1].key('title').instanceof(Text).runs?.some((run) => run.text.match(/(.*?) views/));
|
||||
const is_video = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.some((run) => run.text.match(/(.*?) views/));
|
||||
if (is_video) {
|
||||
this.item_type = 'video';
|
||||
this.#parseVideo();
|
||||
@@ -142,105 +143,144 @@ class MusicResponsiveListItem extends YTNode {
|
||||
|
||||
#parseSong() {
|
||||
this.id = this.#playlist_item_data.video_id || this.endpoint?.payload?.videoId;
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.title = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
|
||||
const duration_text =
|
||||
this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text ||
|
||||
this.#fixed_columns?.[0]?.key('title').instanceof(Text)?.toString();
|
||||
const duration_text = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.key('title').instanceof(Text)?.toString();
|
||||
|
||||
duration_text && (this.duration = {
|
||||
text: duration_text,
|
||||
seconds: timeToSeconds(duration_text)
|
||||
});
|
||||
|
||||
const album = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('MPR')) as TextRun ||
|
||||
this.#flex_columns[2]?.key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('MPR')) as TextRun;
|
||||
if (album) {
|
||||
this.album = {
|
||||
id: album.endpoint?.payload?.browseId,
|
||||
name: album.text,
|
||||
endpoint: album.endpoint
|
||||
if (duration_text) {
|
||||
this.duration = {
|
||||
text: duration_text,
|
||||
seconds: timeToSeconds(duration_text)
|
||||
};
|
||||
}
|
||||
|
||||
const artists = this.#flex_columns[1].key('title').instanceof(Text).runs?.filter((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun[];
|
||||
if (artists) {
|
||||
this.artists = artists.map((artist) => ({
|
||||
name: artist.text,
|
||||
channel_id: artist.endpoint?.payload?.browseId,
|
||||
endpoint: artist.endpoint
|
||||
const album_run =
|
||||
this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('MPR')
|
||||
) ||
|
||||
this.flex_columns.at(2)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('MPR')
|
||||
);
|
||||
|
||||
if (album_run && isTextRun(album_run)) {
|
||||
this.album = {
|
||||
id: album_run.endpoint?.payload?.browseId,
|
||||
name: album_run.text,
|
||||
endpoint: album_run.endpoint
|
||||
};
|
||||
}
|
||||
|
||||
const artist_runs = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.filter(
|
||||
(run) => (isTextRun(run) && run.endpoint) && run.endpoint.payload.browseId.startsWith('UC')
|
||||
);
|
||||
|
||||
if (artist_runs) {
|
||||
this.artists = artist_runs.map((run) => ({
|
||||
name: run.text,
|
||||
channel_id: isTextRun(run) ? run.endpoint?.payload?.browseId : undefined,
|
||||
endpoint: isTextRun(run) ? run.endpoint : undefined
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#parseVideo() {
|
||||
this.id = this.#playlist_item_data.video_id;
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.views = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => run.text.match(/(.*?) views/))?.text;
|
||||
this.title = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
this.views = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find((run) => run.text.match(/(.*?) views/))?.toString();
|
||||
|
||||
const authors = this.#flex_columns[1].key('title').instanceof(Text).runs?.filter((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun[];
|
||||
if (authors) {
|
||||
this.authors = authors.map((author) => ({
|
||||
name: author.text,
|
||||
channel_id: author.endpoint?.payload?.browseId,
|
||||
endpoint: author.endpoint
|
||||
}));
|
||||
const author_runs = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.filter(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('UC')
|
||||
);
|
||||
|
||||
if (author_runs) {
|
||||
this.authors = author_runs.map((run) => {
|
||||
return {
|
||||
name: run.text,
|
||||
channel_id: isTextRun(run) ? run.endpoint?.payload?.browseId : undefined,
|
||||
endpoint: isTextRun(run) ? run.endpoint : undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const duration_text = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text ||
|
||||
this.#fixed_columns[0]?.key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text;
|
||||
duration_text && (this.duration = {
|
||||
text: duration_text,
|
||||
seconds: timeToSeconds(duration_text)
|
||||
});
|
||||
const duration_text = this.flex_columns[1].key('title').instanceof(Text).runs?.find(
|
||||
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text;
|
||||
|
||||
if (duration_text) {
|
||||
this.duration = {
|
||||
text: duration_text,
|
||||
seconds: timeToSeconds(duration_text)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#parseArtist() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.name = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.subtitle = this.#flex_columns[1].key('title').instanceof(Text);
|
||||
this.subscribers = this.subtitle.runs?.find((run) => (/^(\d*\.)?\d+[M|K]? subscribers?$/i).test(run.text))?.text || '';
|
||||
this.name = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
this.subtitle = this.flex_columns.at(1)?.key('title').instanceof(Text);
|
||||
this.subscribers = this.subtitle?.runs?.find((run) => (/^(\d*\.)?\d+[M|K]? subscribers?$/i).test(run.text))?.text || '';
|
||||
}
|
||||
|
||||
#parseLibraryArtist() {
|
||||
this.name = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.subtitle = this.#flex_columns[1].key('title').instanceof(Text);
|
||||
this.name = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
this.subtitle = this.flex_columns.at(1)?.key('title').instanceof(Text);
|
||||
this.song_count = this.subtitle?.runs?.find((run) => (/^\d+(,\d+)? songs?$/i).test(run.text))?.text || '';
|
||||
}
|
||||
|
||||
#parseAlbum() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
|
||||
const author = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun;
|
||||
author && (this.author = {
|
||||
name: author.text,
|
||||
channel_id: author.endpoint?.payload?.browseId,
|
||||
endpoint: author.endpoint
|
||||
});
|
||||
const author_run = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('UC')
|
||||
);
|
||||
|
||||
this.year = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^[12][0-9]{3}$/).test(run.text))?.text;
|
||||
if (author_run && isTextRun(author_run)) {
|
||||
this.author = {
|
||||
name: author_run.text,
|
||||
channel_id: author_run.endpoint?.payload?.browseId,
|
||||
endpoint: author_run.endpoint
|
||||
};
|
||||
}
|
||||
|
||||
this.year = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) => (/^[12][0-9]{3}$/).test(run.text)
|
||||
)?.text;
|
||||
}
|
||||
|
||||
#parsePlaylist() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
|
||||
const item_count_run = this.#flex_columns[1].key('title')
|
||||
const item_count_run = this.flex_columns.at(1)?.key('title')
|
||||
.instanceof(Text).runs?.find((run) => run.text.match(/\d+ (song|songs)/));
|
||||
|
||||
this.item_count = item_count_run ? item_count_run.text : undefined;
|
||||
|
||||
const author = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun;
|
||||
const author_run = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('UC')
|
||||
);
|
||||
|
||||
if (author) {
|
||||
if (author_run && isTextRun(author_run)) {
|
||||
this.author = {
|
||||
name: author.text,
|
||||
channel_id: author.endpoint?.payload?.browseId,
|
||||
endpoint: author.endpoint
|
||||
name: author_run.text,
|
||||
channel_id: author_run.endpoint?.payload?.browseId,
|
||||
endpoint: author_run.endpoint
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicResponsiveListItem;
|
||||
get thumbnails() {
|
||||
return this.thumbnail?.contents || [];
|
||||
}
|
||||
}
|
||||
26
src/parser/classes/MusicTastebuilderShelf.ts
Normal file
26
src/parser/classes/MusicTastebuilderShelf.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Parser from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import Text from './misc/Text.js';
|
||||
import MusicTastebuilderShelfThumbnail from './MusicTastebuilderShelfThumbnail.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class MusicTasteBuilderShelf extends YTNode {
|
||||
static type = 'MusicTasteBuilderShelf';
|
||||
|
||||
thumbnail: MusicTastebuilderShelfThumbnail | null;
|
||||
primary_text: Text;
|
||||
secondary_text: Text;
|
||||
action_button: Button | null;
|
||||
is_visible: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnail = Parser.parseItem(data.thumbnail, MusicTastebuilderShelfThumbnail);
|
||||
this.primary_text = new Text(data.primaryText);
|
||||
this.secondary_text = new Text(data.secondaryText);
|
||||
this.action_button = Parser.parseItem(data.actionButton, Button);
|
||||
this.is_visible = data.isVisible;
|
||||
}
|
||||
}
|
||||
14
src/parser/classes/MusicTastebuilderShelfThumbnail.ts
Normal file
14
src/parser/classes/MusicTastebuilderShelfThumbnail.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Thumbnail } from '../misc.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class MusicTastebuilderShelfThumbnail extends YTNode {
|
||||
static type = 'MusicTastebuilderShelfThumbnail';
|
||||
|
||||
thumbnail: Thumbnail[];
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { RawNode } from '../index.js';
|
||||
|
||||
class PlayerCaptionsTracklist extends YTNode {
|
||||
static type = 'PlayerCaptionsTracklist';
|
||||
|
||||
caption_tracks: {
|
||||
caption_tracks?: {
|
||||
base_url: string;
|
||||
name: Text;
|
||||
vss_id: string;
|
||||
@@ -13,7 +14,7 @@ class PlayerCaptionsTracklist extends YTNode {
|
||||
is_translatable: boolean;
|
||||
}[];
|
||||
|
||||
audio_tracks: {
|
||||
audio_tracks?: {
|
||||
audio_track_id: string;
|
||||
captions_initial_state: string;
|
||||
default_caption_track_index: number;
|
||||
@@ -22,39 +23,48 @@ class PlayerCaptionsTracklist extends YTNode {
|
||||
caption_track_indices: number;
|
||||
}[];
|
||||
|
||||
default_audio_track_index: number;
|
||||
default_audio_track_index?: number;
|
||||
|
||||
translation_languages: {
|
||||
translation_languages?: {
|
||||
language_code: string;
|
||||
language_name: Text;
|
||||
}[];
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.caption_tracks = data.captionTracks.map((ct: any) => ({
|
||||
base_url: ct.baseUrl,
|
||||
name: new Text(ct.name),
|
||||
vss_id: ct.vssId,
|
||||
language_code: ct.languageCode,
|
||||
kind: ct.kind,
|
||||
is_translatable: ct.isTranslatable
|
||||
}));
|
||||
|
||||
this.audio_tracks = data.audioTracks.map((at: any) => ({
|
||||
audio_track_id: at.audioTrackId,
|
||||
captions_initial_state: at.captionsInitialState,
|
||||
default_caption_track_index: at.defaultCaptionTrackIndex,
|
||||
has_default_track: at.hasDefaultTrack,
|
||||
visibility: at.visibility,
|
||||
caption_track_indices: at.captionTrackIndices
|
||||
}));
|
||||
if (Reflect.has(data, 'captionTracks')) {
|
||||
this.caption_tracks = data.captionTracks.map((ct: any) => ({
|
||||
base_url: ct.baseUrl,
|
||||
name: new Text(ct.name),
|
||||
vss_id: ct.vssId,
|
||||
language_code: ct.languageCode,
|
||||
kind: ct.kind,
|
||||
is_translatable: ct.isTranslatable
|
||||
}));
|
||||
}
|
||||
|
||||
this.default_audio_track_index = data.defaultAudioTrackIndex;
|
||||
if (Reflect.has(data, 'audioTracks')) {
|
||||
this.audio_tracks = data.audioTracks.map((at: any) => ({
|
||||
audio_track_id: at.audioTrackId,
|
||||
captions_initial_state: at.captionsInitialState,
|
||||
default_caption_track_index: at.defaultCaptionTrackIndex,
|
||||
has_default_track: at.hasDefaultTrack,
|
||||
visibility: at.visibility,
|
||||
caption_track_indices: at.captionTrackIndices
|
||||
}));
|
||||
}
|
||||
|
||||
this.translation_languages = data.translationLanguages.map((tl: any) => ({
|
||||
language_code: tl.languageCode,
|
||||
language_name: new Text(tl.languageName)
|
||||
}));
|
||||
if (Reflect.has(data, 'defaultAudioTrackIndex')) {
|
||||
this.default_audio_track_index = data.defaultAudioTrackIndex;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'translationLanguages')) {
|
||||
this.translation_languages = data.translationLanguages.map((tl: any) => ({
|
||||
language_code: tl.languageCode,
|
||||
language_name: new Text(tl.languageName)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ class PlaylistVideo extends YTNode {
|
||||
is_playable: boolean;
|
||||
menu: Menu | null;
|
||||
upcoming;
|
||||
video_info: Text;
|
||||
accessibility_label?: string;
|
||||
|
||||
duration: {
|
||||
text: string;
|
||||
@@ -40,6 +42,8 @@ class PlaylistVideo extends YTNode {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.is_playable = data.isPlayable;
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
this.video_info = new Text(data.videoInfo);
|
||||
this.accessibility_label = data.title.accessibility.accessibilityData.label;
|
||||
|
||||
const upcoming = data.upcomingEventData && Number(`${data.upcomingEventData.startTime}000`);
|
||||
if (upcoming) {
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ShowingResultsFor extends YTNode {
|
||||
export default class ShowingResultsFor extends YTNode {
|
||||
static type = 'ShowingResultsFor';
|
||||
|
||||
corrected_query: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
original_query: Text;
|
||||
corrected_query_endpoint: NavigationEndpoint;
|
||||
original_query_endpoint: NavigationEndpoint;
|
||||
search_instead_for: Text;
|
||||
showing_results_for: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.corrected_query = new Text(data.correctedQuery);
|
||||
this.endpoint = new NavigationEndpoint(data.correctedQueryEndpoint);
|
||||
this.original_query = new Text(data.originalQuery);
|
||||
this.corrected_query_endpoint = new NavigationEndpoint(data.correctedQueryEndpoint);
|
||||
this.original_query_endpoint = new NavigationEndpoint(data.originalQueryEndpoint);
|
||||
this.search_instead_for = new Text(data.searchInsteadFor);
|
||||
this.showing_results_for = new Text(data.showingResultsFor);
|
||||
}
|
||||
}
|
||||
|
||||
export default ShowingResultsFor;
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class ToggleMenuServiceItem extends YTNode {
|
||||
export default class ToggleMenuServiceItem extends YTNode {
|
||||
static type = 'ToggleMenuServiceItem';
|
||||
|
||||
text: Text;
|
||||
toggled_text: Text;
|
||||
icon_type: string;
|
||||
toggled_icon_type: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
default_endpoint: NavigationEndpoint;
|
||||
toggled_endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.text = new Text(data.defaultText);
|
||||
this.toggled_text = new Text(data.toggledText);
|
||||
this.icon_type = data.defaultIcon.iconType;
|
||||
this.toggled_icon_type = data.toggledIcon.iconType;
|
||||
this.endpoint = new NavigationEndpoint(data.toggledServiceEndpoint);
|
||||
this.default_endpoint = new NavigationEndpoint(data.defaultServiceEndpoint);
|
||||
this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToggleMenuServiceItem;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import MetadataBadge from './MetadataBadge.js';
|
||||
import ExpandableMetadata from './ExpandableMetadata.js';
|
||||
import ThumbnailOverlayTimeStatus from './ThumbnailOverlayTimeStatus.js';
|
||||
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
@@ -98,7 +99,7 @@ class Video extends YTNode {
|
||||
return this.badges.some((badge) => {
|
||||
if (badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW' || badge.label === 'LIVE')
|
||||
return true;
|
||||
});
|
||||
}) || this.thumbnail_overlays.firstOfType(ThumbnailOverlayTimeStatus)?.style === 'LIVE';
|
||||
}
|
||||
|
||||
get is_upcoming(): boolean | undefined {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Button from '../Button.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
|
||||
class MenuNavigationItem extends Button {
|
||||
static type = 'MenuNavigationItem';
|
||||
|
||||
|
||||
@@ -219,6 +219,8 @@ export { default as MusicResponsiveListItemFlexColumn } from './classes/MusicRes
|
||||
export { default as MusicShelf } from './classes/MusicShelf.js';
|
||||
export { default as MusicSideAlignedItem } from './classes/MusicSideAlignedItem.js';
|
||||
export { default as MusicSortFilterButton } from './classes/MusicSortFilterButton.js';
|
||||
export { default as MusicTastebuilderShelf } from './classes/MusicTastebuilderShelf.js';
|
||||
export { default as MusicTastebuilderShelfThumbnail } from './classes/MusicTastebuilderShelfThumbnail.js';
|
||||
export { default as MusicThumbnail } from './classes/MusicThumbnail.js';
|
||||
export { default as MusicTwoRowItem } from './classes/MusicTwoRowItem.js';
|
||||
export { default as MusicVisualHeader } from './classes/MusicVisualHeader.js';
|
||||
|
||||
@@ -497,6 +497,7 @@ export default class Parser {
|
||||
'DisplayAd',
|
||||
'SearchPyv',
|
||||
'MealbarPromo',
|
||||
'PrimetimePromo',
|
||||
'BackgroundPromo',
|
||||
'PromotedSparklesWeb',
|
||||
'RunAttestationCommand',
|
||||
|
||||
@@ -28,8 +28,8 @@ class Library extends Feed<IBrowseResponse> {
|
||||
if (!this.page.contents_memo)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
const stats = this.page.contents_memo.getType(ProfileColumnStats)?.[0];
|
||||
const user_info = this.page.contents_memo.getType(ProfileColumnUserInfo)?.[0];
|
||||
const stats = this.page.contents_memo.getType(ProfileColumnStats).first();
|
||||
const user_info = this.page.contents_memo.getType(ProfileColumnUserInfo).first();
|
||||
|
||||
this.profile = { stats, user_info };
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class Search extends Feed<ISearchResponse> {
|
||||
if (!contents)
|
||||
throw new InnertubeError('No contents found in search response');
|
||||
|
||||
this.results = contents.firstOfType(ItemSection)?.contents;
|
||||
this.results = contents.filterType(ItemSection).find((section) => section.contents && section.contents.length > 0)?.contents;
|
||||
|
||||
this.refinements = this.page.refinements || [];
|
||||
this.estimated_results = this.page.estimated_results;
|
||||
|
||||
@@ -13,7 +13,7 @@ class TimeWatched {
|
||||
contents?: ObservedArray<ItemSection>;
|
||||
|
||||
constructor(response: ApiResponse) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
|
||||
if (!this.#page.contents)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import Parser, { SectionListContinuation } from '../index.js';
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.js';
|
||||
import Parser, { SectionListContinuation } from '../index.js';
|
||||
import MusicTastebuilderShelf from '../classes/MusicTastebuilderShelf.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
class HomeFeed {
|
||||
#page: IBrowseResponse;
|
||||
#actions: Actions;
|
||||
#continuation?: string;
|
||||
|
||||
sections?: ObservedArray<MusicCarouselShelf>;
|
||||
sections?: ObservedArray<MusicCarouselShelf | MusicTastebuilderShelf>;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#actions = actions;
|
||||
@@ -36,7 +37,7 @@ class HomeFeed {
|
||||
}
|
||||
|
||||
this.#continuation = tab.content?.as(SectionList).continuation;
|
||||
this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf);
|
||||
this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf, MusicTastebuilderShelf);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Jinter from 'jintr';
|
||||
import { Jinter } from 'jintr';
|
||||
import { VMPrimative } from '../../types/PlatformShim.js';
|
||||
|
||||
export default function evaluate(code: string, env: Record<string, VMPrimative>) {
|
||||
const runtime = new Jinter.default(code);
|
||||
const runtime = new Jinter(code);
|
||||
for (const [ key, value ] of Object.entries(env)) {
|
||||
runtime.scope.set(key, value);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const is_cjs = !meta_url;
|
||||
const __dirname__ = is_cjs ? __dirname : path.dirname(fileURLToPath(meta_url));
|
||||
|
||||
const package_json = JSON.parse(readFileSync(path.resolve(__dirname__, is_cjs ? '../package.json' : '../../package.json'), 'utf-8'));
|
||||
const repo_url = package_json.homepage?.split('#')[0];
|
||||
|
||||
class Cache implements ICache {
|
||||
#persistent_directory: string;
|
||||
@@ -102,8 +103,8 @@ Platform.load({
|
||||
runtime: 'node',
|
||||
info: {
|
||||
version: package_json.version,
|
||||
bugs_url: package_json.bugs.url,
|
||||
repo_url: package_json.homepage.split('#')[0]
|
||||
bugs_url: package_json.bugs?.url || `${repo_url}/issues`,
|
||||
repo_url
|
||||
},
|
||||
server: true,
|
||||
Cache: Cache,
|
||||
@@ -130,4 +131,4 @@ Platform.load({
|
||||
|
||||
export * from './lib.js';
|
||||
import Innertube from './lib.js';
|
||||
export default Innertube;
|
||||
export default Innertube;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CLIENTS } from '../utils/Constants.js';
|
||||
import { u8ToBase64 } from '../utils/Utils.js';
|
||||
import { base64ToU8, u8ToBase64 } from '../utils/Utils.js';
|
||||
import { VideoMetadata } from '../core/Studio.js';
|
||||
|
||||
import * as VisitorData from './generated/messages/youtube/VisitorData.js';
|
||||
@@ -21,6 +21,11 @@ class Proto {
|
||||
return encodeURIComponent(u8ToBase64(buf).replace(/\+/g, '-').replace(/\//g, '_'));
|
||||
}
|
||||
|
||||
static decodeVisitorData(visitor_data: string): VisitorData.Type {
|
||||
const data = VisitorData.decodeBinary(base64ToU8(decodeURIComponent(visitor_data)));
|
||||
return data;
|
||||
}
|
||||
|
||||
static encodeChannelAnalyticsParams(channel_id: string): string {
|
||||
const buf = ChannelAnalytics.encodeBinary({
|
||||
params: {
|
||||
|
||||
@@ -428,8 +428,15 @@ class FormatUtils {
|
||||
const url = new URL(format.decipher(player));
|
||||
url.searchParams.set('cpn', cpn || '');
|
||||
|
||||
let id;
|
||||
if (format.audio_track) {
|
||||
id = `${format.itag?.toString()}-${format.audio_track.id}`;
|
||||
} else {
|
||||
id = format.itag?.toString();
|
||||
}
|
||||
|
||||
const representation = this.#el(document, 'Representation', {
|
||||
id: format.itag?.toString(),
|
||||
id,
|
||||
codecs,
|
||||
bandwidth: format.bitrate?.toString(),
|
||||
audioSamplingRate: format.audio_sample_rate?.toString()
|
||||
|
||||
@@ -169,6 +169,7 @@ export default class HTTPClient {
|
||||
ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION;
|
||||
break;
|
||||
case 'TV_EMBEDDED':
|
||||
ctx.client.clientName = Constants.CLIENTS.TV_EMBEDDED.NAME;
|
||||
ctx.client.clientVersion = Constants.CLIENTS.TV_EMBEDDED.VERSION;
|
||||
ctx.client.clientScreen = 'EMBED';
|
||||
ctx.thirdParty = { embedUrl: Constants.URLS.YT_BASE };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Memo } from '../parser/helpers.js';
|
||||
import { EmojiRun, TextRun } from '../parser/misc.js';
|
||||
import PlatformShim, { FetchFunction } from '../types/PlatformShim.js';
|
||||
import userAgents from './user-agents.js';
|
||||
|
||||
@@ -82,7 +83,7 @@ export function getRandomUserAgent(type: DeviceCategory): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an authentication token from a cookies' sid..js
|
||||
* Generates an authentication token from a cookies' sid.
|
||||
* @param sid - Sid extracted from cookies
|
||||
*/
|
||||
export async function generateSidAuth(sid: string): Promise<string> {
|
||||
@@ -116,7 +117,7 @@ export function generateRandomString(length: number): string {
|
||||
* @returns seconds
|
||||
*/
|
||||
export function timeToSeconds(time: string): number {
|
||||
const params = time.split(':').map((param) => parseInt(param));
|
||||
const params = time.split(':').map((param) => parseInt(param.replace(/\D/g, '')));
|
||||
switch (params.length) {
|
||||
case 1:
|
||||
return params[0];
|
||||
@@ -217,4 +218,12 @@ export const debugFetch: FetchFunction = (input, init) => {
|
||||
|
||||
export function u8ToBase64(u8: Uint8Array): string {
|
||||
return btoa(String.fromCharCode.apply(null, Array.from(u8)));
|
||||
}
|
||||
|
||||
export function base64ToU8(base64: string): Uint8Array {
|
||||
return new Uint8Array(atob(base64).split('').map((char) => char.charCodeAt(0)));
|
||||
}
|
||||
|
||||
export function isTextRun(run: TextRun | EmojiRun): run is TextRun {
|
||||
return !('emoji' in run);
|
||||
}
|
||||
@@ -233,7 +233,7 @@ describe('YouTube.js Tests', () => {
|
||||
it('should retrieve the "Related" tab', async () => {
|
||||
const info = await yt.music.getInfo(VIDEOS[1].ID);
|
||||
const related = await info.getRelated();
|
||||
expect((related as any).length).toBeGreaterThan(3);
|
||||
expect((related as any).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should retrieve albums', async () => {
|
||||
@@ -278,9 +278,6 @@ describe('YouTube.js Tests', () => {
|
||||
});
|
||||
|
||||
async function download(id: string, yt: Innertube): Promise<boolean> {
|
||||
// TODO: add back info
|
||||
// let got_video_info = false;
|
||||
|
||||
const stream = await yt.download(id, { type: 'video+audio' });
|
||||
const file = fs.createWriteStream(`./${id}.mp4`);
|
||||
|
||||
@@ -288,5 +285,5 @@ async function download(id: string, yt: Innertube): Promise<boolean> {
|
||||
file.write(chunk);
|
||||
}
|
||||
|
||||
return fs.existsSync(`./${id}.mp4`); // && got_video_info;
|
||||
return fs.existsSync(`./${id}.mp4`);
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
"importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
@@ -102,9 +102,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.js",
|
||||
"index.ts",
|
||||
"browser.ts"
|
||||
"src/**/*.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
Reference in New Issue
Block a user