Compare commits

...

80 Commits

Author SHA1 Message Date
github-actions[bot]
e54c0c4bf1 chore(main): release 6.4.1 (#507)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-10-02 00:04:10 -03:00
LuanRT
8e372d5c67 fix(Feed): Do not throw when multiple continuations are present 2023-10-01 23:39:54 -03:00
LuanRT
987f50604a fix(Playlist): Throw a more helpful error when parsing empty responses 2023-10-01 23:31:05 -03:00
Luan
69702085c6 refactor: Move transcript logic to MediaInfo (#511)
* refactor: Move transcript logic to `MediaInfo`

+ Add support for retrieving different languages.

* docs: Update and add examples
2023-09-17 22:17:14 -03:00
absidue
d2959b3a55 perf: Cache deciphered n-params by info response (#505) 2023-09-17 18:52:32 -03:00
absidue
68df321858 perf(generator): Remove duplicate checks in isMiscType (#506) 2023-09-15 15:25:08 -03:00
Ryan Sandbach
f4bc8508d0 chore(docs): Minor update (#502)
* Updated documentation example to matche syntax of existing parsers.

* Changed from require since package is setup as module.
2023-09-12 03:49:36 -03:00
LuanRT
e216124bb0 chore: update docs 2023-09-10 02:14:14 -03:00
github-actions[bot]
6d98abbd53 chore(main): release 6.4.0 (#499)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-09-10 02:08:24 -03:00
LuanRT
fba3fc9714 fix(BackstagePost): vote_button type mismatch 2023-09-10 02:06:01 -03:00
Luan
f94ea6cf91 feat: Add support for retrieving transcripts (#500)
* feat: Add support for retrieving transcripts

* chore: lint

* chore: update docs

* chore: Do not include nodes in errors thrown

* chore: Improve error messages

* fix(ExpandableMetadata): `expanded_content` type mismatch

* chore: lint
2023-09-10 01:50:30 -03:00
Jeremy Banks
86fb33ed03 feat(PlaylistManager): add .setName() and .setDescription() functions for editing playlists (#498) 2023-09-01 17:40:25 -03:00
github-actions[bot]
bff4210349 chore(main): release 6.3.0 (#495)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-31 20:41:06 -03:00
absidue
91de6e5c0e feat(ChannelMetadata): Add music_artist_name (#497) 2023-08-31 20:29:58 -03:00
absidue
c26972c42a fix(CompactMovie): Add missing import and remove unnecessary console.log (#496) 2023-08-31 20:28:25 -03:00
Jeremy Banks
8bc2aaa358 feat(Session): Add on_behalf_of_user session option. (#494)
This specifies which channel to use if multiple are associated with the logged-in account.
2023-08-31 08:20:37 -03:00
github-actions[bot]
2e5f076fd7 chore(main): release 6.2.0 (#491)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-29 14:25:32 -03:00
absidue
0412fa05ff fix(Format): Fix is_original always being true (#492) 2023-08-29 14:23:08 -03:00
Luan
10c15bfb9f feat(Session): Add fallback for session data retrieval (#490)
* feat(Session): Add fallback for session data retrieval

Should have added this when we first implemented session data retrieval to be honest. It makes a request to YouTube's service worker and the data there can change or the request can just fail.

* chore: lint
2023-08-28 07:18:27 -03:00
github-actions[bot]
4862c35cee chore(main): release 6.1.0 (#489)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-27 16:43:10 -03:00
absidue
2eed1726d5 feat(parser): Add CompactMovie (#487) 2023-08-27 16:29:50 -03:00
absidue
8b69587787 feat(parser): Add AlertWithButton (#486) 2023-08-27 16:29:23 -03:00
absidue
ed7be2a675 feat(parser): Add ChannelHeaderLinksView (#484) 2023-08-27 16:28:49 -03:00
github-actions[bot]
361fb4a9f1 chore(main): release 6.0.2 (#481)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-24 17:06:07 -03:00
Daniel Wykerd
1c3ea2acd3 fix: invalid set ids in dash manifest (#480) 2023-08-24 15:59:10 -03:00
github-actions[bot]
859c4585d9 chore(main): release 6.0.1 (#476)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-22 09:07:49 -03:00
LuanRT
751f2b90fd Merge branch 'main' of https://github.com/LuanRT/YouTube.js 2023-08-22 09:06:23 -03:00
LuanRT
90be877d28 fix(SearchSubMenu): Groups not being parsed due to a typo 2023-08-22 09:06:06 -03:00
github-actions[bot]
052632314b chore(main): release 6.0.0 (#461)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-18 08:39:05 -03:00
LuanRT
22a38c0762 feat(Session): Add IOS to ClientType enum 2023-08-18 08:36:17 -03:00
LuanRT
f7614634b6 chore: lint 2023-08-18 08:26:39 -03:00
LuanRT
bf1510b235 fix(parser): Logger logging classdata as [Object object] 2023-08-18 08:17:23 -03:00
LuanRT
815e54b854 feat(MusicResponsiveListItem): Detect non music tracks properly
This should make it easier to identify podcast episodes.
2023-08-18 08:15:04 -03:00
LuanRT
f7666051f6 chore(parser): update MusicCarouselShelf 2023-08-18 08:11:04 -03:00
LuanRT
494ee8776a feat(parser): add MusicMultiRowListItem
Used to display podcasts.
2023-08-18 08:09:04 -03:00
Daniel Wykerd
87ed3960ff refactor!: replace unnecessary classes with pure functions (#468)
* deps: update linkedom

* refactor!: remove YTNodeGenerator in favour of namespaced pure functions

BREAKING CHANGES:
- Removes `YTNodeGenerator` from `import('youtubei.js').Generator` and exposes its functions directly in `import('youtubei.js').Generator`

* refactor!: replace Parser class with pure functions

- Remove Parser class in favour of pure functions
- Merge duplicate classes `AppendContinuationItemsAction` into a single class
- Move continuation parsers into a seperate file
- Add better custom logging support to parser methods as per issue #460

* refactor!: replace Proto class with pure functions

* chore: update package-lock.json

* refactor!: replace FormatUtils with pure functions and JSX components

- Replace linkedom DASH manifest generation with a dependency free JSX implementation
- Remove FormatUtils class in favour of pure functions
- Remove DOMParser requirement
- Remove duplicate types

* refactor: implement changes from #462

* chore: lint

* fix: deno support

* fix: render valid xml document

* fix: wrong function call in DashUtils

* fix: typo in parser

Co-authored-by: LuanRT <luan.lrt4@gmail.com>

* refactor!: move streaming info logic into seperate function

This allows users to access the same data available in the dash manifest while also simplifying the manifest generation

* chore: lint

* refactor: readability improvements & fixes

Remove redundant getAudioTrackGroups
General readability improvements in StreamingInfo.ts
Share response object between `getBitrate` and `getMimeType` as to not make duplicate requests

* build: remove unnecessary step in deno build

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

* refactor: move types to `types` directory

* docs: add back comments lost during refactor

* chore: lint

---------

Co-authored-by: LuanRT <luan.lrt4@gmail.com>
Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
2023-08-18 06:49:58 -03:00
LuanRT
eb3cca1e2e chore(example/browser): fix ALR requests failing 2023-08-18 06:33:01 -03:00
LuanRT
9971ffe021 chore: update package.json [skip ci] 2023-08-12 23:52:16 -03:00
LuanRT
7949b3df66 chore(COLLABORATORS.md): add Absidue 2023-08-12 23:50:27 -03:00
LuanRT
aa385142e4 chore: update contact email [skip ci] 2023-08-12 23:45:28 -03:00
LuanRT
6c8a916f0f chore: migrate browser example to Shaka player [skip ci] (#471)
* chore: use Shaka for the browser example

* chore: lint

* fix(HashtagFeed): resolve type casting issue so tests pass
2023-08-12 23:21:20 -03:00
absidue
31d27b1bca fix(Format): Extracting audio language from captions (#470) 2023-08-12 16:01:32 -03:00
LuanRT
cb37c6a17b chore: use ESM for dev scripts [skip ci]
Just to keep things consistent.
2023-08-11 19:29:51 -03:00
absidue
1ff3e1a440 fix(toDash): Format grouping into AdaptationSets (#462) 2023-08-09 16:07:03 -03:00
Ronnie Vega
46fe18b763 feat(VideoInfo): support iOS client (#467) 2023-08-09 04:29:25 -03:00
absidue
0dda97e0b0 perf: Cleanup some unnecessary uses of YTNode#key and Maybe (#463) 2023-08-06 19:15:47 -03:00
absidue
e370116092 fix(Playlist): Only try extracting the subtitle for the first page (#465) 2023-08-06 19:14:21 -03:00
LuanRT
3bc53a8c12 fix(parser): Allow any property in the RawResponse interface 2023-08-01 20:38:15 -03:00
github-actions[bot]
74e1a5e068 chore(main): release 5.8.0 (#459)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-30 16:02:19 -03:00
absidue
0fa5a859ae feat(YouTube Playlist): Add subtitle and fix author optionality (#458) 2023-07-30 15:59:39 -03:00
LuanRT
02a111250a chore: update image links 2023-07-28 07:12:45 -03:00
github-actions[bot]
c1886f9a83 chore(main): release 5.7.1 (#455)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-25 02:46:12 -03:00
LuanRT
5f4cbdb904 Merge branch 'main' of https://github.com/LuanRT/YouTube.js 2023-07-25 02:45:00 -03:00
LuanRT
d91695a9ec fix(SearchHeader): remove console.log
Oopsie!
2023-07-25 02:44:43 -03:00
github-actions[bot]
137464ca66 chore(main): release 5.7.0 (#451)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-24 20:48:09 -03:00
LuanRT
6997982cf2 feat(parser): Add SearchHeader
We may want to remove the old SearchSubMenu node in the future but YouTube still uses it sometimes, so we will keep it for now.

Closes #452
2023-07-24 20:26:05 -03:00
absidue
18cbc8c038 feat(parser): Add PageHeader (#450) 2023-07-19 19:00:26 -03:00
github-actions[bot]
30ff087587 chore(main): release 5.6.0 (#448)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-18 15:34:48 -03:00
absidue
1a034733f6 feat(toDash): Add option to include thumbnails in the manifest (#446)
* feat(toDash): Add option to include thumbnails in the manifest

* refactor: Move toDash function back to MediaInfo class
2023-07-18 02:08:02 -03:00
ChunkyProgrammer
c477b824c0 feat(parser): Add IncludingResultsFor (#447) 2023-07-18 01:53:44 -03:00
github-actions[bot]
7e5c3648c1 chore(main): release 5.5.0 (#441)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-16 17:57:11 -03:00
absidue
bdd98a3b9b feat(Format): Populate audio language from captions when available (#445) 2023-07-16 17:54:26 -03:00
LuanRT
06750aaa74 chore: lint 2023-07-16 17:40:05 -03:00
LuanRT
708c5f7394 feat(parser): add MacroMarkersList (#444)
This should fix a few parsing issues that were happening after recent updates.
2023-07-16 17:38:16 -03:00
LuanRT
a9cdbf7010 feat(parser): Add ShowMiniplayerCommand (#443) 2023-07-16 17:34:42 -03:00
LuanRT
b50d1ef67d fix(StructuredDescriptionContent): items can also be a HorizontalCardList 2023-07-16 17:00:36 -03:00
LuanRT
555d257459 feat(parser): Add CommentsSimplebox parser (#442) 2023-07-16 16:46:46 -03:00
titong0
2aef67876e fix(package): Bump Jinter to fix bad export order (#439)
Version 1.0.0 has an export order which crashes some webpack environments (at least I came across it when using next.js 13). Updating to 1.1.0 fixes it. A bit more context here https://github.com/LuanRT/YouTube.js/issues/432

* chore(package): update lock file

* chore: lint

---------

Co-authored-by: LuanRT <luan.lrt4@gmail.com>
2023-07-16 16:23:14 -03:00
ChunkyProgrammer
ae2557d15c feat(parser): Add HashtagTile (#440) 2023-07-16 15:35:55 -03:00
github-actions[bot]
8c688efb4a chore(main): release 5.4.0 (#438)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-14 00:00:43 -03:00
ChunkyProgrammer
cffa868c6e feat(parser): Add Quiz (#437) 2023-07-13 23:57:39 -03:00
ChunkyProgrammer
f267fcd8be Add getReleases and getPodcasts to Channel (#436)
* feat(Channel): Add `getReleases` method

* feat(Channel): Add `getPodcasts` method

* Fix(Playlist): Parse `PlaylistCustomThumbnail`
2023-07-13 15:25:20 -03:00
github-actions[bot]
23c22a93c4 chore(main): release 5.3.0 (#433)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-11 15:48:51 -03:00
absidue
1ca20836bf perf(Format): Cleanup the xtags parsing (#434) 2023-07-11 15:45:42 -03:00
absidue
5f058e69ae perf(toDash): Hoist duplicates from Representation to AdaptationSet (#431) 2023-07-11 15:22:02 -03:00
absidue
3500e92632 feat(toDash): Add color information (#430) 2023-07-10 21:25:48 -03:00
LuanRT
3f57c2fa5c refactor(PlayerEndpoint.ts): send specific params only if using Android based clients 2023-07-10 21:23:10 -03:00
LuanRT
7528ebdb60 chore: fix YouTube Music tests failing 2023-07-10 21:17:06 -03:00
github-actions[bot]
5e3846259f chore(main): release 5.2.1 (#429)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-03 22:30:29 -03:00
LuanRT
222dfce6bb fix: incorrect node parser implementations (#428)
These were causing some issues in v5.2.0.
2023-07-03 21:58:00 -03:00
138 changed files with 6133 additions and 8063 deletions

View File

@@ -1,5 +1,194 @@
# Changelog
## [6.4.1](https://github.com/LuanRT/YouTube.js/compare/v6.4.0...v6.4.1) (2023-10-02)
### Bug Fixes
* **Feed:** Do not throw when multiple continuations are present ([8e372d5](https://github.com/LuanRT/YouTube.js/commit/8e372d5c67f148be288bb0485f2c70ec43fbecd0))
* **Playlist:** Throw a more helpful error when parsing empty responses ([987f506](https://github.com/LuanRT/YouTube.js/commit/987f50604a0163f9a07091ce787995c6f6fddb75))
### Performance Improvements
* Cache deciphered n-params by info response ([#505](https://github.com/LuanRT/YouTube.js/issues/505)) ([d2959b3](https://github.com/LuanRT/YouTube.js/commit/d2959b3a55a5081295da4754627913933bbaf1e7))
* **generator:** Remove duplicate checks in `isMiscType` ([#506](https://github.com/LuanRT/YouTube.js/issues/506)) ([68df321](https://github.com/LuanRT/YouTube.js/commit/68df3218580db10c9a0932c93ff2ce487526ff1e))
## [6.4.0](https://github.com/LuanRT/YouTube.js/compare/v6.3.0...v6.4.0) (2023-09-10)
### Features
* Add support for retrieving transcripts ([#500](https://github.com/LuanRT/YouTube.js/issues/500)) ([f94ea6c](https://github.com/LuanRT/YouTube.js/commit/f94ea6cf917f63f30dd66514b22a4cf43b948f07))
* **PlaylistManager:** add .setName() and .setDescription() functions for editing playlists ([#498](https://github.com/LuanRT/YouTube.js/issues/498)) ([86fb33e](https://github.com/LuanRT/YouTube.js/commit/86fb33ed03a127d9fd4caa695ca97642bffe61bd))
### Bug Fixes
* **BackstagePost:** `vote_button` type mismatch ([fba3fc9](https://github.com/LuanRT/YouTube.js/commit/fba3fc971454d66d80d4920fbd60889a221de381))
## [6.3.0](https://github.com/LuanRT/YouTube.js/compare/v6.2.0...v6.3.0) (2023-08-31)
### Features
* **ChannelMetadata:** Add `music_artist_name` ([#497](https://github.com/LuanRT/YouTube.js/issues/497)) ([91de6e5](https://github.com/LuanRT/YouTube.js/commit/91de6e5c0e5b27e6d12ce5db2f500c5ff78b9830))
* **Session:** Add on_behalf_of_user session option. ([#494](https://github.com/LuanRT/YouTube.js/issues/494)) ([8bc2aaa](https://github.com/LuanRT/YouTube.js/commit/8bc2aaa3587fcf79f69eedbc2bf422a4c6fa7eb1))
### Bug Fixes
* **CompactMovie:** Add missing import and remove unnecessary console.log ([#496](https://github.com/LuanRT/YouTube.js/issues/496)) ([c26972c](https://github.com/LuanRT/YouTube.js/commit/c26972c42a6368822ac254c00f1bbee5a1542486))
## [6.2.0](https://github.com/LuanRT/YouTube.js/compare/v6.1.0...v6.2.0) (2023-08-29)
### Features
* **Session:** Add fallback for session data retrieval ([#490](https://github.com/LuanRT/YouTube.js/issues/490)) ([10c15bf](https://github.com/LuanRT/YouTube.js/commit/10c15bfb9f131a2acea2f26ff3328993d8d8f4aa))
### Bug Fixes
* **Format:** Fix `is_original` always being `true` ([#492](https://github.com/LuanRT/YouTube.js/issues/492)) ([0412fa0](https://github.com/LuanRT/YouTube.js/commit/0412fa05ff1f00960b398c2f18d5ce39ce0cb864))
## [6.1.0](https://github.com/LuanRT/YouTube.js/compare/v6.0.2...v6.1.0) (2023-08-27)
### Features
* **parser:** Add `AlertWithButton` ([#486](https://github.com/LuanRT/YouTube.js/issues/486)) ([8b69587](https://github.com/LuanRT/YouTube.js/commit/8b6958778721ba274283f641779fb60bc6f42cd2))
* **parser:** Add `ChannelHeaderLinksView` ([#484](https://github.com/LuanRT/YouTube.js/issues/484)) ([ed7be2a](https://github.com/LuanRT/YouTube.js/commit/ed7be2a675cf1ec663e743e90db6260c97546739))
* **parser:** Add `CompactMovie` ([#487](https://github.com/LuanRT/YouTube.js/issues/487)) ([2eed172](https://github.com/LuanRT/YouTube.js/commit/2eed1726d5bde7648af09273cc14ab4a315cb23e))
## [6.0.2](https://github.com/LuanRT/YouTube.js/compare/v6.0.1...v6.0.2) (2023-08-24)
### Bug Fixes
* invalid set ids in dash manifest ([#480](https://github.com/LuanRT/YouTube.js/issues/480)) ([1c3ea2a](https://github.com/LuanRT/YouTube.js/commit/1c3ea2acd38652c6b40a0817a7836c672a776c4e))
## [6.0.1](https://github.com/LuanRT/YouTube.js/compare/v6.0.0...v6.0.1) (2023-08-22)
### Bug Fixes
* **SearchSubMenu:** Groups not being parsed due to a typo ([90be877](https://github.com/LuanRT/YouTube.js/commit/90be877d28e0ef013056eaeaa4f2765c91addd61))
## [6.0.0](https://github.com/LuanRT/YouTube.js/compare/v5.8.0...v6.0.0) (2023-08-18)
### ⚠ BREAKING CHANGES
* replace unnecessary classes with pure functions ([#468](https://github.com/LuanRT/YouTube.js/issues/468))
### Features
* **MusicResponsiveListItem:** Detect non music tracks properly ([815e54b](https://github.com/LuanRT/YouTube.js/commit/815e54b854fcda3f5423231c8495ce1fb69d8237))
* **parser:** add `MusicMultiRowListItem` ([494ee87](https://github.com/LuanRT/YouTube.js/commit/494ee8776af0839d3ee2cca3d2fd836680cfdb9e))
* **Session:** Add `IOS` to `ClientType` enum ([22a38c0](https://github.com/LuanRT/YouTube.js/commit/22a38c0762499de74f0aeb3ef01332f893518b08))
* **VideoInfo:** support iOS client ([#467](https://github.com/LuanRT/YouTube.js/issues/467)) ([46fe18b](https://github.com/LuanRT/YouTube.js/commit/46fe18b763e0c943b24ea10fdf25456ab9ade709))
### Bug Fixes
* **Format:** Extracting audio language from captions ([#470](https://github.com/LuanRT/YouTube.js/issues/470)) ([31d27b1](https://github.com/LuanRT/YouTube.js/commit/31d27b1bca489ee0053d2783f1a956609845a901))
* **parser:** Allow any property in the `RawResponse` interface ([3bc53a8](https://github.com/LuanRT/YouTube.js/commit/3bc53a8c12e65b22f19a3e337641196b692a94db))
* **parser:** Logger logging `classdata` as `[Object object]` ([bf1510b](https://github.com/LuanRT/YouTube.js/commit/bf1510b235e3ee7d13d51f092babd1105c3d6b9f))
* **Playlist:** Only try extracting the subtitle for the first page ([#465](https://github.com/LuanRT/YouTube.js/issues/465)) ([e370116](https://github.com/LuanRT/YouTube.js/commit/e3701160928e9e959b88ca215c6b0a44c70ca6e6))
* **toDash:** Format grouping into AdaptationSets ([#462](https://github.com/LuanRT/YouTube.js/issues/462)) ([1ff3e1a](https://github.com/LuanRT/YouTube.js/commit/1ff3e1a440389e71055d4b201c29021ca5b39254))
### Performance Improvements
* Cleanup some unnecessary uses of `YTNode#key` and `Maybe` ([#463](https://github.com/LuanRT/YouTube.js/issues/463)) ([0dda97e](https://github.com/LuanRT/YouTube.js/commit/0dda97e0b03171de52d7f11a5abf78911e74cead))
### Code Refactoring
* replace unnecessary classes with pure functions ([#468](https://github.com/LuanRT/YouTube.js/issues/468)) ([87ed396](https://github.com/LuanRT/YouTube.js/commit/87ed3960ffa1c738b6f3b5acaf423647db4d367e))
## [5.8.0](https://github.com/LuanRT/YouTube.js/compare/v5.7.1...v5.8.0) (2023-07-30)
### Features
* **YouTube Playlist:** Add subtitle and fix author optionality ([#458](https://github.com/LuanRT/YouTube.js/issues/458)) ([0fa5a85](https://github.com/LuanRT/YouTube.js/commit/0fa5a859ae15a35266297079e3e34fd9f3a5ebf4))
## [5.7.1](https://github.com/LuanRT/YouTube.js/compare/v5.7.0...v5.7.1) (2023-07-25)
### Bug Fixes
* **SearchHeader:** remove console.log ([d91695a](https://github.com/LuanRT/YouTube.js/commit/d91695a9ec6c55445cbeedba4ace4ac1e0a72eee))
## [5.7.0](https://github.com/LuanRT/YouTube.js/compare/v5.6.0...v5.7.0) (2023-07-24)
### Features
* **parser:** Add `PageHeader` ([#450](https://github.com/LuanRT/YouTube.js/issues/450)) ([18cbc8c](https://github.com/LuanRT/YouTube.js/commit/18cbc8c038ddddffa1ba1519e56a8054b2996e42))
* **parser:** Add `SearchHeader` ([6997982](https://github.com/LuanRT/YouTube.js/commit/6997982cf2db87edf4929e9a77e2690e7b630d3d)), closes [#452](https://github.com/LuanRT/YouTube.js/issues/452)
## [5.6.0](https://github.com/LuanRT/YouTube.js/compare/v5.5.0...v5.6.0) (2023-07-18)
### Features
* **parser:** Add `IncludingResultsFor` ([#447](https://github.com/LuanRT/YouTube.js/issues/447)) ([c477b82](https://github.com/LuanRT/YouTube.js/commit/c477b824c084552169062f72cde8890e77b31f59))
* **toDash:** Add option to include thumbnails in the manifest ([#446](https://github.com/LuanRT/YouTube.js/issues/446)) ([1a03473](https://github.com/LuanRT/YouTube.js/commit/1a034733f6bb641e2d97df12de81ae3516c1f703))
## [5.5.0](https://github.com/LuanRT/YouTube.js/compare/v5.4.0...v5.5.0) (2023-07-16)
### Features
* **Format:** Populate audio language from captions when available ([#445](https://github.com/LuanRT/YouTube.js/issues/445)) ([bdd98a3](https://github.com/LuanRT/YouTube.js/commit/bdd98a3b9be39c11942043a300a6ebce9a15efc6))
* **parser:** Add `CommentsSimplebox` parser ([#442](https://github.com/LuanRT/YouTube.js/issues/442)) ([555d257](https://github.com/LuanRT/YouTube.js/commit/555d257459b76d7c0158e9c6b189a75a82b10faf))
* **parser:** Add `HashtagTile` ([#440](https://github.com/LuanRT/YouTube.js/issues/440)) ([ae2557d](https://github.com/LuanRT/YouTube.js/commit/ae2557d15c9df09bb92e0dc6191670d72b36631a))
* **parser:** add `MacroMarkersList` ([#444](https://github.com/LuanRT/YouTube.js/issues/444)) ([708c5f7](https://github.com/LuanRT/YouTube.js/commit/708c5f7394b4ea140836b9483848cb61b97ea1af))
* **parser:** Add `ShowMiniplayerCommand` ([#443](https://github.com/LuanRT/YouTube.js/issues/443)) ([a9cdbf7](https://github.com/LuanRT/YouTube.js/commit/a9cdbf7010e7b9b9cfde5db645d51bdad51006c5))
### Bug Fixes
* **package:** Bump Jinter to fix bad export order ([#439](https://github.com/LuanRT/YouTube.js/issues/439)) ([2aef678](https://github.com/LuanRT/YouTube.js/commit/2aef67876ec19118b37d3cecd429ccf8239989e0))
* **StructuredDescriptionContent:** `items` can also be a `HorizontalCardList` ([b50d1ef](https://github.com/LuanRT/YouTube.js/commit/b50d1ef67d81276864818de10c61b5a7980cbc1a))
## [5.4.0](https://github.com/LuanRT/YouTube.js/compare/v5.3.0...v5.4.0) (2023-07-14)
### Features
* **Channel:** Add `getPodcasts()` method ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
* **Channel:** Add `getReleases()` method ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
* **parser:** Add `Quiz` ([#437](https://github.com/LuanRT/YouTube.js/issues/437)) ([cffa868](https://github.com/LuanRT/YouTube.js/commit/cffa868c6eeb579047653fac65da8e913fb3c621))
### Bug Fixes
* **Playlist:** Parse `PlaylistCustomThumbnail` for `thumbnail_renderer` ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
## [5.3.0](https://github.com/LuanRT/YouTube.js/compare/v5.2.1...v5.3.0) (2023-07-11)
### Features
* **toDash:** Add color information ([#430](https://github.com/LuanRT/YouTube.js/issues/430)) ([3500e92](https://github.com/LuanRT/YouTube.js/commit/3500e926327d560b1db036bfe503c276b91922ac))
### Performance Improvements
* **Format:** Cleanup the xtags parsing ([#434](https://github.com/LuanRT/YouTube.js/issues/434)) ([1ca2083](https://github.com/LuanRT/YouTube.js/commit/1ca20836bf343c78461fab7ad3b71db2b96e65c3))
* **toDash:** Hoist duplicates from Representation to AdaptationSet ([#431](https://github.com/LuanRT/YouTube.js/issues/431)) ([5f058e6](https://github.com/LuanRT/YouTube.js/commit/5f058e69ae8594491133f7f96287bea4137f7822))
## [5.2.1](https://github.com/LuanRT/YouTube.js/compare/v5.2.0...v5.2.1) (2023-07-04)
### Bug Fixes
* incorrect node parser implementations ([#428](https://github.com/LuanRT/YouTube.js/issues/428)) ([222dfce](https://github.com/LuanRT/YouTube.js/commit/222dfce6bbd13b2cd80ae11540cbc0edd9053fc5))
## [5.2.0](https://github.com/LuanRT/YouTube.js/compare/v5.1.0...v5.2.0) (2023-06-28)

View File

@@ -4,7 +4,6 @@ This page lists the collaborators who have contributed to the development and su
## [LuanRT](https://github.com/LuanRT)
[![Github Sponsors](https://img.shields.io/badge/donate-30363D?style=flat-square&logo=GitHub-Sponsors&logoColor=#white)](https://github.com/sponsors/LuanRT)
[![Ko-Fi](https://img.shields.io/badge/Ko--Fi-30363D?style=flat-square&logo=ko-fi)](https://ko-fi.com/luanrt)
Owner and maintainer.
@@ -15,4 +14,7 @@ Initial parser implementation, several bug fixes, major refactorings and general
Bug fixes and TypeScript support.
## [patrickkfkan](https://github.com/patrickkfkan)
Major refactorings, improved YouTube Music support, and bug fixes.
Major refactorings, improved YouTube Music support, and bug fixes.
## [Absidue](https://github.com/absidue)
Several bug fixes, new features & improved MPD support.

View File

@@ -32,7 +32,7 @@
<br>
<br>
<a href="https://serpapi.com" target="_blank">
<img width="80" alt="SerpApi" src="https://luanrt.is-a.dev/assets/img/serpapi.svg" />
<img width="80" alt="SerpApi" src="https://luanrt.github.io/assets/img/serpapi.svg" />
<br>
<sub>
API to get search engine results with ease.
@@ -325,6 +325,9 @@ Retrieves video info.
- `<info>#download(options)`
- Downloads the video. See [download](#download).
- `<info>#getTranscript()`
- Retrieves the video's transcript.
- `<info>#filters`
- Returns filters that can be applied to the watch next feed.
@@ -542,6 +545,8 @@ Retrieves contents for a given channel.
- `<channel>#getVideos()`
- `<channel>#getShorts()`
- `<channel>#getLiveStreams()`
- `<channel>#getReleases()`
- `<channel>#getPodcasts()`
- `<channel>#getPlaylists()`
- `<channel>#getHome()`
- `<channel>#getCommunity()`
@@ -786,7 +791,7 @@ We are immensely grateful to all the wonderful people who have contributed to th
## Contact
LuanRT - [@thesciencephile][twitter] - luan.lrt4@gmail.com
LuanRT - [@thesciencephile][twitter] - luanrt@thatsciencephile.com
Project Link: [https://github.com/LuanRT/YouTube.js][project]

View File

@@ -1,3 +1,3 @@
export * from './deno/src/platform/deno.ts';
import Innertube from './deno/src/platform/deno.ts';
export default Innertube;
export default Innertube;

View File

@@ -10,6 +10,8 @@ Playlist management class.
* [.addVideos(playlist_id, video_ids)](#addvideos)
* [.removeVideos(playlist_id, video_ids)](#removevideos)
* [.moveVideo(playlist_id, moved_video_id, predecessor_video_id)](#movevideo)
* [.setName(playlist_id, name)](#setname)
* [.setDescription(playlist_id, description)](#setdescription)
<a name="create"></a>
### create(title, video_ids)
@@ -69,4 +71,29 @@ Moves a video to a new position within a given playlist.
| --- | --- | --- |
| playlist_id | `string` | Playlist id |
| moved_video_id | `string` | the video to be moved |
| predecessor_video_id | `string` | the video present in the target position |
| predecessor_video_id | `string` | the video present in the target position |
<a name="setname"></a>
### setName(playlist_id, name)
Sets the name / title for the given playlist.
**Returns:** `Promise.<ApiResponse>`
| Param | Type | Description |
| --- | --- | --- |
| playlist_id | `string` | Playlist id |
| name | `string` | Name / title |
<a name="setdescription"></a>
### setDescription(playlist_id, description)
Sets the description for the given playlist.
**Returns:** `Promise.<ApiResponse>`
| Param | Type | Description |
| --- | --- | --- |
| playlist_id | `string` | Playlist id |
| description | `string` | Description |

View File

@@ -31,24 +31,22 @@ For example, suppose we have found a new renderer named `verticalListRenderer`.
> `../classes/VerticalList.ts`
```ts
import Parser from '..';
import { YTNode } from '../helpers';
import type { RawNode } from '../index.js';
import Parser, { RawNode } from '../index.js';
import { YTNode } from '../helpers.js';
class VerticalList extends YTNode {
export default class VerticalList extends YTNode {
static type = 'VerticalList';
header;
contents;
constructor(data: RawNode) {
super();
// parse the data here, ex;
this.header = Parser.parseItem(data.header);
this.contents = Parser.parseArray(data.contents);
}
}
export default VerticalList;
```
You may use the parser's generated class for the new renderer as a starting point for your own implementation.

View File

@@ -1,4 +1,4 @@
const { Innertube, UniversalCache } = require('youtubei.js');
import { Innertube, UniversalCache } from 'youtubei.js';
(async () => {
const yt = await Innertube.create({

View File

@@ -1,21 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="stylesheet" href="/src/assets/style.css" />
<link rel="stylesheet" href="/src/assets/player.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YouTube.js Example</title>
</head>
<body>
<form>
<input type="text" name="id" placeholder="Video ID or URL" />
<input type="submit" value="Play" />
</form>
<div id="loader"></div>
<div id="video_container">
<video id="video"></video>
<div class="loader" id="loader"></div>
<div id="video-container">
<div class="shaka-container" id="shaka-container" data-shaka-player-container>
<video class="videoel" id="videoel" data-shaka-player autoplay></video>
</div>
<h2 id="title"></h2>
<div id="metadata"></div>
<hr />
@@ -26,5 +28,4 @@
</footer>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -13,6 +13,6 @@
"vite": "^3.0.0"
},
"dependencies": {
"dashjs": "^4.4.0"
"shaka-player": "^4.3.8"
}
}

View File

@@ -0,0 +1,423 @@
@import url(https://fonts.googleapis.com/css?family=Material+Icons+Sharp);
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Me5Q.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmEU9vAw.ttf) format('truetype');
}
.shaka-container {
font-family: 'Roboto', sans-serif;
}
.shaka-container .shaka-bottom-controls {
width: 100%;
padding: 0;
padding-bottom: 0;
z-index: 1;
}
.shaka-container .shaka-bottom-controls {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
}
.shaka-container .shaka-ad-controls {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
}
.shaka-container .shaka-spinner .shaka-spinner-path {
stroke: #ffffff;
}
.shaka-container .shaka-scrim-container {
margin: 0;
width: 100%;
height: 100%;
flex-shrink: 1;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0;
transition: opacity cubic-bezier(.4, 0, .6, 1) .6s;
background: linear-gradient(to top, hsla(0, 0%, 0%, 0.61), transparent 15%);
}
.shaka-container .shaka-play-button {
width: 100px;
height: 100px;
border-radius: 0;
background-color: transparent;
filter: invert();
box-shadow: none;
-webkit-box-ordinal-group: -3;
-ms-flex-order: -4;
order: -4;
}
.shaka-container .shaka-small-play-button {
-webkit-box-ordinal-group: -2;
-ms-flex-order: -3;
order: -3;
}
.shaka-container .shaka-mute-button {
-webkit-box-ordinal-group: -1;
-ms-flex-order: -2;
order: -2;
}
.shaka-container .shaka-controls-button-panel>* {
margin: 0;
padding: 3px 8px;
color: #EEE;
height: 40px;
}
.shaka-container .shaka-controls-button-panel>*:hover {
color: #FFF;
}
.shaka-container .shaka-controls-button-panel .shaka-volume-bar-container {
position: relative;
z-index: 10;
left: -1px;
-webkit-box-ordinal-group: 0;
-ms-flex-order: -1;
order: -1;
opacity: 0;
width: 0px;
-webkit-transition: width 0.2s cubic-bezier(0.4, 0, 1, 1);
height: 3px;
transition: width 0.2s cubic-bezier(0.4, 0, 1, 1);
padding: 0;
}
.shaka-container .shaka-controls-button-panel .shaka-volume-bar-container:hover,
.shaka-container .shaka-controls-button-panel .shaka-volume-bar-container:focus {
display: block;
width: 50px;
opacity: 1;
padding: 0 6px;
}
.shaka-container .shaka-mute-button:hover+div {
opacity: 1;
width: 50px;
padding: 0 6px;
}
.shaka-container .shaka-current-time {
padding: 0 10px;
font-size: 12px;
}
.shaka-container .shaka-seek-bar-container {
height: 3px;
position: relative;
top: -1px;
border-radius: 0;
margin-bottom: 0;
}
.shaka-container .shaka-seek-bar-container .shaka-range-element {
opacity: 0;
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1);
}
.shaka-container .shaka-seek-bar-container:hover {
height: 5px;
top: 0;
cursor: pointer;
}
.shaka-container .shaka-seek-bar-container:hover .shaka-range-element {
opacity: 1;
cursor: pointer;
}
.shaka-container .shaka-seek-bar-container input[type=range]::-webkit-slider-thumb {
background: #FF0000;
cursor: pointer;
}
.shaka-container .shaka-seek-bar-container input[type=range]::-moz-range-thumb {
background: #FF0000;
cursor: pointer;
}
.shaka-container .shaka-seek-bar-container input[type=range]::-ms-thumb {
background: #FF0000;
cursor: pointer;
}
.shaka-container .shaka-video-container * {
font-family: 'Roboto', sans-serif;
}
.shaka-container .shaka-video-container .material-icons-round {
font-family: 'Material Icons Sharp';
}
.shaka-container .shaka-overflow-menu,
.shaka-container .shaka-settings-menu {
border-radius: 2px;
background: rgba(37, 37, 37, 0.9);
text-shadow: 0 0 2px rgb(0 0 0%);
-webkit-transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1);
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1);
-moz-user-select: none;
-ms-user-select: none;
animation: fade 0.3s;
-webkit-user-select: none;
right: 10px;
bottom: 50px;
padding: 0;
min-width: 200px;
}
@keyframes fade {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.shaka-container .shaka-settings-menu {
padding: 0 0 8px;
}
.shaka-container .shaka-settings-menu button {
font-size: 12px;
}
.shaka-container .shaka-settings-menu button span {
margin-left: 33px;
font-size: 13px;
}
.shaka-container .shaka-settings-menu button[aria-selected="true"] {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.shaka-container .shaka-settings-menu button[aria-selected="true"] span {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2;
margin-left: 0;
}
.shaka-container .shaka-settings-menu button[aria-selected="true"] i {
-webkit-box-ordinal-group: 2;
-ms-flex-order: 1;
order: 1;
font-size: 18px;
padding-left: 5px;
}
.shaka-container .shaka-overflow-menu button {
padding: 0;
}
.shaka-container .shaka-overflow-menu button i {
display: none;
}
.shaka-container .shaka-overflow-menu button .shaka-overflow-button-label {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: default;
outline: none;
height: 40px;
-webkit-box-flex: 0;
-ms-flex: 0 0 100%;
flex: 0 0 100%;
}
.shaka-container .shaka-overflow-menu button .shaka-overflow-button-label span {
-ms-flex-negative: initial;
flex-shrink: initial;
padding-left: 15px;
font-size: 13px;
font-weight: 500;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.shaka-container .shaka-overflow-menu span+span {
color: #FFF;
font-weight: 400 !important;
font-size: 12px !important;
padding-right: 8px;
padding-left: 0 !important;
}
.shaka-container .shaka-overflow-menu span+span:after {
content: "navigate_next";
font-family: 'Material Icons Sharp';
font-size: 20px;
}
.shaka-container .shaka-overflow-menu .shaka-pip-button span+span {
padding-right: 15px !important;
}
.shaka-container .shaka-overflow-menu .shaka-pip-button span+span:after {
content: "";
}
.shaka-container .shaka-back-to-overflow-button {
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
font-size: 12px;
color: #eee;
height: 40px;
}
.shaka-container .shaka-back-to-overflow-button .material-icons-round {
font-size: 15px;
padding-right: 10px;
}
.shaka-container .shaka-back-to-overflow-button span {
margin-left: 3px !important;
}
.shaka-container .shaka-overflow-menu button:hover,
.shaka-container .shaka-settings-menu button:hover {
background-color: rgba(255, 255, 255, 0.1);
cursor: pointer;
}
.shaka-container .shaka-overflow-menu button:hover label,
.shaka-container .shaka-settings-menu button:hover label {
cursor: pointer;
}
.shaka-container .shaka-overflow-menu button,
.shaka-container .shaka-settings-menu button {
color: #EEE;
}
.shaka-container .shaka-captions-off {
color: #BFBFBF;
}
.shaka-container .shaka-overflow-menu-button {
font-size: 18px;
margin-right: 5px;
}
.shaka-container .shaka-fullscreen-button:hover {
font-size: 25px;
-webkit-transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1);
transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1);
}
.shaka-container .shaka-overflow-menu,
.shaka-container .shaka-settings-menu {
border-radius: 10px;
}
@media (prefers-color-scheme: light) {
.shaka-container .shaka-overflow-menu,
.shaka-container .shaka-settings-menu {
background: rgba(255, 255, 255, 0.9);
}
.shaka-container .shaka-overflow-menu span+span,
.shaka-container .shaka-overflow-menu button,
.shaka-container .shaka-settings-menu button {
color: #000000;
}
}
@media (min-width: 800px) {
.shaka-container .shaka-controls-button-panel {
-webkit-box-ordinal-group: 3;
-ms-flex-order: 2;
order: 2;
height: 40px;
padding: 0 10px;
}
}
@media (max-width: 800px) {
.shaka-container .shaka-scrim-container {
background: rgba(0, 0, 0, 0.5);
}
.shaka-container .shaka-range-container {
margin: 0;
top: 0;
}
.shaka-container .shaka-mute-button {
display: none;
}
.shaka-container .shaka-overflow-menu,
.shaka-container .shaka-settings-menu {
bottom: 0;
top: 0;
left: 0;
right: 0;
width: 80%;
margin: 10px;
border-radius: 10px;
}
.shaka-container .shaka-overflow-menu button,
.shaka-container .shaka-settings-menu button {
width: 100%;
height: 40px;
padding: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.shaka-container .shaka-overflow-menu button span,
.shaka-container .shaka-settings-menu button span {
margin-left: 0;
padding-left: 15px;
}
}

View File

@@ -0,0 +1,135 @@
body {
display: flex;
flex-direction: column;
align-items: center;
background-color: #202020;
color: rgb(255, 255, 255);
line-height: 1.6;
font-family: Roboto, Arial, sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: transparent;
}
hr {
width: 100%;
border: 1px solid transparent;
background-color: rgb(68, 68, 68);
}
form {
margin: 0.5rem 0;
display: none;
border-radius: 0.3rem;
background-color: rgb(68, 68, 68);
}
form input {
padding: 0.5rem;
border: none;
color: rgb(255, 255, 255);
}
form input[type="text"] {
background: transparent;
}
form input[type="text"]:focus {
outline: none;
}
form input[type="submit"] {
color: rgb(255, 255, 255);
background-color: rgba(0, 0, 0, 0.244);
cursor: pointer;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-background-clip: text;
-webkit-text-fill-color: #ffffff;
transition: background-color 5000s ease-in-out 0s;
}
#loader {
display: block;
border: 10px solid rgb(68, 68, 68);
border-top: 10px solid rgb(255, 255, 255);
border-radius: 50%;
width: 50px;
height: 50px;
align-self: center;
animation: spin 1s linear infinite;
margin: 0.5rem 0;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#shaka-container {
height: 40vw;
}
#video-container {
display: none;
flex-direction: column;
position: relative;
width: 70vw !important;
margin: 0.5rem 0;
}
#metadata {
display: flex;
flex-direction: row;
align-self: left;
margin: 0.5rem 0;
}
#metadata>#metadata-item {
margin: 0 0.3rem;
background-color: #ffffff;
color: rgba(0, 0, 0, 0.757);
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 0.3rem;
}
#video-container>#description {
align-self: left;
margin-left: 0.5rem;
font-size: medium;
}
video {
width: 100%;
height: 100%;
}
footer {
margin: 0.5rem 0;
}
@media screen and (max-width: 768px) {
video {
height: auto;
}
#shaka-container {
height: auto;
}
#video-container {
width: 100% !important;
}
}

View File

@@ -1,27 +1,27 @@
import './style.css';
import { Innertube, UniversalCache } from '../../../../bundle/browser';
import dashjs from 'dashjs';
const description = document.getElementById('description') as HTMLDivElement;
const form = document.querySelector('form') as HTMLFormElement;
// @ts-ignore - Shaka's TS support is not the best.
import shaka from 'shaka-player/dist/shaka-player.ui.js';
import "shaka-player/dist/controls.css";
const title = document.getElementById('title') as HTMLHeadingElement;
const description = document.getElementById('description') as HTMLDivElement;
const metadata = document.getElementById('metadata') as HTMLDivElement;
const loader = document.getElementById('loader') as HTMLDivElement;
const video = document.getElementById('video') as HTMLVideoElement;
const video_container = document.getElementById('video_container') as HTMLDivElement;
const form = document.querySelector('form') as HTMLFormElement;
async function main() {
const yt = await Innertube.create({
generate_session_locally: true,
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
// url
const url = typeof input === 'string'
? new URL(input)
: input instanceof URL
? input
: new URL(input.url);
// transform the url for use with our proxy
// Transform the url for use with our proxy.
url.searchParams.set('__host', url.host);
url.host = 'localhost:8080';
url.protocol = 'http';
@@ -32,13 +32,15 @@ async function main() {
? input.headers
: new Headers();
// now serialize the headers
// Now serialize the headers.
url.searchParams.set('__headers', JSON.stringify([...headers]));
// @ts-ignore
input.duplex = 'half';
if (input instanceof Request) {
// @ts-ignore
input.duplex = 'half';
}
// copy over the request
// Copy over the request.
const request = new Request(
url,
input instanceof Request ? input : undefined,
@@ -46,7 +48,6 @@ async function main() {
headers.delete('user-agent');
// fetch the url
return fetch(request, init ? {
...init,
headers
@@ -60,45 +61,46 @@ async function main() {
form.animate({ opacity: [0, 1] }, { duration: 300, easing: 'ease-in-out' });
form.style.display = 'block';
showUI(false);
showUI({ hidePlayer: true });
let player: dashjs.MediaPlayerClass | undefined;
let player: shaka.Player | undefined;
let ui: shaka.ui.Overlay | undefined;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (player) {
player.reset();
player.destroy();
}
hideUI();
let video_id;
let videoId;
const video_id_or_url = document.querySelector<HTMLInputElement>('input[type=text]')?.value;
const videoIdOrURL = document.querySelector<HTMLInputElement>('input[type=text]')?.value;
if (!video_id_or_url) {
if (!videoIdOrURL) {
title.textContent = 'No video id or URL provided';
showUI(false);
showUI({ hidePlayer: true });
return;
}
try {
if (video_id_or_url.match(/(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/)) {
const endpoint = await yt.resolveURL(video_id_or_url);
if (videoIdOrURL.match(/(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/)) {
const endpoint = await yt.resolveURL(videoIdOrURL);
if (!endpoint.payload.videoId) {
title.textContent = 'Could not resolve URL';
showUI(false);
showUI({ hidePlayer: true });
return;
}
video_id = endpoint.payload.videoId;
videoId = endpoint.payload.videoId;
} else {
video_id = video_id_or_url;
videoId = videoIdOrURL;
}
const info = await yt.getInfo(video_id);
const info = await yt.getInfo(videoId);
title.textContent = info.basic_info.title || null;
description.innerHTML = info.secondary_info?.description.toHTML() || '';
@@ -106,51 +108,172 @@ async function main() {
document.title = info.basic_info.title || '';
metadata!.innerHTML = '';
metadata!.innerHTML += `<div class="metadata_item">${info.primary_info?.published.toHTML()}</div>`;
metadata!.innerHTML += `<div class="metadata_item">${info.primary_info?.view_count.toHTML()}</div>`;
metadata!.innerHTML += `<div class="metadata_item">${info.basic_info.like_count} likes</div>`;
metadata.innerHTML = '';
metadata.innerHTML += `<div id="metadata-item">${info.primary_info?.published.toHTML()}</div>`;
metadata.innerHTML += `<div id="metadata-item">${info.primary_info?.view_count.toHTML()}</div>`;
metadata.innerHTML += `<div id="metadata-item">${info.basic_info.like_count} likes</div>`;
showUI(true);
showUI({ hidePlayer: false });
const dash = await info.toDash((url) => {
url.searchParams.set('__host', url.host);
url.host = 'localhost:8080';
url.protocol = 'http';
return url;
});
const dash = await info.toDash();
const uri = 'data:application/dash+xml;charset=utf-8;base64,' + btoa(dash);
// create and append video element
const video_element = document.querySelector('video') as HTMLVideoElement;
video_element.setAttribute('controls', 'true');
video_element.poster = info.basic_info.thumbnail![0].url;
// use dash.js to parse the manifest
if (player) {
player.destroy();
await player.destroy();
player = undefined;
}
player = dashjs.MediaPlayer().create();
player.initialize(video_element, uri, true);
player.setInitialMediaSettingsFor('audio', { lang: 'en-US' });
if (ui) {
ui.destroy();
ui = undefined;
}
const videoEl = document.getElementById('videoel') as HTMLVideoElement;
const shakaContainer = document.getElementById('shaka-container') as HTMLDivElement;
shakaContainer
.querySelectorAll("div")
.forEach(node => node.remove());
shaka.polyfill.installAll();
if (shaka.Player.isBrowserSupported()) {
videoEl.poster = info.basic_info.thumbnail![0].url;
player = new shaka.Player(videoEl);
ui = new shaka.ui.Overlay(player, shakaContainer, videoEl);
const config = {
seekBarColors: {
base: 'rgba(255,255,255,.2)',
buffered: 'rgba(255,255,255,.4)',
played: 'rgb(255,0,0)',
},
fadeDelay: 0,
};
ui.configure(config);
const overflowMenuButton = document.querySelector('.shaka-overflow-menu-button');
if (overflowMenuButton) {
overflowMenuButton.innerHTML = 'settings';
}
const backToOverflowButton = document.querySelector('.shaka-back-to-overflow-button .material-icons-round');
if (backToOverflowButton) {
backToOverflowButton.innerHTML = 'arrow_back_ios_new';
}
player.configure({
streaming: {
bufferingGoal: 180,
rebufferingGoal: 0.02,
bufferBehind: 300
}
});
player.getNetworkingEngine()?.registerRequestFilter((_type: any, request: any) => {
const uri = request.uris[0];
const url = new URL(uri);
const headers = request.headers;
if (url.host.endsWith(".googlevideo.com") || headers.Range) {
url.searchParams.set('__host', url.host);
url.host = 'localhost:8080';
url.protocol = 'http';
}
request.method = 'POST';
// protobuf - { 15: 0 }
request.body = new Uint8Array([120, 0]);
if (url.pathname === "/videoplayback") {
if (headers.Range) {
request.headers = {};
url.searchParams.set("range", headers.Range.split("=")[1]);
url.searchParams.set("alr", "yes");
}
}
request.uris[0] = url.toString();
});
// The UTF-8 characters "h", "t", "t", and "p".
const HTTP_IN_HEX = 0x68747470;
const RequestType = shaka.net.NetworkingEngine.RequestType;
player.getNetworkingEngine()?.registerResponseFilter(async (type: any, response: any) => {
const dataView = new DataView(response.data);
if (response.data.byteLength < 4 ||
dataView.getUint32(0) != HTTP_IN_HEX) {
// This doesn't start with "http", so it is not an ALR.
return;
}
// Interpret the response data as a URL string.
const response_as_string = shaka.util.StringUtils.fromUTF8(response.data);
let retry_parameters;
if (type == RequestType.MANIFEST) {
retry_parameters = player!.getConfiguration().manifest.retryParameters;
} else if (type == RequestType.SEGMENT) {
retry_parameters = player!.getConfiguration().streaming.retryParameters;
} else if (type == RequestType.LICENSE) {
retry_parameters = player!.getConfiguration().drm.retryParameters;
} else {
retry_parameters = shaka.net.NetworkingEngine.defaultRetryParameters();
}
// Make another request for the redirect URL.
const uris = [response_as_string];
const redirect_request = shaka.net.NetworkingEngine.makeRequest(uris, retry_parameters);
const request_operation = player!.getNetworkingEngine()!.request(type, redirect_request);
const redirect_response = await request_operation.promise;
// Modify the original response to contain the results of the redirect
// response.
response.data = redirect_response.data;
response.headers = redirect_response.headers;
response.uri = redirect_response.uri;
});
try {
await player.load(uri);
} catch (e) {
console.error('Could not load manifest', e);
}
} else {
console.error('Browser not supported!');
}
} catch (error) {
title.textContent = 'An error occurred (see console)';
showUI(false);
showUI({ hidePlayer: true });
console.error(error);
}
});
}
function showUI(with_video = true) {
loader.style.display = 'none';
video.style.display = with_video ? 'block' : 'none';
function showUI(args: { hidePlayer?: boolean } = {
hidePlayer: true,
}) {
const ytplayer = document.getElementById('shaka-container') as HTMLDivElement;
ytplayer.style.display = args.hidePlayer ? 'none' : 'block';
const video_container = document.getElementById('video-container') as HTMLDivElement;
video_container.animate({ opacity: [0, 1] }, { duration: 300, easing: 'ease-in-out' });
video_container.style.display = 'block';
loader.style.display = 'none';
}
function hideUI() {
const video_container = document.getElementById('video-container') as HTMLDivElement;
video_container.style.display = 'none';
loader.style.display = 'block';
}

View File

@@ -1,90 +0,0 @@
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: rgb(32, 32, 32);
color: rgb(255, 255, 255);
}
hr {
height: 1px;
width: 100%;
border: 1px solid transparent;
background-color: rgb(68, 68, 68);
}
form {
margin: 0.5rem 0;
display: none;
}
#loader {
display: block;
border: 10px solid rgb(68, 68, 68);
border-top: 10px solid rgb(255, 255, 255);
border-radius: 50%;
width: 50px;
height: 50px;
align-self: center;
animation: spin 1s linear infinite;
margin: 0.5rem 0;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#video_container {
display: none;
flex-direction: column;
position: relative;
width: 70vw !important;
margin: 0.5rem 0;
}
#metadata {
display: flex;
flex-direction: row;
align-self: left;
margin: 0.5rem 0;
}
#metadata > .metadata_item {
margin: 0 0.3rem;
background-color: beige;
color: black;
font: 1em bold;
padding: 0.2rem 0.5rem;
border-radius: 0.3rem;
}
#video_container > #description {
align-self: left;
margin-left: 0.5rem;
font-size: medium;
}
video {
width: 100%;
height: 40vw;
}
footer {
margin: 0.5rem 0;
}
@media screen and (max-width: 768px) {
video {
height: auto;
}
#video_container {
width: 100% !important;
}
}

View File

@@ -0,0 +1,16 @@
import { Innertube } from 'youtubei.js';
(async () => {
const yt = await Innertube.create({ generate_session_locally: true });
const info = await yt.getInfo('hePb00CqvP0');
const defaultTranscriptInfo = await info.getTranscript();
console.log(`Got ${defaultTranscriptInfo.selectedLanguage} transcript with ${defaultTranscriptInfo.transcript.content.body.initial_segments.length} lines.`);
console.log("Fetching Hebrew transcript...");
const heTranscriptInfo = await defaultTranscriptInfo.selectLanguage('Hebrew');
console.log(`Got ${heTranscriptInfo.selectedLanguage} transcript with ${heTranscriptInfo.transcript.content.body.initial_segments.length} lines.`);
})();

5971
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "5.2.0",
"version": "6.4.1",
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
"type": "module",
"types": "./dist/src/platform/lib.d.ts",
@@ -56,7 +56,8 @@
"Wykerd (https://github.com/wykerd/)",
"MasterOfBob777 (https://github.com/MasterOfBob777)",
"patrickkfkan (https://github.com/patrickkfkan)",
"akkadaska (https://github.com/akkadaska)"
"akkadaska (https://github.com/akkadaska)",
"Absidue (https://github.com/absidue)"
],
"directories": {
"test": "./test",
@@ -68,10 +69,10 @@
"lint": "npx eslint ./src",
"lint:fix": "npx eslint --fix ./src",
"build": "npm run build:parser-map && npm run build:proto && npm run build:esm && npm run bundle:node && npm run bundle:browser && npm run bundle:browser:prod",
"build:parser-map": "node ./scripts/build-parser-map.cjs",
"build:parser-map": "node ./scripts/gen-parser-map.mjs",
"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",
"build:deno": "npx cpy ./src ./deno && npx esbuild ./src/utils/DashManifest.tsx --keep-names --format=esm --platform=neutral --target=es2020 --outfile=./deno/src/utils/DashManifest.js && npx cpy ./package.json ./deno && npx replace \".js';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'./DashManifest.ts';\" \"'./DashManifest.js';\" ./deno -r && npx replace \"'jintr';\" \"'https://esm.sh/jintr';\" ./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 --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 --conditions=module --outfile=./bundle/browser.js --platform=browser",
"bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify",
@@ -84,12 +85,12 @@
},
"license": "MIT",
"dependencies": {
"jintr": "^1.0.0",
"linkedom": "^0.14.12",
"jintr": "^1.1.0",
"tslib": "^2.5.0",
"undici": "^5.19.1"
},
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/jest": "^28.1.7",
"@types/node": "^17.0.45",
"@typescript-eslint/eslint-plugin": "^5.30.6",

View File

@@ -1,10 +1,13 @@
const glob = require('glob');
const fs = require('fs');
const path = require('path');
import glob from "glob";
import path from 'path';
import fs from 'fs';
import url from 'url';
const import_list = [];
const misc_imports = [];
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
glob.sync('../src/parser/classes/**/*.{js,ts}', { cwd: __dirname })
.forEach((file) => {
// Trim path

View File

@@ -1,52 +0,0 @@
import { fetch } from 'undici';
import { gunzip } from 'zlib';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { writeFile } from 'fs/promises';
(async () => {
const __dirname = dirname(fileURLToPath(import.meta.url));
const buf = await (await fetch('https://github.com/intoli/user-agents/blob/master/src/user-agents.json.gz?raw=true')).arrayBuffer();
const bytes = new Uint8Array(buf);
// Only get desktop and mobile agents
const allowed_agents = new Set([
'desktop',
'mobile'
]);
const decompressed = await new Promise((resolve, reject) => {
gunzip(bytes, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result.buffer);
}
});
});
const contents = new TextDecoder().decode(decompressed);
const agents = JSON.parse(contents);
if (!Array.isArray(agents)) {
throw new Error('Invalid user-agents.json');
}
const agentsByDevice = agents.reduce((acc, agent) => {
const device = agent.deviceCategory;
if (!allowed_agents.has(device))
return acc;
if (!acc[device]) {
acc[device] = [];
}
// We dont want to massive of a list of agents for each device
if (acc[device].length <= 25) acc[device].push(agent.userAgent);
return acc;
}, {});
await writeFile(resolve(__dirname, '..', 'src', 'utils', 'user-agents.ts'), `/* eslint-disable */\n/* Generated file do not edit */\nexport default ${JSON.stringify(agentsByDevice, null, 2)} as { desktop: string[], mobile: string[] };`);
})();

48
scripts/get-agents.mjs Normal file
View File

@@ -0,0 +1,48 @@
import { fetch } from 'undici';
import { gunzip } from 'zlib';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { writeFile } from 'fs/promises';
const __dirname = dirname(fileURLToPath(import.meta.url));
const buf = await (await fetch('https://github.com/intoli/user-agents/blob/master/src/user-agents.json.gz?raw=true')).arrayBuffer();
const bytes = new Uint8Array(buf);
// Only get desktop and mobile agents
const allowed_agents = new Set([
'desktop',
'mobile'
]);
const decompressed = await new Promise((resolve, reject) => {
gunzip(bytes, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result.buffer);
}
});
});
const contents = new TextDecoder().decode(decompressed);
const agents = JSON.parse(contents);
if (!Array.isArray(agents)) {
throw new Error('Invalid user-agents.json');
}
const agentsByDevice = agents.reduce((acc, agent) => {
const device = agent.deviceCategory;
if (!allowed_agents.has(device))
return acc;
if (!acc[device]) {
acc[device] = [];
}
// We dont want to massive of a list of agents for each device
if (acc[device].length <= 25) acc[device].push(agent.userAgent);
return acc;
}, {});
await writeFile(resolve(__dirname, '..', 'src', 'utils', 'user-agents.ts'), `/* eslint-disable */\n/* Generated file do not edit */\nexport default ${JSON.stringify(agentsByDevice, null, 2)} as { desktop: string[], mobile: string[] };`);

View File

@@ -19,7 +19,7 @@ import { Kids, Music, Studio } from './core/clients/index.js';
import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js';
import { Feed, TabbedFeed } from './core/mixins/index.js';
import Proto from './proto/index.js';
import * as Proto from './proto/index.js';
import * as Constants from './utils/Constants.js';
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.js';
@@ -36,13 +36,13 @@ import {
import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.js';
import type { ApiResponse } from './core/Actions.js';
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js';
import { type IBrowseResponse, type IParsedResponse } from './parser/types/index.js';
import type { INextRequest } from './types/index.js';
import type { DownloadOptions, FormatOptions } from './utils/FormatUtils.js';
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.js';
export type InnertubeConfig = SessionOptions;
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS'
export type InnerTubeClient = 'WEB' | 'iOS' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS';
export type SearchFilters = Partial<{
upload_date: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';

View File

@@ -10,6 +10,7 @@ import type { FetchFunction } from '../types/PlatformShim.js';
*/
export default class Player {
#nsig_sc;
#nsig_cache;
#sig_sc;
#sig_sc_timestamp;
#player_id;
@@ -21,6 +22,8 @@ export default class Player {
this.#sig_sc_timestamp = signature_timestamp;
this.#player_id = player_id;
this.#nsig_cache = new Map<string, string>();
}
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise<Player> {
@@ -66,7 +69,7 @@ export default class Player {
return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id);
}
decipher(url?: string, signature_cipher?: string, cipher?: string): string {
decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map<string, string>): string {
url = url || signature_cipher || cipher;
if (!url)
@@ -93,15 +96,23 @@ export default class Player {
const n = url_components.searchParams.get('n');
if (n) {
const nsig = Platform.shim.eval(this.#nsig_sc, {
nsig: n
});
let nsig;
if (typeof nsig !== 'string')
throw new PlayerError('Failed to decipher nsig');
if (this_response_nsig_cache && this_response_nsig_cache.has(n)) {
nsig = this_response_nsig_cache.get(n) as string;
} else {
nsig = Platform.shim.eval(this.#nsig_sc, {
nsig: n
});
if (nsig.startsWith('enhanced_except_')) {
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
if (typeof nsig !== 'string')
throw new PlayerError('Failed to decipher nsig');
if (nsig.startsWith('enhanced_except_')) {
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
} else if (this_response_nsig_cache) {
this_response_nsig_cache.set(n, nsig);
}
}
url_components.searchParams.set('n', nsig);

View File

@@ -3,7 +3,7 @@ import EventEmitterLike from '../utils/EventEmitterLike.js';
import Actions from './Actions.js';
import Player from './Player.js';
import Proto from '../proto/index.js';
import * as Proto from '../proto/index.js';
import type { ICache } from '../types/Cache.js';
import type { FetchFunction } from '../types/PlatformShim.js';
import HTTPClient from '../utils/HTTPClient.js';
@@ -16,6 +16,7 @@ export enum ClientType {
WEB = 'WEB',
KIDS = 'WEB_KIDS',
MUSIC = 'WEB_REMIX',
IOS = 'iOS',
ANDROID = 'ANDROID',
ANDROID_MUSIC = 'ANDROID_MUSIC',
ANDROID_CREATOR = 'ANDROID_CREATOR',
@@ -62,6 +63,7 @@ export interface Context {
user: {
enableSafetyMode: boolean;
lockedSafetyMode: boolean;
onBehalfOfUser?: string;
};
thirdParty?: {
embedUrl: string;
@@ -83,6 +85,10 @@ export interface SessionOptions {
* Only works if you are signed in with cookies.
*/
account_index?: number;
/**
* Specify the Page ID of the YouTube profile/channel to use, if the logged-in account has multiple profiles.
*/
on_behalf_of_user?: string;
/**
* Specifies whether to retrieve the JS player. Disabling this will make session creation faster.
* **NOTE:** Deciphering formats is not possible without the JS player.
@@ -192,7 +198,8 @@ export default class Session extends EventEmitterLike {
options.device_category,
options.client_type,
options.timezone,
options.fetch
options.fetch,
options.on_behalf_of_user
);
return new Session(
@@ -212,14 +219,22 @@ export default class Session extends EventEmitterLike {
device_category: DeviceCategory = 'desktop',
client_name: ClientType = ClientType.WEB,
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
fetch: FetchFunction = Platform.shim.fetch
fetch: FetchFunction = Platform.shim.fetch,
on_behalf_of_user?: string
) {
let session_data: SessionData;
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user };
if (generate_session_locally) {
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data });
session_data = this.#generateSessionData(session_args);
} else {
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data }, fetch);
try {
// This can fail if the data changes or the request is blocked for some reason.
session_data = await this.#retrieveSessionData(session_args, fetch);
} catch (err) {
session_data = this.#generateSessionData(session_args);
}
}
return { ...session_data, account_index };
@@ -233,6 +248,7 @@ export default class Session extends EventEmitterLike {
client_name: string;
enable_safety_mode: boolean;
visitor_data: string;
on_behalf_of_user?: string;
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
@@ -292,7 +308,8 @@ export default class Session extends EventEmitterLike {
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
lockedSafetyMode: false,
onBehalfOfUser: options.on_behalf_of_user
}
};
@@ -307,6 +324,7 @@ export default class Session extends EventEmitterLike {
client_name: string;
enable_safety_mode: boolean;
visitor_data: string;
on_behalf_of_user?: string;
}): SessionData {
let visitor_id = generateRandomString(11);
@@ -339,7 +357,8 @@ export default class Session extends EventEmitterLike {
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
lockedSafetyMode: false,
onBehalfOfUser: options.on_behalf_of_user
}
};

View File

@@ -18,7 +18,7 @@ import PlaylistPanel from '../../parser/classes/PlaylistPanel.js';
import SearchSuggestionsSection from '../../parser/classes/SearchSuggestionsSection.js';
import SectionList from '../../parser/classes/SectionList.js';
import Tab from '../../parser/classes/Tab.js';
import Proto from '../../proto/index.js';
import * as Proto from '../../proto/index.js';
import type { ObservedArray, YTNode } from '../../parser/helpers.js';
import type { MusicSearchFilters } from '../../types/index.js';
@@ -329,7 +329,7 @@ export default class Music {
if (!page.contents)
throw new InnertubeError('Unexpected response', page);
if (page.contents.item().key('type').string() === 'Message')
if (page.contents.item().type === 'Message')
throw new InnertubeError(page.contents.item().as(Message).text.toString(), video_id);
const section_list = page.contents.item().as(SectionList).contents;

View File

@@ -1,4 +1,4 @@
import Proto from '../../proto/index.js';
import * as Proto from '../../proto/index.js';
import * as Constants from '../../utils/Constants.js';
import { InnertubeError, MissingParamError, Platform } from '../../utils/Utils.js';

View File

@@ -8,6 +8,11 @@ export const PATH = '/player';
* @returns The payload.
*/
export function build(opts: PlayerEndpointOptions): IPlayerRequest {
const is_android =
opts.client === 'ANDROID' ||
opts.client === 'YTMUSIC_ANDROID' ||
opts.client === 'YTSTUDIO_ANDROID';
return {
playbackContext: {
contentPlaybackContext: {
@@ -37,8 +42,8 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
...{
client: opts.client,
playlistId: opts.playlist_id,
// Workaround streaming URLs returning 403 when using Android clients and throttling in web clients.
params: '2AMBCgIQBg'
// Workaround streaming URLs returning 403 or getting throttled when using Android based clients.
params: is_android ? '2AMBCgIQBg' : opts.params
}
};
}

View File

@@ -15,7 +15,9 @@ export function build(opts: EditPlaylistEndpointOptions): IEditPlaylistRequest {
...{
addedVideoId: action.added_video_id,
setVideoId: action.set_video_id,
movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor
movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor,
playlistDescription: action.playlist_description,
playlistName: action.playlist_name
}
}))
};

View File

@@ -3,7 +3,7 @@ import Analytics from '../../parser/youtube/Analytics.js';
import Settings from '../../parser/youtube/Settings.js';
import TimeWatched from '../../parser/youtube/TimeWatched.js';
import Proto from '../../proto/index.js';
import * as Proto from '../../proto/index.js';
import { InnertubeError } from '../../utils/Utils.js';
import { Account, BrowseEndpoint, Channel } from '../endpoints/index.js';

View File

@@ -1,4 +1,4 @@
import Proto from '../../proto/index.js';
import * as Proto from '../../proto/index.js';
import type Actions from '../Actions.js';
import type { ApiResponse } from '../Actions.js';

View File

@@ -200,4 +200,60 @@ export default class PlaylistManager {
action_result: response.data.actions // TODO: implement actions in the parser
};
}
}
/**
* Sets the name (title) for the given playlist.
* @param playlist_id - The playlist ID.
* @param name - The name / title to use for the playlist.
*/
async setName(playlist_id: string, name: string): Promise<{ playlist_id: string; action_result: any; }> {
throwIfMissing({ playlist_id, name });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
payload.actions.push({
action: 'ACTION_SET_PLAYLIST_NAME',
playlist_name: name
});
const response = await this.#actions.execute(
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
);
return {
playlist_id,
action_result: response.data.actions
};
}
/**
* Sets the description for the given playlist.
* @param playlist_id - The playlist ID.
* @param description - The description to use for the playlist.
*/
async setDescription(playlist_id: string, description: string): Promise<{ playlist_id: string; action_result: any; }> {
throwIfMissing({ playlist_id, description });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
payload.actions.push({
action: 'ACTION_SET_PLAYLIST_DESCRIPTION',
playlist_description: description
});
const response = await this.#actions.execute(
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
);
return {
playlist_id,
action_result: response.data.actions
};
}
}

View File

@@ -185,10 +185,8 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
*/
async getContinuationData(): Promise<T | undefined> {
if (this.#continuation) {
if (this.#continuation.length > 1)
throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page');
if (this.#continuation.length === 0)
throw new InnertubeError('There are no continuations');
throw new InnertubeError('There are no continuations.');
const response = await this.#continuation[0].endpoint.call<T>(this.#actions, { parse: true });

View File

@@ -1,12 +1,17 @@
import type { ApiResponse } from '../Actions.js';
import type Actions from '../Actions.js';
import * as Constants from '../../utils/Constants.js';
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.js';
import FormatUtils from '../../utils/FormatUtils.js';
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/FormatUtils.js';
import * as FormatUtils from '../../utils/FormatUtils.js';
import { InnertubeError } from '../../utils/Utils.js';
import type Format from '../../parser/classes/misc/Format.js';
import type { INextResponse, IPlayerResponse } from '../../parser/index.js';
import Parser from '../../parser/index.js';
import type { DashOptions } from '../../types/DashOptions.js';
import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.js';
import { getStreamingInfo } from '../../utils/StreamingInfo.js';
import ContinuationItem from '../../parser/classes/ContinuationItem.js';
import TranscriptInfo from '../../parser/youtube/TranscriptInfo.js';
export default class MediaInfo {
#page: [IPlayerResponse, INextResponse?];
@@ -37,10 +42,32 @@ export default class MediaInfo {
* Generates a DASH manifest from the streaming data.
* @param url_transformer - Function to transform the URLs.
* @param format_filter - Function to filter the formats.
* @param options - Additional options to customise the manifest generation
* @returns DASH manifest
*/
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter): Promise<string> {
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions);
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise<string> {
let storyboards;
if (options.include_thumbnails && this.#page[0].storyboards?.is(PlayerStoryboardSpec)) {
storyboards = this.#page[0].storyboards;
}
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards);
}
/**
* Get a cleaned up representation of the adaptive_formats
*/
getStreamingInfo(url_transformer?: URLTransformer, format_filter?: FormatFilter) {
return getStreamingInfo(
this.streaming_data,
url_transformer,
format_filter,
this.cpn,
this.#actions.session.player,
this.#actions,
this.#page[0].storyboards?.is(PlayerStoryboardSpec) ? this.#page[0].storyboards : undefined
);
}
/**
@@ -59,6 +86,36 @@ export default class MediaInfo {
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
}
/**
* Retrieves the video's transcript.
* @param video_id - The video id.
*/
async getTranscript(): Promise<TranscriptInfo> {
const next_response = this.page[1];
if (!next_response)
throw new InnertubeError('Cannot get transcript from basic video info.');
if (!next_response.engagement_panels)
throw new InnertubeError('Engagement panels not found. Video likely has no transcript.');
const transcript_panel = next_response.engagement_panels.get({
panel_identifier: 'engagement-panel-searchable-transcript'
});
if (!transcript_panel)
throw new InnertubeError('Transcript panel not found. Video likely has no transcript.');
const transcript_continuation = transcript_panel.content?.as(ContinuationItem);
if (!transcript_continuation)
throw new InnertubeError('Transcript continuation not found.');
const response = await transcript_continuation.endpoint.call(this.actions);
return new TranscriptInfo(this.actions, response);
}
/**
* Adds video to the watch history.
*/

View File

@@ -310,7 +310,7 @@ const example_data = {
// The first argument is the name of the class, the second is the data you have for the node.
// It will return a class that extends YTNode.
const Example = Generator.YTNodeGenerator.generateRuntimeClass('Example', example_data);
const Example = Generator.generateRuntimeClass('Example', example_data);
// You may now use this class as you would any other node.
const example = new Example(example_data);

View File

@@ -0,0 +1,19 @@
import Button from './Button.js';
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
export default class AlertWithButton extends YTNode {
static type = 'AlertWithButton';
text: Text;
alert_type: string;
dismiss_button: Button | null;
constructor(data: RawNode) {
super();
this.text = new Text(data.text);
this.alert_type = data.type;
this.dismiss_button = Parser.parseItem(data.dismissButton, Button);
}
}

View File

@@ -1,5 +1,6 @@
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import Button from './Button.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import CommentActionButtons from './comments/CommentActionButtons.js';
import Menu from './menus/Menu.js';
@@ -18,7 +19,7 @@ export default class BackstagePost extends YTNode {
vote_count?: Text;
menu?: Menu | null;
action_buttons?: CommentActionButtons | null;
vote_button?: CommentActionButtons | null;
vote_button?: Button | null;
surface: string;
endpoint?: NavigationEndpoint;
attachment;
@@ -56,7 +57,7 @@ export default class BackstagePost extends YTNode {
}
if (Reflect.has(data, 'voteButton')) {
this.vote_button = Parser.parseItem(data.voteButton, CommentActionButtons);
this.vote_button = Parser.parseItem(data.voteButton, Button);
}
if (Reflect.has(data, 'navigationEndpoint')) {

View File

@@ -15,26 +15,20 @@ export default class Button extends YTNode {
constructor(data: RawNode) {
super();
if (Reflect.has(data, 'text')) {
if (Reflect.has(data, 'text'))
this.text = new Text(data.text).toString();
}
if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'label')) {
if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'label'))
this.label = data.accessibility.label;
}
if (Reflect.has(data, 'tooltip')) {
if (Reflect.has(data, 'tooltip'))
this.tooltip = data.tooltip;
}
if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType')) {
if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType'))
this.icon_type = data.icon.iconType;
}
if (Reflect.has(data, 'isDisabled')) {
if (Reflect.has(data, 'isDisabled'))
this.is_disabled = data.isDisabled;
}
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command);
}

View File

@@ -2,6 +2,7 @@ import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import Button from './Button.js';
import ChannelHeaderLinks from './ChannelHeaderLinks.js';
import ChannelHeaderLinksView from './ChannelHeaderLinksView.js';
import SubscribeButton from './SubscribeButton.js';
import Author from './misc/Author.js';
import Text from './misc/Text.js';
@@ -18,7 +19,7 @@ export default class C4TabbedHeader extends YTNode {
videos_count?: Text;
sponsor_button?: Button | null;
subscribe_button?: SubscribeButton | Button | null;
header_links?: ChannelHeaderLinks | null;
header_links?: ChannelHeaderLinks | ChannelHeaderLinksView | null;
channel_handle?: Text;
channel_id?: string;
@@ -58,7 +59,7 @@ export default class C4TabbedHeader extends YTNode {
}
if (Reflect.has(data, 'headerLinks')) {
this.header_links = Parser.parseItem(data.headerLinks, ChannelHeaderLinks);
this.header_links = Parser.parseItem(data.headerLinks, [ ChannelHeaderLinks, ChannelHeaderLinksView ]);
}
if (Reflect.has(data, 'channelHandleText')) {

View File

@@ -7,14 +7,11 @@ export default class CarouselLockup extends YTNode {
static type = 'CarouselLockup';
info_rows: ObservedArray<InfoRow>;
video_lockup?: CompactVideo;
video_lockup?: CompactVideo | null;
constructor(data: RawNode) {
super();
this.info_rows = Parser.parseArray(data.infoRows, InfoRow);
const video_lockup = Parser.parseItem(data.videoLockup, CompactVideo);
if (video_lockup != null) {
this.video_lockup = video_lockup;
}
this.video_lockup = Parser.parseItem(data.videoLockup, CompactVideo);
}
}

View File

@@ -0,0 +1,22 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Text from './misc/Text.js';
export default class ChannelHeaderLinksView extends YTNode {
static type = 'ChannelHeaderLinksView';
first_link?: Text;
more?: Text;
constructor(data: RawNode) {
super();
if (Reflect.has(data, 'firstLink')) {
this.first_link = Text.fromAttributed(data.firstLink);
}
if (Reflect.has(data, 'more')) {
this.more = Text.fromAttributed(data.more);
}
}
}

View File

@@ -14,6 +14,7 @@ export default class ChannelMetadata extends YTNode {
is_family_safe: boolean;
keywords: string[];
avatar: Thumbnail[];
music_artist_name?: string;
available_countries: string[];
android_deep_link: string;
android_appindexing_link: string;
@@ -30,6 +31,8 @@ export default class ChannelMetadata extends YTNode {
this.is_family_safe = data.isFamilySafe;
this.keywords = data.keywords;
this.avatar = Thumbnail.fromResponse(data.avatar);
// Can be an empty string sometimes, so we need the extra length check
this.music_artist_name = typeof data.musicArtistName === 'string' && data.musicArtistName.length > 0 ? data.musicArtistName : undefined;
this.available_countries = data.availableCountryCodes;
this.android_deep_link = data.androidDeepLink;
this.android_appindexing_link = data.androidAppindexingLink;

View File

@@ -0,0 +1,57 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import Author from './misc/Author.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import Menu from './menus/Menu.js';
import { timeToSeconds } from '../../utils/Utils.js';
export default class CompactMovie extends YTNode {
static type = 'CompactMovie';
id: string;
title: Text;
top_metadata_items: Text;
thumbnails: Thumbnail[];
thumbnail_overlays: ObservedArray<YTNode>;
author: Author;
duration: {
text: string;
seconds: number;
};
endpoint: NavigationEndpoint;
badges: ObservedArray<YTNode>;
use_vertical_poster: boolean;
menu: Menu | null;
constructor(data: RawNode) {
super();
const overlay_time_status = data.thumbnailOverlays
.find((overlay: RawNode) => overlay.thumbnailOverlayTimeStatusRenderer)
?.thumbnailOverlayTimeStatusRenderer.text || 'N/A';
this.id = data.videoId;
this.title = new Text(data.title);
this.top_metadata_items = new Text(data.topMetadataItems);
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
this.author = new Author(data.shortBylineText);
const durationText = data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString();
this.duration = {
text: durationText,
seconds: timeToSeconds(durationText)
};
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.badges = Parser.parseArray(data.badges);
this.use_vertical_poster = data.useVerticalPoster;
this.menu = Parser.parseItem(data.menu, Menu);
}
}

View File

@@ -0,0 +1,16 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Thumbnail from './misc/Thumbnail.js';
export default class ContentPreviewImageView extends YTNode {
static type = 'ContentPreviewImageView';
image: Thumbnail[];
style: string;
constructor(data: RawNode) {
super();
this.image = data.image.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width);
this.style = data.style;
}
}

View File

@@ -0,0 +1,13 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
export default class DynamicTextView extends YTNode {
static type = 'DynamicTextView';
text: string;
constructor(data: RawNode) {
super();
this.text = data.text.content;
}
}

View File

@@ -1,20 +1,27 @@
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import ContinuationItem from './ContinuationItem.js';
import EngagementPanelTitleHeader from './EngagementPanelTitleHeader.js';
import MacroMarkersList from './MacroMarkersList.js';
import ProductList from './ProductList.js';
import SectionList from './SectionList.js';
import StructuredDescriptionContent from './StructuredDescriptionContent.js';
export default class EngagementPanelSectionList extends YTNode {
static type = 'EngagementPanelSectionList';
target_id: String;
content?: SectionList|ContinuationItem|StructuredDescriptionContent;
header: EngagementPanelTitleHeader | null;
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | ProductList | null;
target_id?: string;
panel_identifier?: string;
visibility?: string;
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader);
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList, ProductList ]);
this.panel_identifier = data.panelIdentifier;
this.target_id = data.targetId;
const content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent ]);
if (content !== null) {
this.content = content;
}
this.visibility = data.visibility;
}
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import Text from './misc/Text.js';
import Button from './Button.js';
export default class EngagementPanelTitleHeader extends YTNode {
static type = 'EngagementPanelTitleHeader';
title: Text;
visibility_button: Button | null;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.visibility_button = Parser.parseItem(data.visibilityButton, Button);
}
}

View File

@@ -2,6 +2,7 @@ import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import Button from './Button.js';
import HorizontalCardList from './HorizontalCardList.js';
import HorizontalList from './HorizontalList.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
@@ -15,7 +16,7 @@ export default class ExpandableMetadata extends YTNode {
expanded_title: Text;
};
expanded_content: HorizontalCardList | null;
expanded_content: HorizontalCardList | HorizontalList | null;
expand_button: Button | null;
collapse_button: Button | null;
@@ -31,7 +32,7 @@ export default class ExpandableMetadata extends YTNode {
};
}
this.expanded_content = Parser.parseItem(data.expandedContent, HorizontalCardList);
this.expanded_content = Parser.parseItem(data.expandedContent, [ HorizontalCardList, HorizontalList ]);
this.expand_button = Parser.parseItem(data.expandButton, Button);
this.collapse_button = Parser.parseItem(data.collapseButton, Button);
}

View File

@@ -1,22 +1,22 @@
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
import { Text } from '../misc.js';
import type { RawNode } from '../index.js';
export default class ExpandableVideoDescriptionBody extends YTNode {
static type = 'ExpandableVideoDescriptionBody';
show_more_text: Text;
show_less_text: Text;
attributed_description_body_text: {
content: String
};
attributed_description_body_text?: string;
constructor(data: RawNode) {
super();
this.show_more_text = new Text(data.showMoreText);
this.show_less_text = new Text(data.showLessText);
this.attributed_description_body_text = {
content: data.attributedDescriptionBodyText.content
};
if (Reflect.has(data, 'attributedDescriptionBodyText')) {
this.attributed_description_body_text = data.attributedDescriptionBodyText?.content;
}
}
}

View File

@@ -4,6 +4,7 @@ import { Text } from '../misc.js';
export default class Factoid extends YTNode {
static type = 'Factoid';
label: Text;
value: Text;
accessibility_text: String;

View File

@@ -0,0 +1,16 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import { Text } from '../misc.js';
export default class FancyDismissibleDialog extends YTNode {
static type = 'FancyDismissibleDialog';
dialog_message: Text;
confirm_label: Text;
constructor(data: RawNode) {
super();
this.dialog_message = new Text(data.dialogMessage);
this.confirm_label = new Text(data.confirmLabel);
}
}

View File

@@ -1,6 +1,6 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../parser.js';
import * as Parser from '../parser.js';
import Author from './misc/Author.js';
import Text from './misc/Text.js';
import NavigationEndpoint from './NavigationEndpoint.js';

View File

@@ -1,4 +1,4 @@
import Parser from '../parser.js';
import * as Parser from '../parser.js';
import GuideEntry from './GuideEntry.js';
import type { RawNode } from '../index.js';
import { type ObservedArray, YTNode } from '../helpers.js';

View File

@@ -1,4 +1,4 @@
import Parser from '../parser.js';
import * as Parser from '../parser.js';
import type { RawNode } from '../index.js';
import { type ObservedArray, YTNode } from '../helpers.js';

View File

@@ -1,5 +1,5 @@
import Text from './misc/Text.js';
import Parser from '../parser.js';
import * as Parser from '../parser.js';
import { type ObservedArray, YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';

View File

@@ -0,0 +1,28 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import { Thumbnail } from '../misc.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
export default class HashtagTile extends YTNode {
static type = 'HashtagTile';
hashtag: Text;
hashtag_info_text: Text;
hashtag_thumbnail: Thumbnail[];
endpoint: NavigationEndpoint;
hashtag_background_color: number;
hashtag_video_count: Text;
hashtag_channel_count: Text;
constructor(data: RawNode) {
super();
this.hashtag = new Text(data.hashtag);
this.hashtag_info_text = new Text(data.hashtagInfoText);
this.hashtag_thumbnail = Thumbnail.fromResponse(data.hashtagThumbnail);
this.endpoint = new NavigationEndpoint(data.onTapCommand);
this.hashtag_background_color = data.hashtagBackgroundColor;
this.hashtag_video_count = new Text(data.hashtagVideoCount);
this.hashtag_channel_count = new Text(data.hashtagChannelCount);
}
}

View File

@@ -0,0 +1,25 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
export default class IncludingResultsFor extends YTNode {
static type = 'IncludingResultsFor';
including_results_for: Text;
corrected_query: Text;
corrected_query_endpoint: NavigationEndpoint;
search_only_for?: Text;
original_query?: Text;
original_query_endpoint?: NavigationEndpoint;
constructor(data: RawNode) {
super();
this.including_results_for = new Text(data.includingResultsFor);
this.corrected_query = new Text(data.correctedQuery);
this.corrected_query_endpoint = new NavigationEndpoint(data.correctedQueryEndpoint);
this.search_only_for = Reflect.has(data, 'searchOnlyFor') ? new Text(data.searchOnlyFor) : undefined;
this.original_query = Reflect.has(data, 'originalQuery') ? new Text(data.originalQuery) : undefined;
this.original_query_endpoint = Reflect.has(data, 'originalQueryEndpoint') ? new NavigationEndpoint(data.originalQueryEndpoint) : undefined;
}
}

View File

@@ -1,38 +1,29 @@
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import { Text } from '../misc.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import type { RawNode } from '../index.js';
export default class InfoRow extends YTNode {
static type = 'InfoRow';
metadata_text?: Text;
metadata_endpoint?: NavigationEndpoint;
info_row_expand_status_key: String;
title: Text;
default_metadata?: Text;
expanded_metadata?: Text;
info_row_expand_status_key?: String;
constructor(data: RawNode) {
super();
if ('defaultMetadata' in data && 'runs' in data.defaultMetadata) {
const runs = data.defaultMetadata.runs;
if (runs.length > 0) {
const run = runs[0];
this.metadata_text = run?.text;
if ('navigationEndpoint' in run) {
this.metadata_endpoint = Parser.parseItem({ navigationEndpoint: run.navigationEndpoint }, NavigationEndpoint) || undefined;
}
}
}
if ('expandedMetadata' in data && 'runs' in data.expandedMetadata) {
this.metadata_text = new Text(data.expandedMetadata);
}
if (this.metadata_text === undefined) {
this.metadata_text = data.expandedMetadata?.simpleText
? new Text(data.expandedMetadata)
: data.defaultMetadata?.simpleText
? new Text(data.defaultMetadata)
: undefined;
}
this.info_row_expand_status_key = data.infoRowExpandStatusKey;
this.title = new Text(data.title);
if (Reflect.has(data, 'defaultMetadata')) {
this.default_metadata = new Text(data.defaultMetadata);
}
if (Reflect.has(data, 'expandedMetadata')) {
this.expanded_metadata = new Text(data.expandedMetadata);
}
if (Reflect.has(data, 'infoRowExpandStatusKey')) {
this.info_row_expand_status_key = data.infoRowExpandStatusKey;
}
}
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import Menu from './menus/Menu.js';
import Text from './misc/Text.js';
export default class MacroMarkersInfoItem extends YTNode {
static type = 'MacroMarkersInfoItem';
info_text: Text;
menu: Menu | null;
constructor(data: RawNode) {
super();
this.info_text = new Text(data.infoText);
this.menu = Parser.parseItem(data.menu, Menu);
}
}

View File

@@ -0,0 +1,18 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import { Text } from '../misc.js';
import MacroMarkersInfoItem from './MacroMarkersInfoItem.js';
import MacroMarkersListItem from './MacroMarkersListItem.js';
export default class MacroMarkersList extends YTNode {
static type = 'MacroMarkersList';
contents: ObservedArray<MacroMarkersInfoItem | MacroMarkersListItem>;
sync_button_label: Text;
constructor(data: RawNode) {
super();
this.contents = Parser.parseArray(data.contents, [ MacroMarkersInfoItem, MacroMarkersListItem ]);
this.sync_button_label = new Text(data.syncButtonLabel);
}
}

View File

@@ -2,6 +2,7 @@ import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import MusicCarouselShelfBasicHeader from './MusicCarouselShelfBasicHeader.js';
import MusicMultiRowListItem from './MusicMultiRowListItem.js';
import MusicNavigationButton from './MusicNavigationButton.js';
import MusicResponsiveListItem from './MusicResponsiveListItem.js';
import MusicTwoRowItem from './MusicTwoRowItem.js';
@@ -10,13 +11,13 @@ export default class MusicCarouselShelf extends YTNode {
static type = 'MusicCarouselShelf';
header: MusicCarouselShelfBasicHeader | null;
contents: ObservedArray<MusicTwoRowItem | MusicResponsiveListItem | MusicNavigationButton>;
contents: ObservedArray<MusicTwoRowItem | MusicResponsiveListItem | MusicMultiRowListItem | MusicNavigationButton>;
num_items_per_column?: number;
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header, MusicCarouselShelfBasicHeader);
this.contents = Parser.parseArray(data.contents, [ MusicTwoRowItem, MusicResponsiveListItem, MusicNavigationButton ]);
this.contents = Parser.parseArray(data.contents, [ MusicTwoRowItem, MusicResponsiveListItem, MusicMultiRowListItem, MusicNavigationButton ]);
if (Reflect.has(data, 'numItemsPerColumn')) {
this.num_items_per_column = parseInt(data.numItemsPerColumn);

View File

@@ -0,0 +1,44 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import { Text } from '../misc.js';
import Menu from './menus/Menu.js';
import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay.js';
import MusicThumbnail from './MusicThumbnail.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class MusicMultiRowListItem extends YTNode {
static type = 'MusicMultiRowListItem';
thumbnail: MusicThumbnail | null;
overlay: MusicItemThumbnailOverlay | null;
on_tap: NavigationEndpoint;
menu: Menu | null;
subtitle: Text;
title: Text;
second_title?: Text;
description?: Text;
display_style?: string;
constructor(data: RawNode) {
super();
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
this.overlay = Parser.parseItem(data.overlay, MusicItemThumbnailOverlay);
this.on_tap = new NavigationEndpoint(data.onTap);
this.menu = Parser.parseItem(data.menu, Menu);
this.subtitle = new Text(data.subtitle);
this.title = new Text(data.title);
if (Reflect.has(data, 'secondTitle')) {
this.second_title = new Text(data.secondTitle);
}
if (Reflect.has(data, 'description')) {
this.description = new Text(data.description);
}
if (Reflect.has(data, 'displayStyle')) {
this.display_style = data.displayStyle;
}
}
}

View File

@@ -1,5 +1,10 @@
// TODO: Clean up and refactor this.
import { YTNode } from '../helpers.js';
import { isTextRun, timeToSeconds } from '../../utils/Utils.js';
import type { ObservedArray } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay.js';
import MusicResponsiveListItemFixedColumn from './MusicResponsiveListItemFixedColumn.js';
@@ -9,11 +14,6 @@ import NavigationEndpoint from './NavigationEndpoint.js';
import Menu from './menus/Menu.js';
import Text from './misc/Text.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';
export default class MusicResponsiveListItem extends YTNode {
static type = 'MusicResponsiveListItem';
@@ -24,8 +24,8 @@ export default class MusicResponsiveListItem extends YTNode {
playlist_set_video_id: string;
};
endpoint: NavigationEndpoint | null;
item_type: 'album' | 'playlist' | 'artist' | 'library_artist' | 'video' | 'song' | 'endpoint' | 'unknown' | undefined;
endpoint?: NavigationEndpoint;
item_type: 'album' | 'playlist' | 'artist' | 'library_artist' | 'non_music_track' | 'video' | 'song' | 'endpoint' | 'unknown' | undefined;
index?: Text;
thumbnail?: MusicThumbnail | null;
badges;
@@ -82,9 +82,21 @@ export default class MusicResponsiveListItem extends YTNode {
playlist_set_video_id: data?.playlistItemData?.playlistSetVideoId || null
};
this.endpoint = data.navigationEndpoint ? new NavigationEndpoint(data.navigationEndpoint) : null;
if (Reflect.has(data, 'navigationEndpoint')) {
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
}
const page_type = this.endpoint?.payload?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType;
let page_type = this.endpoint?.payload?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType;
if (!page_type) {
const is_non_music_track = this.flex_columns.find(
(col) => col.title.endpoint?.payload?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE'
);
if (is_non_music_track) {
page_type = 'MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE';
}
}
switch (page_type) {
case 'MUSIC_PAGE_TYPE_ALBUM':
@@ -104,27 +116,41 @@ export default class MusicResponsiveListItem extends YTNode {
this.item_type = 'library_artist';
this.#parseLibraryArtist();
break;
case 'MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE':
this.item_type = 'non_music_track';
this.#parseNonMusicTrack();
break;
default:
if (this.flex_columns[1]) {
this.#parseVideoOrSong();
} else {
this.#parseOther();
}
break;
}
if (data.index) {
if (Reflect.has(data, 'index')) {
this.index = new Text(data.index);
}
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
this.badges = Parser.parseArray(data.badges);
this.menu = Parser.parseItem(data.menu, Menu);
this.overlay = Parser.parseItem(data.overlay, MusicItemThumbnailOverlay);
if (Reflect.has(data, 'thumbnail')) {
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
}
if (Reflect.has(data, 'badges')) {
this.badges = Parser.parseArray(data.badges);
}
if (Reflect.has(data, 'menu')) {
this.menu = Parser.parseItem(data.menu, Menu);
}
if (Reflect.has(data, 'overlay')) {
this.overlay = Parser.parseItem(data.overlay, MusicItemThumbnailOverlay);
}
}
#parseOther() {
this.title = this.flex_columns.first().key('title').instanceof(Text).toString();
this.title = this.flex_columns.first().title.toString();
if (this.endpoint) {
this.item_type = 'endpoint';
@@ -134,7 +160,7 @@ export default class MusicResponsiveListItem extends YTNode {
}
#parseVideoOrSong() {
const is_video = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.some((run) => run.text.match(/(.*?) views/));
const is_video = this.flex_columns.at(1)?.title.runs?.some((run) => run.text.match(/(.*?) views/));
if (is_video) {
this.item_type = 'video';
this.#parseVideo();
@@ -146,10 +172,10 @@ export default class MusicResponsiveListItem extends YTNode {
#parseSong() {
this.id = this.#playlist_item_data.video_id || this.endpoint?.payload?.videoId;
this.title = this.flex_columns.first().key('title').instanceof(Text).toString();
this.title = this.flex_columns.first().title.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();
const duration_text = this.flex_columns.at(1)?.title.runs?.find(
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.title?.toString();
if (duration_text) {
this.duration = {
@@ -159,12 +185,12 @@ export default class MusicResponsiveListItem extends YTNode {
}
const album_run =
this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
this.flex_columns.at(1)?.title.runs?.find(
(run) =>
(isTextRun(run) && run.endpoint) &&
run.endpoint.payload.browseId.startsWith('MPR')
) ||
this.flex_columns.at(2)?.key('title').instanceof(Text).runs?.find(
this.flex_columns.at(2)?.title.runs?.find(
(run) =>
(isTextRun(run) && run.endpoint) &&
run.endpoint.payload.browseId.startsWith('MPR')
@@ -178,7 +204,7 @@ export default class MusicResponsiveListItem extends YTNode {
};
}
const artist_runs = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.filter(
const artist_runs = this.flex_columns.at(1)?.title.runs?.filter(
(run) => (isTextRun(run) && run.endpoint) && run.endpoint.payload.browseId.startsWith('UC')
);
@@ -193,10 +219,10 @@ export default class MusicResponsiveListItem extends YTNode {
#parseVideo() {
this.id = this.#playlist_item_data.video_id;
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();
this.title = this.flex_columns.first().title.toString();
this.views = this.flex_columns.at(1)?.title.runs?.find((run) => run.text.match(/(.*?) views/))?.toString();
const author_runs = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.filter(
const author_runs = this.flex_columns.at(1)?.title.runs?.filter(
(run) =>
(isTextRun(run) && run.endpoint) &&
run.endpoint.payload.browseId.startsWith('UC')
@@ -212,8 +238,8 @@ export default class MusicResponsiveListItem extends YTNode {
});
}
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;
const duration_text = this.flex_columns[1].title.runs?.find(
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.title.runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text;
if (duration_text) {
this.duration = {
@@ -225,22 +251,27 @@ export default class MusicResponsiveListItem extends YTNode {
#parseArtist() {
this.id = this.endpoint?.payload?.browseId;
this.name = this.flex_columns.first().key('title').instanceof(Text).toString();
this.subtitle = this.flex_columns.at(1)?.key('title').instanceof(Text);
this.name = this.flex_columns.first().title.toString();
this.subtitle = this.flex_columns.at(1)?.title;
this.subscribers = this.subtitle?.runs?.find((run) => (/^(\d*\.)?\d+[M|K]? subscribers?$/i).test(run.text))?.text || '';
}
#parseLibraryArtist() {
this.name = this.flex_columns.first().key('title').instanceof(Text).toString();
this.subtitle = this.flex_columns.at(1)?.key('title').instanceof(Text);
this.name = this.flex_columns.first().title.toString();
this.subtitle = this.flex_columns.at(1)?.title;
this.song_count = this.subtitle?.runs?.find((run) => (/^\d+(,\d+)? songs?$/i).test(run.text))?.text || '';
}
#parseNonMusicTrack() {
this.id = this.#playlist_item_data.video_id || this.endpoint?.payload?.videoId;
this.title = this.flex_columns.first().title.toString();
}
#parseAlbum() {
this.id = this.endpoint?.payload?.browseId;
this.title = this.flex_columns.first().title.toString();
const author_run = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
const author_run = this.flex_columns.at(1)?.title.runs?.find(
(run) =>
(isTextRun(run) && run.endpoint) &&
run.endpoint.payload.browseId.startsWith('UC')
@@ -254,7 +285,7 @@ export default class MusicResponsiveListItem extends YTNode {
};
}
this.year = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
this.year = this.flex_columns.at(1)?.title.runs?.find(
(run) => (/^[12][0-9]{3}$/).test(run.text)
)?.text;
}
@@ -263,12 +294,12 @@ export default class MusicResponsiveListItem extends YTNode {
this.id = this.endpoint?.payload?.browseId;
this.title = this.flex_columns.first().title.toString();
const item_count_run = this.flex_columns.at(1)?.key('title')
.instanceof(Text).runs?.find((run) => run.text.match(/\d+ (song|songs)/));
const item_count_run = this.flex_columns.at(1)?.title
.runs?.find((run) => run.text.match(/\d+ (song|songs)/));
this.item_count = item_count_run ? item_count_run.text : undefined;
const author_run = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
const author_run = this.flex_columns.at(1)?.title.runs?.find(
(run) =>
(isTextRun(run) && run.endpoint) &&
run.endpoint.payload.browseId.startsWith('UC')

View File

@@ -4,12 +4,14 @@ import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import type { IParsedResponse } from '../types/ParsedResponse.js';
import CreatePlaylistDialog from './CreatePlaylistDialog.js';
import OpenPopupAction from './actions/OpenPopupAction.js';
export default class NavigationEndpoint extends YTNode {
static type = 'NavigationEndpoint';
payload;
dialog?: CreatePlaylistDialog | YTNode | null;
open_popup?: OpenPopupAction | null;
metadata: {
url?: string;
@@ -24,6 +26,9 @@ export default class NavigationEndpoint extends YTNode {
if (Reflect.has(data || {}, 'innertubeCommand'))
data = data.innertubeCommand;
if (Reflect.has(data || {}, 'openPopupAction'))
this.open_popup = new OpenPopupAction(data.openPopupAction);
const name = Object.keys(data || {})
.find((item) =>
item.endsWith('Endpoint') ||
@@ -36,6 +41,7 @@ export default class NavigationEndpoint extends YTNode {
this.dialog = Parser.parseItem(this.payload.dialog || this.payload.content);
}
if (data?.serviceEndpoint) {
data = data.serviceEndpoint;
}
@@ -85,9 +91,9 @@ export default class NavigationEndpoint extends YTNode {
}
}
call<T extends IParsedResponse>(actions: Actions, args: { [ key: string ]: any; parse: true }): Promise<T>;
call(actions: Actions, args?: { [ key: string ]: any; parse?: false }): Promise<ApiResponse>;
call(actions: Actions, args?: { [ key: string ]: any; parse?: boolean }): Promise<IParsedResponse | ApiResponse> {
call<T extends IParsedResponse>(actions: Actions, args: { [key: string]: any; parse: true }): Promise<T>;
call(actions: Actions, args?: { [key: string]: any; parse?: false }): Promise<ApiResponse>;
call(actions: Actions, args?: { [key: string]: any; parse?: boolean }): Promise<IParsedResponse | ApiResponse> {
if (!actions)
throw new Error('An active caller must be provided');
if (!this.metadata.api_url)

View File

@@ -0,0 +1,16 @@
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import PageHeaderView from './PageHeaderView.js';
export default class PageHeader extends YTNode {
static type = 'PageHeader';
page_title: string;
content: PageHeaderView | null;
constructor(data: RawNode) {
super();
this.page_title = data.pageTitle;
this.content = Parser.parseItem(data.content, PageHeaderView);
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import ContentPreviewImageView from './ContentPreviewImageView.js';
import DynamicTextView from './DynamicTextView.js';
export default class PageHeaderView extends YTNode {
static type = 'PageHeaderView';
image: ContentPreviewImageView | null;
title: DynamicTextView | null;
constructor(data: RawNode) {
super();
this.image = Parser.parseItem(data.image, ContentPreviewImageView);
this.title = Parser.parseItem(data.title, DynamicTextView);
}
}

View File

@@ -17,10 +17,10 @@ export default class PlayerCaptionsTracklist extends YTNode {
audio_tracks?: {
audio_track_id: string;
captions_initial_state: string;
default_caption_track_index: number;
default_caption_track_index?: number;
has_default_track: boolean;
visibility: string;
caption_track_indices: number;
caption_track_indices: number[];
}[];
default_audio_track_index?: number;

View File

@@ -1,19 +1,21 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
export interface StoryboardData {
template_url: string;
thumbnail_width: number;
thumbnail_height: number;
thumbnail_count: number;
interval: number;
columns: number;
rows: number;
storyboard_count: number;
}
export default class PlayerStoryboardSpec extends YTNode {
static type = 'PlayerStoryboardSpec';
boards: {
template_url: string;
thumbnail_width: number;
thumbnail_height: number;
thumbnail_count: number;
interval: number;
columns: number;
rows: number;
storyboard_count: number;
};
boards: StoryboardData[];
constructor(data: RawNode) {
super();

View File

@@ -1,6 +1,7 @@
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import PlaylistCustomThumbnail from './PlaylistCustomThumbnail.js';
import PlaylistVideoThumbnail from './PlaylistVideoThumbnail.js';
import Author from './misc/Author.js';
import Text from './misc/Text.js';
@@ -13,7 +14,7 @@ export default class Playlist extends YTNode {
title: Text;
author: Text | Author;
thumbnails: Thumbnail[];
thumbnail_renderer?: PlaylistVideoThumbnail;
thumbnail_renderer?: PlaylistVideoThumbnail | PlaylistCustomThumbnail;
video_count: Text;
video_count_short: Text;
first_videos: ObservedArray<YTNode>;
@@ -44,7 +45,7 @@ export default class Playlist extends YTNode {
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
if (Reflect.has(data, 'thumbnailRenderer')) {
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, PlaylistVideoThumbnail) || undefined;
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, [ PlaylistVideoThumbnail, PlaylistCustomThumbnail ]) || undefined;
}
if (Reflect.has(data, 'viewPlaylistText')) {

View File

@@ -8,9 +8,10 @@ export default class PlaylistHeader extends YTNode {
id: string;
title: Text;
subtitle: Text | null;
stats: Text[];
brief_stats: Text[];
author: Author;
author: Author | null;
description: Text;
num_videos: Text;
view_count: Text;
@@ -27,9 +28,10 @@ export default class PlaylistHeader extends YTNode {
super();
this.id = data.playlistId;
this.title = new Text(data.title);
this.subtitle = data.subtitle ? new Text(data.subtitle) : null;
this.stats = data.stats.map((stat: RawNode) => new Text(stat));
this.brief_stats = data.briefStats.map((stat: RawNode) => new Text(stat));
this.author = new Author({ ...data.ownerText, navigationEndpoint: data.ownerEndpoint }, data.ownerBadges, null);
this.author = data.ownerText || data.ownerEndpoint ? new Author({ ...data.ownerText, navigationEndpoint: data.ownerEndpoint }, data.ownerBadges, null) : null;
this.description = new Text(data.descriptionText);
this.num_videos = new Text(data.numVideosText);
this.view_count = new Text(data.viewCountText);

View File

@@ -0,0 +1,15 @@
import type { ObservedArray} from '../helpers.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
export default class ProductList extends YTNode {
static type = 'ProductList';
contents: ObservedArray<YTNode>;
constructor(data: RawNode) {
super();
this.contents = Parser.parseArray(data.contents);
}
}

View File

@@ -0,0 +1,16 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import { Text } from '../misc.js';
export default class ProductListHeader extends YTNode {
static type = 'ProductListHeader';
title: Text;
suppress_padding_disclaimer: boolean;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.suppress_padding_disclaimer = !!data.suppressPaddingDisclaimer;
}
}

View File

@@ -0,0 +1,31 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import { Text, Thumbnail } from '../misc.js';
import Button from './Button.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class ProductListItem extends YTNode {
static type = 'ProductListItem';
title: Text;
accessibility_title: string;
thumbnail: Thumbnail[];
price: string;
endpoint: NavigationEndpoint;
merchant_name: string;
stay_in_app: boolean;
view_button: Button | null;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.accessibility_title = data.accessibilityTitle;
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
this.price = data.price;
this.endpoint = new NavigationEndpoint(data.onClickCommand);
this.merchant_name = data.merchantName;
this.stay_in_app = !!data.stayInApp;
this.view_button = Parser.parseItem(data.viewButton, Button);
}
}

View File

@@ -0,0 +1,25 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Text from './misc/Text.js';
export default class Quiz extends YTNode {
static type = 'Quiz';
choices: {
text: Text;
is_correct: boolean;
}[];
total_votes: Text;
constructor(data: RawNode) {
super();
this.choices = data.choices.map((choice: RawNode) => ({
text: new Text(choice.text),
is_correct: choice.isCorrect
}));
this.total_votes = new Text(data.totalVotes);
}
}

View File

@@ -0,0 +1,18 @@
import type { ObservedArray } from '../helpers.js';
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import SearchFilterGroup from './SearchFilterGroup.js';
import Text from './misc/Text.js';
export default class SearchFilterOptionsDialog extends YTNode {
static type = 'SearchFilterOptionsDialog';
title: Text;
groups: ObservedArray<SearchFilterGroup>;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.groups = Parser.parseArray(data.groups, SearchFilterGroup);
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import Button from './Button.js';
import ChipCloud from './ChipCloud.js';
export default class SearchHeader extends YTNode {
static type = 'SearchHeader';
chip_bar: ChipCloud | null;
search_filter_button: Button | null;
constructor(data: RawNode) {
super();
this.chip_bar = Parser.parseItem(data.chipBar, ChipCloud);
this.search_filter_button = Parser.parseItem(data.searchFilterButton, Button);
}
}

View File

@@ -8,14 +8,19 @@ import ToggleButton from './ToggleButton.js';
export default class SearchSubMenu extends YTNode {
static type = 'SearchSubMenu';
title: Text;
groups: ObservedArray<SearchFilterGroup>;
button: ToggleButton | null;
title?: Text;
groups?: ObservedArray<SearchFilterGroup>;
button?: ToggleButton | null;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.groups = Parser.parseArray(data.groups, SearchFilterGroup);
this.button = Parser.parseItem(data.button, ToggleButton);
if (Reflect.has(data, 'title'))
this.title = new Text(data.title);
if (Reflect.has(data, 'groups'))
this.groups = Parser.parseArray(data.groups, SearchFilterGroup);
if (Reflect.has(data, 'button'))
this.button = Parser.parseItem(data.button, ToggleButton);
}
}

View File

@@ -1,6 +1,6 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../parser.js';
import * as Parser from '../parser.js';
import BackstagePost from './BackstagePost.js';
import Button from './Button.js';
import Menu from './menus/Menu.js';

View File

@@ -1,16 +1,28 @@
import { type ObservedArray, YTNode } from '../helpers.js';
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import ExpandableVideoDescriptionBody from './ExpandableVideoDescriptionBody.js';
import HorizontalCardList from './HorizontalCardList.js';
import VideoDescriptionHeader from './VideoDescriptionHeader.js';
import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.js';
import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js';
import VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.js';
import VideoDescriptionCourseSection from './VideoDescriptionCourseSection.js';
export default class StructuredDescriptionContent extends YTNode {
static type = 'StructuredDescriptionContent';
items: ObservedArray<VideoDescriptionHeader|ExpandableVideoDescriptionBody|VideoDescriptionMusicSection>;
items: ObservedArray<
VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection |
VideoDescriptionInfocardsSection | VideoDescriptionTranscriptSection | VideoDescriptionTranscriptSection |
VideoDescriptionCourseSection | HorizontalCardList
>;
constructor(data: RawNode) {
super();
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection ]);
this.items = Parser.parseArray(data.items, [
VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection,
VideoDescriptionInfocardsSection, VideoDescriptionCourseSection, VideoDescriptionTranscriptSection,
VideoDescriptionTranscriptSection, HorizontalCardList
]);
}
}
}

View File

@@ -0,0 +1,34 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
export default class StructuredDescriptionPlaylistLockup extends YTNode {
static type = 'StructuredDescriptionPlaylistLockup';
thumbnail: Thumbnail[];
title: Text;
short_byline_text: Text;
video_count_short_text: Text;
endpoint: NavigationEndpoint;
thumbnail_width: number;
aspect_ratio: number;
max_lines_title: number;
max_lines_short_byline_text: number;
overlay_position: string;
constructor(data: RawNode) {
super();
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
this.title = new Text(data.title);
this.short_byline_text = new Text(data.shortBylineText);
this.video_count_short_text = new Text(data.videoCountShortText);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.thumbnail_width = data.thumbnailWidth;
this.aspect_ratio = data.aspectRatio;
this.max_lines_title = data.maxLinesTitle;
this.max_lines_short_byline_text = data.maxLinesShortBylineText;
this.overlay_position = data.overlayPosition;
}
}

View File

@@ -0,0 +1,15 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import TranscriptSearchPanel from './TranscriptSearchPanel.js';
export default class Transcript extends YTNode {
static type = 'Transcript';
content: TranscriptSearchPanel | null;
constructor(data: RawNode) {
super();
this.content = Parser.parseItem(data.content, TranscriptSearchPanel);
}
}

View File

@@ -0,0 +1,15 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import SortFilterSubMenu from './SortFilterSubMenu.js';
export default class TranscriptFooter extends YTNode {
static type = 'TranscriptFooter';
language_menu: SortFilterSubMenu | null;
constructor(data: RawNode) {
super();
this.language_menu = Parser.parseItem(data.languageMenu, SortFilterSubMenu);
}
}

View File

@@ -0,0 +1,23 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import Button from './Button.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import { Text } from '../misc.js';
export default class TranscriptSearchBox extends YTNode {
static type = 'TranscriptSearchBox';
formatted_placeholder: Text;
clear_button: Button | null;
endpoint: NavigationEndpoint;
search_button: Button | null;
constructor(data: RawNode) {
super();
this.formatted_placeholder = new Text(data.formattedPlaceholder);
this.clear_button = Parser.parseItem(data.clearButton, Button);
this.endpoint = new NavigationEndpoint(data.onTextChangeCommand);
this.search_button = Parser.parseItem(data.searchButton, Button);
}
}

View File

@@ -0,0 +1,23 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import TranscriptFooter from './TranscriptFooter.js';
import TranscriptSearchBox from './TranscriptSearchBox.js';
import TranscriptSegmentList from './TranscriptSegmentList.js';
export default class TranscriptSearchPanel extends YTNode {
static type = 'TranscriptSearchPanel';
header: TranscriptSearchBox | null;
body: TranscriptSegmentList | null;
footer: TranscriptFooter | null;
target_id: string;
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header, TranscriptSearchBox);
this.body = Parser.parseItem(data.body, TranscriptSegmentList);
this.footer = Parser.parseItem(data.footer, TranscriptFooter);
this.target_id = data.targetId;
}
}

View File

@@ -0,0 +1,18 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Text from './misc/Text.js';
export default class TranscriptSectionHeader extends YTNode {
static type = 'TranscriptSectionHeader';
start_ms: string;
end_ms: string;
snippet: Text;
constructor(data: RawNode) {
super();
this.start_ms = data.startMs;
this.end_ms = data.endMs;
this.snippet = new Text(data.snippet);
}
}

View File

@@ -0,0 +1,22 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import { Text } from '../misc.js';
export default class TranscriptSegment extends YTNode {
static type = 'TranscriptSegment';
start_ms: string;
end_ms: string;
snippet: Text;
start_time_text: Text;
target_id: string;
constructor(data: RawNode) {
super();
this.start_ms = data.startMs;
this.end_ms = data.endMs;
this.snippet = new Text(data.snippet);
this.start_time_text = new Text(data.startTimeText);
this.target_id = data.targetId;
}
}

View File

@@ -0,0 +1,24 @@
import type { ObservedArray } from '../helpers.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import { Text } from '../misc.js';
import TranscriptSectionHeader from './TranscriptSectionHeader.js';
import TranscriptSegment from './TranscriptSegment.js';
export default class TranscriptSegmentList extends YTNode {
static type = 'TranscriptSegmentList';
initial_segments: ObservedArray<TranscriptSegment | TranscriptSectionHeader>;
no_result_label: Text;
retry_label: Text;
touch_captions_enabled: boolean;
constructor(data: RawNode) {
super();
this.initial_segments = Parser.parseArray(data.initialSegments, [ TranscriptSegment, TranscriptSectionHeader ]);
this.no_result_label = new Text(data.noResultLabel);
this.retry_label = new Text(data.retryLabel);
this.touch_captions_enabled = data.touchCaptionsEnabled;
}
}

View File

@@ -0,0 +1,15 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import Factoid from './Factoid.js';
export default class UploadTimeFactoid extends YTNode {
static type = 'UploadTimeFactoid';
factoid: Factoid | null;
constructor(data: RawNode) {
super();
this.factoid = Parser.parseItem(data.factoid, Factoid);
}
}

View File

@@ -0,0 +1,19 @@
import type { ObservedArray} from '../helpers.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import StructuredDescriptionPlaylistLockup from './StructuredDescriptionPlaylistLockup.js';
import Text from './misc/Text.js';
export default class VideoDescriptionCourseSection extends YTNode {
static type = 'VideoDescriptionCourseSection';
section_title: Text;
media_lockups: ObservedArray<StructuredDescriptionPlaylistLockup>;
constructor(data: RawNode) {
super();
this.section_title = new Text(data.sectionTitle);
this.media_lockups = Parser.parseArray(data.mediaLockups, [ StructuredDescriptionPlaylistLockup ]);
}
}

View File

@@ -1,17 +1,18 @@
import { type ObservedArray, YTNode } from '../helpers.js';
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import { Text } from '../misc.js';
import type { Thumbnail } from '../misc.js';
import { Text, Thumbnail } from '../misc.js';
import Factoid from './Factoid.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import UploadTimeFactoid from './UploadTimeFactoid.js';
import ViewCountFactoid from './ViewCountFactoid.js';
export default class VideoDescriptionHeader extends YTNode {
static type = 'VideoDescriptionHeader';
channel: Text;
channel_navigation_endpoint?: NavigationEndpoint;
channel_thumbnails: String[];
factoids: ObservedArray<Factoid>;
channel_thumbnail: Thumbnail[];
factoids: ObservedArray<Factoid | ViewCountFactoid | UploadTimeFactoid>;
publish_date: Text;
title: Text;
views: Text;
@@ -20,10 +21,10 @@ export default class VideoDescriptionHeader extends YTNode {
super();
this.title = new Text(data.title);
this.channel = new Text(data.channel);
this.channel_navigation_endpoint = Parser.parseItem(data.channelNavigationEndpoint, NavigationEndpoint) || undefined;
this.channel_thumbnails = data.channelThumbnail.thumbnails.map((thumbnail: Thumbnail) => thumbnail.url);
this.channel_navigation_endpoint = new NavigationEndpoint(data.channelNavigationEndpoint);
this.channel_thumbnail = Thumbnail.fromResponse(data.channelThumbnail);
this.publish_date = new Text(data.publishDate);
this.views = new Text(data.views);
this.factoids = Parser.parseArray(data.factoid, Factoid);
this.factoids = Parser.parseArray(data.factoid, [ Factoid, ViewCountFactoid, UploadTimeFactoid ]);
}
}

View File

@@ -0,0 +1,28 @@
import Parser, { type RawNode } from '../index.js';
import { YTNode } from '../helpers.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import Button from './Button.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class VideoDescriptionInfocardsSection extends YTNode {
static type = 'VideoDescriptionInfocardsSection';
section_title: Text;
creator_videos_button: Button | null;
creator_about_button: Button | null;
section_subtitle: Text;
channel_avatar: Thumbnail[];
channel_endpoint: NavigationEndpoint;
constructor(data: RawNode) {
super();
this.section_title = new Text(data.sectionTitle);
this.creator_videos_button = Parser.parseItem(data.creatorVideosButton, Button);
this.creator_about_button = Parser.parseItem(data.creatorAboutButton, Button);
this.section_subtitle = new Text(data.sectionSubtitle);
this.channel_avatar = Thumbnail.fromResponse(data.channelAvatar);
this.channel_endpoint = new NavigationEndpoint(data.channelEndpoint);
}
}

View File

@@ -1,13 +1,14 @@
import { type ObservedArray, YTNode } from '../helpers.js';
import CarouselLockup from './CarouselLockup.js';
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import { Text } from '../misc.js';
import CarouselLockup from './CarouselLockup.js';
export default class VideoDescriptionMusicSection extends YTNode {
static type = 'VideoDescriptionMusicSection';
carousel_lockups: ObservedArray<CarouselLockup>;
section_title: Text;
constructor(data: RawNode) {
super();
this.carousel_lockups = Parser.parseArray(data.carouselLockups, CarouselLockup);

View File

@@ -0,0 +1,20 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import { Text } from '../misc.js';
import Button from './Button.js';
export default class VideoDescriptionTranscriptSection extends YTNode {
static type = 'VideoDescriptionTranscriptSection';
section_title: Text;
sub_header_text: Text;
primary_button: Button | null;
constructor(data: RawNode) {
super();
this.section_title = new Text(data.sectionTitle);
this.sub_header_text = new Text(data.subHeaderText);
this.primary_button = Parser.parseItem(data.primaryButton, Button);
}
}

View File

@@ -24,7 +24,7 @@ export default class VideoSecondaryInfo extends YTNode {
this.description = new Text(data.description);
if (Reflect.has(data, 'attributedDescription')) {
this.description = new Text(this.#convertAttributedDescriptionToRuns(data.attributedDescription));
this.description = Text.fromAttributed(data.attributedDescription);
}
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
@@ -34,72 +34,4 @@ export default class VideoSecondaryInfo extends YTNode {
this.default_expanded = data.defaultExpanded;
this.description_collapsed_lines = data.descriptionCollapsedLines;
}
#convertAttributedDescriptionToRuns(description: RawNode) {
const runs: {
text: string,
navigationEndpoint?: RawNode,
attachment?: RawNode
}[] = [];
const content = description.content;
const command_runs = description.commandRuns;
let last_end_index = 0;
if (command_runs) {
for (const item of command_runs) {
const length: number = item.length;
const start_index: number = item.startIndex;
if (start_index > last_end_index) {
runs.push({
text: content.slice(last_end_index, start_index)
});
}
if (Reflect.has(item, 'onTap')) {
let attachment = null;
if (Reflect.has(description, 'attachmentRuns')) {
const attachment_runs = description.attachmentRuns;
for (const attatchment_run of attachment_runs) {
if ((attatchment_run.startIndex - 2) == start_index) {
attachment = attatchment_run;
break;
}
}
}
if (attachment) {
runs.push({
text: content.slice(start_index, start_index + length),
navigationEndpoint: item.onTap,
attachment
});
} else {
runs.push({
text: content.slice(start_index, start_index + length),
navigationEndpoint: item.onTap
});
}
}
last_end_index = start_index + length;
}
if (last_end_index < content.length) {
runs.push({
text: content.slice(last_end_index)
});
}
} else {
runs.push({
text: content
});
}
return { runs };
}
}

View File

@@ -0,0 +1,19 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import Parser from '../index.js';
import Factoid from './Factoid.js';
export default class ViewCountFactoid extends YTNode {
static type = 'ViewCountFactoid';
view_count_entity_key: string;
factoid: Factoid | null;
view_count_type: string;
constructor(data: RawNode) {
super();
this.view_count_entity_key = data.viewCountEntityKey;
this.factoid = Parser.parseItem(data.factoid, [ Factoid ]);
this.view_count_type = data.viewCountType;
}
}

View File

@@ -1,16 +1,17 @@
import Parser from '../../index.js';
import type { RawNode } from '../../index.js';
import { type SuperParsedResult, YTNode } from '../../helpers.js';
import type { ObservedArray } from '../../helpers.js';
import { YTNode } from '../../helpers.js';
export default class AppendContinuationItemsAction extends YTNode {
static type = 'AppendContinuationItemsAction';
items: SuperParsedResult<YTNode>;
contents: ObservedArray<YTNode> | null;
target: string;
constructor(data: RawNode) {
super();
this.items = Parser.parse(data.continuationItems);
this.contents = Parser.parseArray(data.continuationItems);
this.target = data.target;
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../../helpers.js';
import type { RawNode } from '../../index.js';
import Parser from '../../index.js';
import Transcript from '../Transcript.js';
export default class UpdateEngagementPanelAction extends YTNode {
static type = 'UpdateEngagementPanelAction';
target_id: string;
content: Transcript | null;
constructor(data: RawNode) {
super();
this.target_id = data.targetId;
this.content = Parser.parseItem(data.content, Transcript);
}
}

View File

@@ -11,7 +11,7 @@ import CommentReplyDialog from './CommentReplyDialog.js';
import PdgCommentChip from './PdgCommentChip.js';
import SponsorCommentBadge from './SponsorCommentBadge.js';
import Proto from '../../../proto/index.js';
import * as Proto from '../../../proto/index.js';
import { InnertubeError } from '../../../utils/Utils.js';
import { YTNode } from '../../helpers.js';

View File

@@ -1,9 +1,10 @@
import Text from '../misc/Text.js';
import Thumbnail from '../misc/Thumbnail.js';
import CommentsSimplebox from './CommentsSimplebox.js';
import CommentsEntryPointTeaser from './CommentsEntryPointTeaser.js';
import { YTNode } from '../../helpers.js';
import type { RawNode } from '../../index.js';
import { Parser } from '../../index.js';
import CommentsEntryPointTeaser from './CommentsEntryPointTeaser.js';
export default class CommentsEntryPointHeader extends YTNode {
static type = 'CommentsEntryPointHeader';
@@ -12,7 +13,7 @@ export default class CommentsEntryPointHeader extends YTNode {
comment_count?: Text;
teaser_avatar?: Thumbnail[];
teaser_content?: Text;
content_renderer?: CommentsEntryPointTeaser | null;
content_renderer?: CommentsEntryPointTeaser | CommentsSimplebox | null;
simplebox_placeholder?: Text;
constructor(data: RawNode) {
@@ -35,7 +36,7 @@ export default class CommentsEntryPointHeader extends YTNode {
}
if (Reflect.has(data, 'contentRenderer')) {
this.content_renderer = Parser.parseItem(data.contentRenderer, CommentsEntryPointTeaser);
this.content_renderer = Parser.parseItem(data.contentRenderer, [ CommentsEntryPointTeaser, CommentsSimplebox ]);
}
if (Reflect.has(data, 'simpleboxPlaceholder')) {

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../../helpers.js';
import Text from '../misc/Text.js';
import Thumbnail from '../misc/Thumbnail.js';
import type { RawNode } from '../../index.js';
export default class CommentsSimplebox extends YTNode {
static type = 'CommentsSimplebox';
simplebox_avatar: Thumbnail[];
simplebox_placeholder: Text;
constructor(data: RawNode) {
super();
this.simplebox_avatar = Thumbnail.fromResponse(data.simpleboxAvatar);
this.simplebox_placeholder = new Text(data.simpleboxPlaceholder);
}
}

View File

@@ -0,0 +1,17 @@
import type { ObservedArray} from '../../helpers.js';
import { YTNode } from '../../helpers.js';
import type { RawNode } from '../../index.js';
import Parser from '../../index.js';
import MenuNavigationItem from './MenuNavigationItem.js';
import MenuServiceItem from './MenuServiceItem.js';
export default class MenuPopup extends YTNode {
static type = 'MenuPopup';
items: ObservedArray<MenuNavigationItem | MenuServiceItem>;
constructor(data: RawNode) {
super();
this.items = Parser.parseArray(data.items, [ MenuNavigationItem, MenuServiceItem ]);
}
}

View File

@@ -3,6 +3,8 @@ import { InnertubeError } from '../../../utils/Utils.js';
import type { RawNode } from '../../index.js';
export default class Format {
#this_response_nsig_cache?: Map<string, string>;
itag: number;
mime_type: string;
is_type_otf: boolean;
@@ -46,7 +48,17 @@ export default class Format {
is_descriptive?: boolean;
is_original?: boolean;
constructor(data: RawNode) {
color_info?: {
primaries?: string;
transfer_characteristics?: string;
matrix_coefficients?: string;
};
constructor(data: RawNode, this_response_nsig_cache?: Map<string, string>) {
if (this_response_nsig_cache) {
this.#this_response_nsig_cache = this_response_nsig_cache;
}
this.itag = data.itag;
this.mime_type = data.mimeType;
this.is_type_otf = data.type === 'FORMAT_STREAM_TYPE_OTF';
@@ -81,14 +93,24 @@ export default class Format {
this.has_audio = !!data.audioBitrate || !!data.audioQuality;
this.has_video = !!data.qualityLabel;
this.color_info = data.colorInfo ? {
primaries: data.colorInfo.primaries?.replace('COLOR_PRIMARIES_', ''),
transfer_characteristics: data.colorInfo.transferCharacteristics?.replace('COLOR_TRANSFER_CHARACTERISTICS_', ''),
matrix_coefficients: data.colorInfo.matrixCoefficients?.replace('COLOR_MATRIX_COEFFICIENTS_', '')
} : undefined;
if (this.has_audio) {
const args = new URLSearchParams(this.cipher || this.signature_cipher);
const url_components = new URLSearchParams(args.get('url') || this.url);
this.language = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('lang='))?.split('=').at(1) || null;
this.is_dubbed = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'dubbed';
this.is_descriptive = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'descriptive';
this.is_original = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'original' || !this.is_dubbed;
const xtags = url_components.get('xtags')?.split(':');
const audio_content = xtags?.find((x) => x.startsWith('acont='))?.split('=')[1];
this.language = xtags?.find((x: string) => x.startsWith('lang='))?.split('=')[1] || null;
this.is_dubbed = audio_content === 'dubbed';
this.is_descriptive = audio_content === 'descriptive';
this.is_original = audio_content === 'original' || (!this.is_dubbed && !this.is_descriptive);
if (Reflect.has(data, 'audioTrack')) {
this.audio_track = {
@@ -106,6 +128,6 @@ export default class Format {
*/
decipher(player: Player | undefined): string {
if (!player) throw new InnertubeError('Cannot decipher format, this session appears to have no valid player.');
return player.decipher(this.url, this.signature_cipher, this.cipher);
return player.decipher(this.url, this.signature_cipher, this.cipher, this.#this_response_nsig_cache);
}
}

View File

@@ -46,6 +46,74 @@ export default class Text {
}
}
static fromAttributed(data: RawNode): Text {
const runs: {
text: string,
navigationEndpoint?: RawNode,
attachment?: RawNode
}[] = [];
const content = data.content;
const command_runs = data.commandRuns;
let last_end_index = 0;
if (command_runs) {
for (const item of command_runs) {
const length: number = item.length;
const start_index: number = item.startIndex;
if (start_index > last_end_index) {
runs.push({
text: content.slice(last_end_index, start_index)
});
}
if (Reflect.has(item, 'onTap')) {
let attachment = null;
if (Reflect.has(data, 'attachmentRuns')) {
const attachment_runs = data.attachmentRuns;
for (const attatchment_run of attachment_runs) {
if ((attatchment_run.startIndex - 2) == start_index) {
attachment = attatchment_run;
break;
}
}
}
if (attachment) {
runs.push({
text: content.slice(start_index, start_index + length),
navigationEndpoint: item.onTap,
attachment
});
} else {
runs.push({
text: content.slice(start_index, start_index + length),
navigationEndpoint: item.onTap
});
}
}
last_end_index = start_index + length;
}
if (last_end_index < content.length) {
runs.push({
text: content.slice(last_end_index)
});
}
} else {
runs.push({
text: content
});
}
return new Text({ runs });
}
/**
* Converts the text to HTML.
* @returns The HTML.

Some files were not shown because too many files have changed in this diff Show More