mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 10:02:16 +00:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d98abbd53 | ||
|
|
fba3fc9714 | ||
|
|
f94ea6cf91 | ||
|
|
86fb33ed03 | ||
|
|
bff4210349 | ||
|
|
91de6e5c0e | ||
|
|
c26972c42a | ||
|
|
8bc2aaa358 | ||
|
|
2e5f076fd7 | ||
|
|
0412fa05ff | ||
|
|
10c15bfb9f | ||
|
|
4862c35cee | ||
|
|
2eed1726d5 | ||
|
|
8b69587787 | ||
|
|
ed7be2a675 | ||
|
|
361fb4a9f1 | ||
|
|
1c3ea2acd3 | ||
|
|
859c4585d9 | ||
|
|
751f2b90fd | ||
|
|
90be877d28 | ||
|
|
052632314b | ||
|
|
22a38c0762 | ||
|
|
f7614634b6 | ||
|
|
bf1510b235 | ||
|
|
815e54b854 | ||
|
|
f7666051f6 | ||
|
|
494ee8776a | ||
|
|
87ed3960ff | ||
|
|
eb3cca1e2e | ||
|
|
9971ffe021 | ||
|
|
7949b3df66 | ||
|
|
aa385142e4 | ||
|
|
6c8a916f0f | ||
|
|
31d27b1bca | ||
|
|
cb37c6a17b | ||
|
|
1ff3e1a440 | ||
|
|
46fe18b763 | ||
|
|
0dda97e0b0 | ||
|
|
e370116092 | ||
|
|
3bc53a8c12 | ||
|
|
74e1a5e068 | ||
|
|
0fa5a859ae | ||
|
|
02a111250a | ||
|
|
c1886f9a83 | ||
|
|
5f4cbdb904 | ||
|
|
d91695a9ec | ||
|
|
137464ca66 | ||
|
|
6997982cf2 | ||
|
|
18cbc8c038 | ||
|
|
30ff087587 | ||
|
|
1a034733f6 | ||
|
|
c477b824c0 | ||
|
|
7e5c3648c1 | ||
|
|
bdd98a3b9b | ||
|
|
06750aaa74 | ||
|
|
708c5f7394 | ||
|
|
a9cdbf7010 | ||
|
|
b50d1ef67d | ||
|
|
555d257459 | ||
|
|
2aef67876e | ||
|
|
ae2557d15c | ||
|
|
8c688efb4a | ||
|
|
cffa868c6e | ||
|
|
f267fcd8be | ||
|
|
23c22a93c4 | ||
|
|
1ca20836bf | ||
|
|
5f058e69ae | ||
|
|
3500e92632 | ||
|
|
3f57c2fa5c | ||
|
|
7528ebdb60 | ||
|
|
5e3846259f | ||
|
|
222dfce6bb | ||
|
|
83cbfd631b | ||
|
|
4f9427d752 | ||
|
|
07c1b3e0e5 | ||
|
|
89548ad48a | ||
|
|
519be72445 | ||
|
|
e434bb2632 | ||
|
|
a11e5962c6 | ||
|
|
77b39c79ee | ||
|
|
7c530d30ee | ||
|
|
1e07a184ff | ||
|
|
5de7b24dc5 | ||
|
|
01fd1ee72a | ||
|
|
84b4f1efd1 | ||
|
|
046103a4d8 | ||
|
|
beb4733e84 | ||
|
|
66b026bf49 | ||
|
|
26734194ab | ||
|
|
38a83c3c2a | ||
|
|
b1f19f16ac | ||
|
|
891d889408 | ||
|
|
d4adb9eb6b | ||
|
|
3b0498b68b |
221
CHANGELOG.md
221
CHANGELOG.md
@@ -1,5 +1,226 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **VideoDetails:** Add is_post_live_dvr property ([#411](https://github.com/LuanRT/YouTube.js/issues/411)) ([a11e596](https://github.com/LuanRT/YouTube.js/commit/a11e5962c6eb73b14623a9de1e6c8c2534146b1e))
|
||||
* **ytmusic:** Add support for YouTube Music mood filters ([#404](https://github.com/LuanRT/YouTube.js/issues/404)) ([77b39c7](https://github.com/LuanRT/YouTube.js/commit/77b39c79ee0768eb203b7d47ea81286d470c21f2))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **OAuth:** client identity matching ([#421](https://github.com/LuanRT/YouTube.js/issues/421)) ([07c1b3e](https://github.com/LuanRT/YouTube.js/commit/07c1b3e0e57cb1fa42e4772775bfd1437bbc731f))
|
||||
* **PlayerEndpoint:** Use different player params ([#419](https://github.com/LuanRT/YouTube.js/issues/419)) ([519be72](https://github.com/LuanRT/YouTube.js/commit/519be72445b7ff392b396e16bcb1dc05c7df8976))
|
||||
* **Playlist:** Add thumbnail_renderer on Playlist when response includes it ([#424](https://github.com/LuanRT/YouTube.js/issues/424)) ([4f9427d](https://github.com/LuanRT/YouTube.js/commit/4f9427d752e89faec8dd1c4fd7a9607dca998c7a))
|
||||
* **VideoInfo.ts:** reimplement `get music_tracks` ([#409](https://github.com/LuanRT/YouTube.js/issues/409)) ([e434bb2](https://github.com/LuanRT/YouTube.js/commit/e434bb2632fe2b20aab6f1e707a93ca76f9d5c91))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **Search:** Speed up results parsing ([#408](https://github.com/LuanRT/YouTube.js/issues/408)) ([1e07a18](https://github.com/LuanRT/YouTube.js/commit/1e07a184ffaff508ad5ba869cb5e7dc9f095f744))
|
||||
* **toDash:** Speed up format filtering ([#405](https://github.com/LuanRT/YouTube.js/issues/405)) ([5de7b24](https://github.com/LuanRT/YouTube.js/commit/5de7b24dc55fca3eb8fccc6fa30d3c2cd60b8184))
|
||||
|
||||
## [5.1.0](https://github.com/LuanRT/YouTube.js/compare/v5.0.4...v5.1.0) (2023-05-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ReelItem:** Add accessibility label ([#401](https://github.com/LuanRT/YouTube.js/issues/401)) ([046103a](https://github.com/LuanRT/YouTube.js/commit/046103a4d8af09fafefab6e9f971184eeca75c2e))
|
||||
* **toDash:** Add audio track labels to the manifest when available ([#402](https://github.com/LuanRT/YouTube.js/issues/402)) ([84b4f1e](https://github.com/LuanRT/YouTube.js/commit/84b4f1efd111321e4f3e5a87844790c4ec9b0b52))
|
||||
|
||||
## [5.0.4](https://github.com/LuanRT/YouTube.js/compare/v5.0.3...v5.0.4) (2023-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bundles:** Use ESM tslib build for the browser bundles ([#397](https://github.com/LuanRT/YouTube.js/issues/397)) ([2673419](https://github.com/LuanRT/YouTube.js/commit/26734194ab0bc5a9f57e1c509d7646ce8903d0c6))
|
||||
* **Utils:** Circular dependency introduced in 38a83c3c2aa814150d1d9b8ed99fca915c1d67fe ([#400](https://github.com/LuanRT/YouTube.js/issues/400)) ([66b026b](https://github.com/LuanRT/YouTube.js/commit/66b026bf493d71a39e12825938fe54dc63aefd16))
|
||||
* **Utils:** Use instanceof in deepCompare instead of the constructor name ([#398](https://github.com/LuanRT/YouTube.js/issues/398)) ([38a83c3](https://github.com/LuanRT/YouTube.js/commit/38a83c3c2aa814150d1d9b8ed99fca915c1d67fe))
|
||||
|
||||
## [5.0.3](https://github.com/LuanRT/YouTube.js/compare/v5.0.2...v5.0.3) (2023-05-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Video:** typo causing node parsing to fail ([3b0498b](https://github.com/LuanRT/YouTube.js/commit/3b0498b68b5378e63283e792bd45571c0b919e0b))
|
||||
|
||||
## [5.0.2](https://github.com/LuanRT/YouTube.js/compare/v5.0.1...v5.0.2) (2023-04-30)
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ This page lists the collaborators who have contributed to the development and su
|
||||
|
||||
## [LuanRT](https://github.com/LuanRT)
|
||||
[](https://github.com/sponsors/LuanRT)
|
||||
[](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.
|
||||
16
README.md
16
README.md
@@ -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.
|
||||
@@ -542,6 +542,8 @@ Retrieves contents for a given channel.
|
||||
- `<channel>#getVideos()`
|
||||
- `<channel>#getShorts()`
|
||||
- `<channel>#getLiveStreams()`
|
||||
- `<channel>#getReleases()`
|
||||
- `<channel>#getPodcasts()`
|
||||
- `<channel>#getPlaylists()`
|
||||
- `<channel>#getHome()`
|
||||
- `<channel>#getCommunity()`
|
||||
@@ -658,6 +660,16 @@ console.info('Playback url:', url);
|
||||
| video_id | `string` | Video id |
|
||||
| options | `FormatOptions` | Format options |
|
||||
|
||||
<a name="gettranscript"></a>
|
||||
### `getTranscript(video_id)`
|
||||
Retrieves a given video's transcript.
|
||||
|
||||
**Returns**: `Promise<Transcript>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | Video id |
|
||||
|
||||
<a name="download"></a>
|
||||
### `download(video_id, options?)`
|
||||
Downloads a given video.
|
||||
@@ -786,7 +798,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]
|
||||
|
||||
|
||||
2
deno.ts
2
deno.ts
@@ -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;
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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>
|
||||
@@ -13,6 +13,6 @@
|
||||
"vite": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dashjs": "^4.4.0"
|
||||
"shaka-player": "^4.3.8"
|
||||
}
|
||||
}
|
||||
423
examples/browser/web/src/assets/player.css
Normal file
423
examples/browser/web/src/assets/player.css
Normal 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;
|
||||
}
|
||||
}
|
||||
135
examples/browser/web/src/assets/style.css
Normal file
135
examples/browser/web/src/assets/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
5971
package-lock.json
generated
5971
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "5.0.2",
|
||||
"version": "6.4.0",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
@@ -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,12 +69,12 @@
|
||||
"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 --outfile=./bundle/browser.js --platform=browser",
|
||||
"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",
|
||||
"prepare": "npm run build",
|
||||
"watch": "npx tsc --watch"
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
48
scripts/get-agents.mjs
Normal 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[] };`);
|
||||
@@ -14,13 +14,15 @@ import NotificationsMenu from './parser/youtube/NotificationsMenu.js';
|
||||
import Playlist from './parser/youtube/Playlist.js';
|
||||
import Search from './parser/youtube/Search.js';
|
||||
import VideoInfo from './parser/youtube/VideoInfo.js';
|
||||
import ContinuationItem from './parser/classes/ContinuationItem.js';
|
||||
import Transcript from './parser/classes/Transcript.js';
|
||||
|
||||
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 Constants from './utils/Constants.js';
|
||||
import * as Proto from './proto/index.js';
|
||||
import * as Constants from './utils/Constants.js';
|
||||
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.js';
|
||||
|
||||
import {
|
||||
@@ -36,13 +38,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 IGetTranscriptResponse, 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';
|
||||
@@ -332,6 +334,35 @@ export default class Innertube {
|
||||
return info.chooseFormat(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a video's transcript.
|
||||
* @param video_id - The video id.
|
||||
*/
|
||||
async getTranscript(video_id: string): Promise<Transcript> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const next_response = await this.actions.execute(NextEndpoint.PATH, { ...NextEndpoint.build({ video_id }), parse: true });
|
||||
|
||||
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 transcript_response = await transcript_continuation.endpoint.call<IGetTranscriptResponse>(this.actions, { parse: true });
|
||||
|
||||
return transcript_response.actions_memo.getType(Transcript).first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a given video. If you only need the direct download link see {@link getStreamingData}.
|
||||
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Constants from '../utils/Constants.js';
|
||||
import * as Constants from '../utils/Constants.js';
|
||||
import { OAuthError, Platform } from '../utils/Utils.js';
|
||||
import type Session from './Session.js';
|
||||
|
||||
@@ -96,7 +96,7 @@ export default class OAuth {
|
||||
client_id: this.#identity.client_id,
|
||||
scope: Constants.OAUTH.SCOPE,
|
||||
device_id: Platform.shim.uuidv4(),
|
||||
model_name: Constants.OAUTH.MODEL_NAME
|
||||
device_model: Constants.OAUTH.MODEL_NAME
|
||||
};
|
||||
|
||||
const response = await this.#session.http.fetch_function(new URL('/o/oauth2/device/code', Constants.URLS.YT_BASE), {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.js';
|
||||
|
||||
import Constants from '../utils/Constants.js';
|
||||
import * as Constants from '../utils/Constants.js';
|
||||
|
||||
import type { ICache } from '../types/Cache.js';
|
||||
import type { FetchFunction } from '../types/PlatformShim.js';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Constants, { CLIENTS } from '../utils/Constants.js';
|
||||
import * as Constants from '../utils/Constants.js';
|
||||
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);
|
||||
|
||||
@@ -325,7 +343,7 @@ export default class Session extends EventEmitterLike {
|
||||
screenWidthPoints: 1920,
|
||||
visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)),
|
||||
clientName: options.client_name,
|
||||
clientVersion: CLIENTS.WEB.VERSION,
|
||||
clientVersion: Constants.CLIENTS.WEB.VERSION,
|
||||
osName: 'Windows',
|
||||
osVersion: '10.0',
|
||||
platform: options.device_category.toUpperCase(),
|
||||
@@ -339,11 +357,12 @@ export default class Session extends EventEmitterLike {
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
}
|
||||
};
|
||||
|
||||
return { context, api_key: CLIENTS.WEB.API_KEY, api_version: CLIENTS.WEB.API_VERSION };
|
||||
return { context, api_key: Constants.CLIENTS.WEB.API_KEY, api_version: Constants.CLIENTS.WEB.API_VERSION };
|
||||
}
|
||||
|
||||
async signIn(credentials?: Credentials): Promise<void> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Proto from '../../proto/index.js';
|
||||
import { Constants } from '../../utils/index.js';
|
||||
import * as Proto from '../../proto/index.js';
|
||||
import * as Constants from '../../utils/Constants.js';
|
||||
import { InnertubeError, MissingParamError, Platform } from '../../utils/Utils.js';
|
||||
|
||||
import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Clients.js';
|
||||
|
||||
@@ -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: {
|
||||
@@ -28,14 +33,17 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
|
||||
}
|
||||
}
|
||||
},
|
||||
attestationRequest: {
|
||||
omitBotguardData: true
|
||||
},
|
||||
racyCheckOk: true,
|
||||
contentCheckOk: true,
|
||||
videoId: opts.video_id,
|
||||
...{
|
||||
client: opts.client,
|
||||
playlistId: opts.playlist_id,
|
||||
// Workaround streaming URLs returning 403 when using Android clients and throttling in web clients.
|
||||
params: '8AEB'
|
||||
// Workaround streaming URLs returning 403 or getting throttled when using Android based clients.
|
||||
params: is_android ? '2AMBCgIQBg' : opts.params
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { ApiResponse } from '../Actions.js';
|
||||
import type Actions from '../Actions.js';
|
||||
import Constants from '../../utils/Constants.js';
|
||||
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.js';
|
||||
import FormatUtils from '../../utils/FormatUtils.js';
|
||||
import * as Constants from '../../utils/Constants.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';
|
||||
|
||||
export default class MediaInfo {
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
@@ -37,10 +40,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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
19
src/parser/classes/AlertWithButton.ts
Normal file
19
src/parser/classes/AlertWithButton.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
17
src/parser/classes/CarouselLockup.ts
Normal file
17
src/parser/classes/CarouselLockup.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import InfoRow from './InfoRow.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import CompactVideo from './CompactVideo.js';
|
||||
|
||||
export default class CarouselLockup extends YTNode {
|
||||
static type = 'CarouselLockup';
|
||||
|
||||
info_rows: ObservedArray<InfoRow>;
|
||||
video_lockup?: CompactVideo | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.info_rows = Parser.parseArray(data.infoRows, InfoRow);
|
||||
this.video_lockup = Parser.parseItem(data.videoLockup, CompactVideo);
|
||||
}
|
||||
}
|
||||
22
src/parser/classes/ChannelHeaderLinksView.ts
Normal file
22
src/parser/classes/ChannelHeaderLinksView.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
57
src/parser/classes/CompactMovie.ts
Normal file
57
src/parser/classes/CompactMovie.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
16
src/parser/classes/ContentPreviewImageView.ts
Normal file
16
src/parser/classes/ContentPreviewImageView.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
13
src/parser/classes/DynamicTextView.ts
Normal file
13
src/parser/classes/DynamicTextView.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
27
src/parser/classes/EngagementPanelSectionList.ts
Normal file
27
src/parser/classes/EngagementPanelSectionList.ts
Normal file
@@ -0,0 +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';
|
||||
|
||||
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;
|
||||
this.visibility = data.visibility;
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/EngagementPanelTitleHeader.ts
Normal file
17
src/parser/classes/EngagementPanelTitleHeader.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
22
src/parser/classes/ExpandableVideoDescriptionBody.ts
Normal file
22
src/parser/classes/ExpandableVideoDescriptionBody.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { YTNode } from '../helpers.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?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.show_more_text = new Text(data.showMoreText);
|
||||
this.show_less_text = new Text(data.showLessText);
|
||||
|
||||
if (Reflect.has(data, 'attributedDescriptionBodyText')) {
|
||||
this.attributed_description_body_text = data.attributedDescriptionBodyText?.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/parser/classes/Factoid.ts
Normal file
18
src/parser/classes/Factoid.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { type RawNode } from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
|
||||
export default class Factoid extends YTNode {
|
||||
static type = 'Factoid';
|
||||
|
||||
label: Text;
|
||||
value: Text;
|
||||
accessibility_text: String;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.label = new Text(data.label);
|
||||
this.value = new Text(data.value);
|
||||
this.accessibility_text = data.accessibilityText;
|
||||
}
|
||||
}
|
||||
16
src/parser/classes/FancyDismissibleDialog.ts
Normal file
16
src/parser/classes/FancyDismissibleDialog.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
28
src/parser/classes/HashtagTile.ts
Normal file
28
src/parser/classes/HashtagTile.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
25
src/parser/classes/IncludingResultsFor.ts
Normal file
25
src/parser/classes/IncludingResultsFor.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/parser/classes/InfoRow.ts
Normal file
29
src/parser/classes/InfoRow.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Text } from '../misc.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class InfoRow extends YTNode {
|
||||
static type = 'InfoRow';
|
||||
|
||||
title: Text;
|
||||
default_metadata?: Text;
|
||||
expanded_metadata?: Text;
|
||||
info_row_expand_status_key?: String;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/MacroMarkersInfoItem.ts
Normal file
17
src/parser/classes/MacroMarkersInfoItem.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
18
src/parser/classes/MacroMarkersList.ts
Normal file
18
src/parser/classes/MacroMarkersList.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
44
src/parser/classes/MusicMultiRowListItem.ts
Normal file
44
src/parser/classes/MusicMultiRowListItem.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
16
src/parser/classes/PageHeader.ts
Normal file
16
src/parser/classes/PageHeader.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/PageHeaderView.ts
Normal file
17
src/parser/classes/PageHeaderView.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
@@ -12,6 +14,7 @@ export default class Playlist extends YTNode {
|
||||
title: Text;
|
||||
author: Text | Author;
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_renderer?: PlaylistVideoThumbnail | PlaylistCustomThumbnail;
|
||||
video_count: Text;
|
||||
video_count_short: Text;
|
||||
first_videos: ObservedArray<YTNode>;
|
||||
@@ -41,6 +44,10 @@ export default class Playlist extends YTNode {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
|
||||
if (Reflect.has(data, 'thumbnailRenderer')) {
|
||||
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, [ PlaylistVideoThumbnail, PlaylistCustomThumbnail ]) || undefined;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'viewPlaylistText')) {
|
||||
this.view_playlist = new Text(data.viewPlaylistText);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
15
src/parser/classes/ProductList.ts
Normal file
15
src/parser/classes/ProductList.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
16
src/parser/classes/ProductListHeader.ts
Normal file
16
src/parser/classes/ProductListHeader.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
31
src/parser/classes/ProductListItem.ts
Normal file
31
src/parser/classes/ProductListItem.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
25
src/parser/classes/Quiz.ts
Normal file
25
src/parser/classes/Quiz.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export default class ReelItem extends YTNode {
|
||||
thumbnails: Thumbnail[];
|
||||
views: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
accessibility_label?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
@@ -20,5 +21,6 @@ export default class ReelItem extends YTNode {
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.views = new Text(data.viewCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.accessibility_label = data.accessibility.accessibilityData.label;
|
||||
}
|
||||
}
|
||||
18
src/parser/classes/SearchFilterOptionsDialog.ts
Normal file
18
src/parser/classes/SearchFilterOptionsDialog.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/SearchHeader.ts
Normal file
17
src/parser/classes/SearchHeader.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
22
src/parser/classes/StructuredDescriptionContent.ts
Normal file
22
src/parser/classes/StructuredDescriptionContent.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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 type VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.js';
|
||||
|
||||
export default class StructuredDescriptionContent extends YTNode {
|
||||
static type = 'StructuredDescriptionContent';
|
||||
|
||||
items: ObservedArray<
|
||||
VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection |
|
||||
VideoDescriptionInfocardsSection | VideoDescriptionTranscriptSection | HorizontalCardList
|
||||
>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection, HorizontalCardList ]);
|
||||
}
|
||||
}
|
||||
15
src/parser/classes/Transcript.ts
Normal file
15
src/parser/classes/Transcript.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
15
src/parser/classes/TranscriptFooter.ts
Normal file
15
src/parser/classes/TranscriptFooter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
23
src/parser/classes/TranscriptSearchBox.ts
Normal file
23
src/parser/classes/TranscriptSearchBox.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
23
src/parser/classes/TranscriptSearchPanel.ts
Normal file
23
src/parser/classes/TranscriptSearchPanel.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
22
src/parser/classes/TranscriptSegment.ts
Normal file
22
src/parser/classes/TranscriptSegment.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
23
src/parser/classes/TranscriptSegmentList.ts
Normal file
23
src/parser/classes/TranscriptSegmentList.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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 TranscriptSegment from './TranscriptSegment.js';
|
||||
|
||||
export default class TranscriptSegmentList extends YTNode {
|
||||
static type = 'TranscriptSegmentList';
|
||||
|
||||
initial_segments: ObservedArray<TranscriptSegment>;
|
||||
no_result_label: Text;
|
||||
retry_label: Text;
|
||||
touch_captions_enabled: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.initial_segments = Parser.parseArray(data.initialSegments, TranscriptSegment);
|
||||
this.no_result_label = new Text(data.noResultLabel);
|
||||
this.retry_label = new Text(data.retryLabel);
|
||||
this.touch_captions_enabled = data.touchCaptionsEnabled;
|
||||
}
|
||||
}
|
||||
15
src/parser/classes/UploadTimeFactoid.ts
Normal file
15
src/parser/classes/UploadTimeFactoid.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export default class Video extends YTNode {
|
||||
this.description_snippet = new Text(data.descriptionSnippet);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'detaildMetadataSnippets')) {
|
||||
if (Reflect.has(data, 'detailedMetadataSnippets')) {
|
||||
this.snippets = data.detailedMetadataSnippets.map((snippet: RawNode) => ({
|
||||
text: new Text(snippet.snippetText),
|
||||
hover_text: new Text(snippet.snippetHoverText)
|
||||
@@ -127,4 +127,4 @@ export default class Video extends YTNode {
|
||||
get best_thumbnail(): Thumbnail | undefined {
|
||||
return this.thumbnails[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
src/parser/classes/VideoDescriptionHeader.ts
Normal file
30
src/parser/classes/VideoDescriptionHeader.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.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_thumbnail: Thumbnail[];
|
||||
factoids: ObservedArray<Factoid | ViewCountFactoid | UploadTimeFactoid>;
|
||||
publish_date: Text;
|
||||
title: Text;
|
||||
views: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.channel = new Text(data.channel);
|
||||
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, ViewCountFactoid, UploadTimeFactoid ]);
|
||||
}
|
||||
}
|
||||
28
src/parser/classes/VideoDescriptionInfocardsSection.ts
Normal file
28
src/parser/classes/VideoDescriptionInfocardsSection.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/VideoDescriptionMusicSection.ts
Normal file
17
src/parser/classes/VideoDescriptionMusicSection.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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);
|
||||
this.section_title = new Text(data.sectionTitle);
|
||||
}
|
||||
}
|
||||
20
src/parser/classes/VideoDescriptionTranscriptSection.ts
Normal file
20
src/parser/classes/VideoDescriptionTranscriptSection.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
19
src/parser/classes/ViewCountFactoid.ts
Normal file
19
src/parser/classes/ViewCountFactoid.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/actions/UpdateEngagementPanelAction.ts
Normal file
17
src/parser/classes/actions/UpdateEngagementPanelAction.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
17
src/parser/classes/comments/CommentsSimplebox.ts
Normal file
17
src/parser/classes/comments/CommentsSimplebox.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/menus/MenuPopup.ts
Normal file
17
src/parser/classes/menus/MenuPopup.ts
Normal 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 ]);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Constants from '../../../utils/Constants.js';
|
||||
import * as Constants from '../../../utils/Constants.js';
|
||||
import type { YTNode} from '../../helpers.js';
|
||||
import { observe, type ObservedArray } from '../../helpers.js';
|
||||
import Parser, { type RawNode } from '../../index.js';
|
||||
|
||||
@@ -46,6 +46,12 @@ export default class Format {
|
||||
is_descriptive?: boolean;
|
||||
is_original?: boolean;
|
||||
|
||||
color_info?: {
|
||||
primaries?: string;
|
||||
transfer_characteristics?: string;
|
||||
matrix_coefficients?: string;
|
||||
};
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.itag = data.itag;
|
||||
this.mime_type = data.mimeType;
|
||||
@@ -81,14 +87,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 = {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -18,6 +18,7 @@ export default class VideoDetails {
|
||||
is_live_content: boolean;
|
||||
is_upcoming: boolean;
|
||||
is_crawlable: boolean;
|
||||
is_post_live_dvr: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.id = data.videoId;
|
||||
@@ -35,6 +36,7 @@ export default class VideoDetails {
|
||||
this.is_live = !!data.isLive;
|
||||
this.is_live_content = !!data.isLiveContent;
|
||||
this.is_upcoming = !!data.isUpcoming;
|
||||
this.is_post_live_dvr = !!data.isPostLiveDvr;
|
||||
this.is_crawlable = !!data.isCrawlable;
|
||||
}
|
||||
}
|
||||
211
src/parser/continuations.ts
Normal file
211
src/parser/continuations.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type { ObservedArray} from './helpers.js';
|
||||
import { YTNode, observe } from './helpers.js';
|
||||
import type { RawNode } from './index.js';
|
||||
import { Thumbnail } from './misc.js';
|
||||
import { NavigationEndpoint, LiveChatItemList, LiveChatHeader, LiveChatParticipantsList, Message } from './nodes.js';
|
||||
import * as Parser from './parser.js';
|
||||
|
||||
export class ItemSectionContinuation extends YTNode {
|
||||
static readonly type = 'itemSectionContinuation';
|
||||
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
continuation?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
if (Array.isArray(data.continuations)) {
|
||||
this.continuation = data.continuations?.at(0)?.nextContinuationData?.continuation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NavigateAction extends YTNode {
|
||||
static readonly type = 'navigateAction';
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.endpoint = new NavigationEndpoint(data.endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowMiniplayerCommand extends YTNode {
|
||||
static readonly type = 'showMiniplayerCommand';
|
||||
|
||||
miniplayer_command: NavigationEndpoint;
|
||||
show_premium_branding: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.miniplayer_command = new NavigationEndpoint(data.miniplayerCommand);
|
||||
this.show_premium_branding = data.showPremiumBranding;
|
||||
}
|
||||
}
|
||||
|
||||
export { default as AppendContinuationItemsAction } from './classes/actions/AppendContinuationItemsAction.js';
|
||||
|
||||
export class ReloadContinuationItemsCommand extends YTNode {
|
||||
static readonly type = 'reloadContinuationItemsCommand';
|
||||
|
||||
target_id: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
slot?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.target_id = data.targetId;
|
||||
this.contents = Parser.parse(data.continuationItems, true);
|
||||
this.slot = data?.slot;
|
||||
}
|
||||
}
|
||||
|
||||
export class SectionListContinuation extends YTNode {
|
||||
static readonly type = 'sectionListContinuation';
|
||||
|
||||
continuation: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parse(data.contents, true);
|
||||
this.continuation =
|
||||
data.continuations?.[0]?.nextContinuationData?.continuation ||
|
||||
data.continuations?.[0]?.reloadContinuationData?.continuation || null;
|
||||
}
|
||||
}
|
||||
|
||||
export class MusicPlaylistShelfContinuation extends YTNode {
|
||||
static readonly type = 'musicPlaylistShelfContinuation';
|
||||
|
||||
continuation: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parse(data.contents, true);
|
||||
this.continuation = data.continuations?.[0].nextContinuationData.continuation || null;
|
||||
}
|
||||
}
|
||||
|
||||
export class MusicShelfContinuation extends YTNode {
|
||||
static readonly type = 'musicShelfContinuation';
|
||||
|
||||
continuation: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
this.continuation =
|
||||
data.continuations?.[0].nextContinuationData?.continuation ||
|
||||
data.continuations?.[0].reloadContinuationData?.continuation || null;
|
||||
}
|
||||
}
|
||||
|
||||
export class GridContinuation extends YTNode {
|
||||
static readonly type = 'gridContinuation';
|
||||
|
||||
continuation: string;
|
||||
items: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parse(data.items, true);
|
||||
this.continuation = data.continuations?.[0].nextContinuationData.continuation || null;
|
||||
}
|
||||
|
||||
get contents() {
|
||||
return this.items;
|
||||
}
|
||||
}
|
||||
|
||||
export class PlaylistPanelContinuation extends YTNode {
|
||||
static readonly type = 'playlistPanelContinuation';
|
||||
|
||||
continuation: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
this.continuation = data.continuations?.[0]?.nextContinuationData?.continuation ||
|
||||
data.continuations?.[0]?.nextRadioContinuationData?.continuation || null;
|
||||
}
|
||||
}
|
||||
|
||||
export class Continuation extends YTNode {
|
||||
static readonly type = 'continuation';
|
||||
|
||||
continuation_type: string;
|
||||
timeout_ms?: number;
|
||||
time_until_last_message_ms?: number;
|
||||
token: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.continuation_type = data.type;
|
||||
this.timeout_ms = data.continuation?.timeoutMs;
|
||||
this.time_until_last_message_ms = data.continuation?.timeUntilLastMessageMsec;
|
||||
this.token = data.continuation?.continuation;
|
||||
}
|
||||
}
|
||||
|
||||
export class LiveChatContinuation extends YTNode {
|
||||
static readonly type = 'liveChatContinuation';
|
||||
|
||||
actions: ObservedArray<YTNode>;
|
||||
action_panel: YTNode | null;
|
||||
item_list: LiveChatItemList | null;
|
||||
header: LiveChatHeader | null;
|
||||
participants_list: LiveChatParticipantsList | null;
|
||||
popout_message: Message | null;
|
||||
emojis: {
|
||||
emoji_id: string;
|
||||
shortcuts: string[];
|
||||
search_terms: string[];
|
||||
image: Thumbnail[];
|
||||
}[];
|
||||
continuation: Continuation;
|
||||
viewer_name: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.actions = Parser.parse(data.actions?.map((action: any) => {
|
||||
delete action.clickTrackingParams;
|
||||
return action;
|
||||
}), true) || observe<YTNode>([]);
|
||||
|
||||
this.action_panel = Parser.parseItem(data.actionPanel);
|
||||
this.item_list = Parser.parseItem(data.itemList, LiveChatItemList);
|
||||
this.header = Parser.parseItem(data.header, LiveChatHeader);
|
||||
this.participants_list = Parser.parseItem(data.participantsList, LiveChatParticipantsList);
|
||||
this.popout_message = Parser.parseItem(data.popoutMessage, Message);
|
||||
|
||||
this.emojis = data.emojis?.map((emoji: any) => ({
|
||||
emoji_id: emoji.emojiId,
|
||||
shortcuts: emoji.shortcuts,
|
||||
search_terms: emoji.searchTerms,
|
||||
image: Thumbnail.fromResponse(emoji.image),
|
||||
is_custom_emoji: emoji.isCustomEmoji
|
||||
})) || [];
|
||||
|
||||
let continuation, type;
|
||||
|
||||
if (data.continuations?.[0].timedContinuationData) {
|
||||
type = 'timed';
|
||||
continuation = data.continuations?.[0].timedContinuationData;
|
||||
} else if (data.continuations?.[0].invalidationContinuationData) {
|
||||
type = 'invalidation';
|
||||
continuation = data.continuations?.[0].invalidationContinuationData;
|
||||
} else if (data.continuations?.[0].liveChatReplayContinuationData) {
|
||||
type = 'replay';
|
||||
continuation = data.continuations?.[0].liveChatReplayContinuationData;
|
||||
}
|
||||
|
||||
this.continuation = new Continuation({ continuation, type });
|
||||
|
||||
this.viewer_name = data.viewerName;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user