Compare commits

...

101 Commits

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

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

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

* refactor!: replace Parser class with pure functions

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

* refactor!: replace Proto class with pure functions

* chore: update package-lock.json

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

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

* refactor: implement changes from #462

* chore: lint

* fix: deno support

* fix: render valid xml document

* fix: wrong function call in DashUtils

* fix: typo in parser

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

* refactor!: move streaming info logic into seperate function

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

* chore: lint

* refactor: readability improvements & fixes

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

* build: remove unnecessary step in deno build

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

* refactor: move types to `types` directory

* docs: add back comments lost during refactor

* chore: lint

---------

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

* chore: lint

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

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

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

* chore(package): update lock file

* chore: lint

---------

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

* feat(Channel): Add `getPodcasts` method

* Fix(Playlist): Parse `PlaylistCustomThumbnail`
2023-07-13 15:25:20 -03:00
github-actions[bot]
23c22a93c4 chore(main): release 5.3.0 (#433)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-11 15:48:51 -03:00
absidue
1ca20836bf perf(Format): Cleanup the xtags parsing (#434) 2023-07-11 15:45:42 -03:00
absidue
5f058e69ae perf(toDash): Hoist duplicates from Representation to AdaptationSet (#431) 2023-07-11 15:22:02 -03:00
absidue
3500e92632 feat(toDash): Add color information (#430) 2023-07-10 21:25:48 -03:00
LuanRT
3f57c2fa5c refactor(PlayerEndpoint.ts): send specific params only if using Android based clients 2023-07-10 21:23:10 -03:00
LuanRT
7528ebdb60 chore: fix YouTube Music tests failing 2023-07-10 21:17:06 -03:00
github-actions[bot]
5e3846259f chore(main): release 5.2.1 (#429)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-07-03 22:30:29 -03:00
LuanRT
222dfce6bb fix: incorrect node parser implementations (#428)
These were causing some issues in v5.2.0.
2023-07-03 21:58:00 -03:00
github-actions[bot]
83cbfd631b chore(main): release 5.2.0 (#406)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-28 17:13:36 -03:00
RikThePixel
4f9427d752 fix(Playlist): Add thumbnail_renderer on Playlist when response includes it (#424) 2023-06-28 17:12:06 -03:00
Patrick Kan
07c1b3e0e5 fix(OAuth): client identity matching (#421) 2023-06-28 16:26:33 -03:00
absidue
89548ad48a chore(constants): Update web client version (#420) 2023-06-28 06:56:57 -03:00
absidue
519be72445 fix(PlayerEndpoint): Use different player params (#419)
* fix(PlayerEndpoint): Use different player params

* fix(PlayerEndpoint): Use new throttling bypass player params
2023-06-28 06:56:21 -03:00
Emma
e434bb2632 fix(VideoInfo.ts): reimplement get music_tracks (#409)
* fix(VideoInfo.ts): reimplement `get music_tracks`

- Add parser classes to parse needed data
  - Add `CarouselLockup`
  - Add `EngagementPanelSectionList`
  - Add `InfoRow`
  - Add `StructuredDescriptionContent`
  - Add `VideoDescriptionMusicSection`
  - Add `VideoDescriptionHeader`
  - Add `Factoid`
  - Add `ExpandableVideoDescriptionBody`
  - Add `AdsEngagementPanelContent`
- Add `engagement_panels` to raw and parsed next responses
- Add `engagement_panels` parsing code to `parser.ts`

* Check for song inside of video_lockup first before checking info_rows

* Add support for pulling artist ids out of music_tracks

- Add support for WRITERS InfoRow
- Check for video id inside of naviagation endpoint on info_row metadata

* Add `AdsEngagementPanelContent` to ignore list

* Switch `map => parseItem` to `parseArray`

* Use `Text` && `NavigationEndpoint`

* Replace `String` with `Text` in `ExpandableVideoDescriptionBody`
2023-06-28 06:54:55 -03:00
absidue
a11e5962c6 feat(VideoDetails): Add is_post_live_dvr property (#411) 2023-05-31 19:12:19 -03:00
Jake Reid Browning
77b39c79ee feat(ytmusic): Add support for YouTube Music mood filters (#404)
* add filters property to ytmusic HomeFeed

* remove implied type

* add applyFilter to HomeFeed

* add test

* remove section_list var
2023-05-23 15:00:05 -03:00
absidue
7c530d30ee chore(parser): Remove extra Array.from call (#407) 2023-05-20 18:00:15 -03:00
absidue
1e07a184ff perf(Search): Speed up results parsing (#408) 2023-05-20 17:57:48 -03:00
absidue
5de7b24dc5 perf(toDash): Speed up format filtering (#405) 2023-05-18 14:41:21 -03:00
github-actions[bot]
01fd1ee72a chore(main): release 5.1.0 (#403)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-14 01:12:16 -03:00
absidue
84b4f1efd1 feat(toDash): Add audio track labels to the manifest when available (#402) 2023-05-14 01:11:02 -03:00
absidue
046103a4d8 feat(ReelItem): Add accessibility label (#401) 2023-05-14 01:09:51 -03:00
github-actions[bot]
beb4733e84 chore(main): release 5.0.4 (#399)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-10 15:35:11 -03:00
absidue
66b026bf49 fix(Utils): Circular dependency introduced in 38a83c3c2a (#400) 2023-05-10 15:32:00 -03:00
absidue
26734194ab fix(bundles): Use ESM tslib build for the browser bundles (#397) 2023-05-06 15:54:18 -03:00
absidue
38a83c3c2a fix(Utils): Use instanceof in deepCompare instead of the constructor name (#398) 2023-05-06 15:47:51 -03:00
absidue
b1f19f16ac refactor(constants): Use namespace import internally (#396) 2023-05-06 15:46:20 -03:00
github-actions[bot]
891d889408 chore(main): release 5.0.3 (#395)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-03 19:47:01 -03:00
LuanRT
d4adb9eb6b chore(PlayerEndpoint.ts): add back attestationRequest field
This was accidentally removed in a recent PR. It tells InnerTube to omit the botguard program data.
2023-05-03 19:31:59 -03:00
LuanRT
3b0498b68b fix(Video): typo causing node parsing to fail 2023-05-02 03:21:02 -03:00
github-actions[bot]
154a5d2868 chore(main): release 5.0.2 (#394)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-30 17:24:14 -03:00
absidue
7c0abfccd7 fix(VideoInfo): Use microformat view_count when videoDetails view_count is NaN (#393) 2023-04-30 17:21:57 -03:00
github-actions[bot]
8f50c668aa chore(main): release 5.0.1 (#392)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-30 00:51:44 -03:00
LuanRT
4f7ec07c3f fix(web): slow downloads due to visitor data (#391)
* fix(web): slow downloads due to visitor data

It seems that YouTube will throttle clients if a shared visitor id is used.

* dev: include `params` for `/player` reqs
2023-04-30 00:50:08 -03:00
github-actions[bot]
ab3d5ab16c chore(main): release 5.0.0 (#389)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-29 02:14:26 -03:00
LuanRT
dd21f8c75a feat(NavigationEndpoint): parse content prop
This usually has dialog nodes.
2023-04-29 01:54:34 -03:00
LuanRT
3a7e58d2b9 chore(eslint): enforce the use of import type 2023-04-28 22:35:09 -03:00
LuanRT
75ea09dde8 fix(android): workaround streaming URLs returning 403 (#390) 2023-04-28 22:02:45 -03:00
LuanRT
95e0294eab refactor!: overhaul core classes and remove redundant code (#388)
* feat(Player.ts): append `cver` to deciphered URLs

* refactor(Actions.ts): remove redundant `getVideoInfo` function

This is leftover code from previous versions. It had many problems and it is no longer required.

* fix(Kids.ts): remove unneeded `await` keywords

* dev: add more endpoints

* chore: update deps

* refactor: separate endpoints into files

* dev: improve types

* dev: add more endpoints

* refactor: put clients in a separate directory inside `core`

* chore: lint

* refactor: move mixins and managers to separate folders

* chore: fix tests

* dev: add `CreateVideoEndpoint`

* chore: clean up

* chore: lint

* chore: add some comments

* chore: remove unnecessary test

* dev: add `playlist/CreateEndpoint`

* dev: add `playlist/DeleteEndpoint`

* dev: add `browse/EditPlaylistEndpoint`

* fix(parser): add a few checks to avoid parsing errors
2023-04-28 19:01:04 -03:00
LuanRT
22ae6c93ee chore(contributing.md): reword 2023-04-23 06:41:02 -03:00
LuanRT
257bd475a0 refactor: clean up parser and tests (#387)
* tests: improve coverage

* refactor: clean up nodes

* chore: lint

* feat(parser): ignore `BrandVideoShelf`

Seems to be used for ads.

* feat(parser): ignore `BrandVideoSingleton` too
2023-04-23 06:37:33 -03:00
github-actions[bot]
f66f0bd656 chore(main): release 4.3.0 (#384)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 07:52:42 -03:00
LuanRT
05de3ec97a feat(GridVideo): add upcoming, upcoming_text, is_reminder_set and buttons
Closes: #380
2023-04-13 07:50:18 -03:00
LuanRT
a0566969ba feat(ToggleMenuServiceItem): parse default nav endpoint 2023-04-13 06:10:17 -03:00
LuanRT
a9cad49333 feat(ytmusic): add taste builder nodes (#383)
Adds MusicTastebuilderShelf and MusicTastebuilderShelfThumbnail. These usually appear on new accounts.
2023-04-13 05:37:49 -03:00
LuanRT
096bf362c9 feat(MusicResponsiveListItem): make flex/fixed cols public (#382)
Plus refactor a few things.
2023-04-13 05:35:46 -03:00
LuanRT
ec9c0979f5 refactor: fix inconsistencies in the guide nodes (#379) 2023-04-11 05:52:47 -03:00
LuanRT
342d1d95e9 chore: fix readme formatting [skip ci] 2023-04-11 05:06:03 -03:00
511 changed files with 10366 additions and 12015 deletions

View File

@@ -8,6 +8,8 @@ extends: [ eslint:recommended, 'plugin:@typescript-eslint/recommended' ]
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: latest
project:
- tsconfig.json
overrides:
-
files:
@@ -30,6 +32,8 @@ rules:
'@typescript-eslint/ban-types': 'off'
'tsdoc/syntax': 'warn'
'@typescript-eslint/no-explicit-any': 'off'
'@typescript-eslint/consistent-type-imports': 'error'
'@typescript-eslint/consistent-type-exports': 'error'
no-template-curly-in-string: error
no-unreachable-loop: error

View File

@@ -1,5 +1,233 @@
# Changelog
## [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)
### Bug Fixes
* **VideoInfo:** Use microformat view_count when videoDetails view_count is NaN ([#393](https://github.com/LuanRT/YouTube.js/issues/393)) ([7c0abfc](https://github.com/LuanRT/YouTube.js/commit/7c0abfccd78a6c291d898f898d73a4f16170e2a9))
## [5.0.1](https://github.com/LuanRT/YouTube.js/compare/v5.0.0...v5.0.1) (2023-04-30)
### Bug Fixes
* **web:** slow downloads due to visitor data ([#391](https://github.com/LuanRT/YouTube.js/issues/391)) ([4f7ec07](https://github.com/LuanRT/YouTube.js/commit/4f7ec07c3f689219b07e8291877c23b6fbf45fb1))
## [5.0.0](https://github.com/LuanRT/YouTube.js/compare/v4.3.0...v5.0.0) (2023-04-29)
### ⚠ BREAKING CHANGES
* overhaul core classes and remove redundant code ([#388](https://github.com/LuanRT/YouTube.js/issues/388))
### Features
* **NavigationEndpoint:** parse `content` prop ([dd21f8c](https://github.com/LuanRT/YouTube.js/commit/dd21f8c75ae1d76180faab4f0ef9ee40920966e3))
### Bug Fixes
* **android:** workaround streaming URLs returning 403 ([#390](https://github.com/LuanRT/YouTube.js/issues/390)) ([75ea09d](https://github.com/LuanRT/YouTube.js/commit/75ea09dde86b1bdf13b197d6e02701899300a371))
### Code Refactoring
* overhaul core classes and remove redundant code ([#388](https://github.com/LuanRT/YouTube.js/issues/388)) ([95e0294](https://github.com/LuanRT/YouTube.js/commit/95e0294eabfdb20bbee2a4bfb751fd101402c5d6))
## [4.3.0](https://github.com/LuanRT/YouTube.js/compare/v4.2.0...v4.3.0) (2023-04-13)
### Features
* **GridVideo:** add `upcoming`, `upcoming_text`, `is_reminder_set` and `buttons` ([05de3ec](https://github.com/LuanRT/YouTube.js/commit/05de3ec97a1fea92543b5e5f84933b86a07ab830)), closes [#380](https://github.com/LuanRT/YouTube.js/issues/380)
* **MusicResponsiveListItem:** make flex/fixed cols public ([#382](https://github.com/LuanRT/YouTube.js/issues/382)) ([096bf36](https://github.com/LuanRT/YouTube.js/commit/096bf362c9bd46a510ecb0d01623c70841e26e26))
* **ToggleMenuServiceItem:** parse default nav endpoint ([a056696](https://github.com/LuanRT/YouTube.js/commit/a0566969ba436f31ca3722d09442a0c6302235d7))
* **ytmusic:** add taste builder nodes ([#383](https://github.com/LuanRT/YouTube.js/issues/383)) ([a9cad49](https://github.com/LuanRT/YouTube.js/commit/a9cad49333aa85c98bbb96e5f2d5b57d9beeb0c7))
## [4.2.0](https://github.com/LuanRT/YouTube.js/compare/v4.1.1...v4.2.0) (2023-04-09)

View File

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

View File

@@ -1,4 +1,4 @@
Welcome to YouTube.js! We're thrilled to have you interested in contributing to our project. As a community-driven project, we believe in the power of collaboration and look forward to working with you. To get started, please follow our easy-to-follow guidelines:
Welcome to YouTube.js! We're thrilled to have you interested in contributing to our project. As a community-driven project, we believe in the power of collaboration and look forward to working with you. To get started, please follow our guidelines:
## Issues

View File

@@ -31,8 +31,8 @@
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://serpapi.com#gh-light-mode-only" target="_blank">
<img width="80" alt="SerpApi" src="https://luanrt.is-a.dev/assets/img/serpapi.svg" />
<a href="https://serpapi.com" target="_blank">
<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()`
@@ -786,7 +788,7 @@ We are immensely grateful to all the wonderful people who have contributed to th
## Contact
LuanRT - [@thesciencephile][twitter] - luan.lrt4@gmail.com
LuanRT - [@thesciencephile][twitter] - luanrt@thatsciencephile.com
Project Link: [https://github.com/LuanRT/YouTube.js][project]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "4.2.0",
"version": "6.1.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",
@@ -103,7 +104,7 @@
"pbkit": "^0.0.59",
"replace": "^1.2.2",
"ts-jest": "^28.0.8",
"typescript": "^4.9.5"
"typescript": "^5.0.0"
},
"bugs": {
"url": "https://github.com/LuanRT/YouTube.js/issues"

View File

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

View File

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

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

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

View File

@@ -1,49 +1,56 @@
import Session, { SessionOptions } from './core/Session.js';
import type { SessionOptions } from './core/Session.js';
import Session from './core/Session.js';
import NavigationEndpoint from './parser/classes/NavigationEndpoint.js';
import type Format from './parser/classes/misc/Format.js';
import Channel from './parser/youtube/Channel.js';
import Comments from './parser/youtube/Comments.js';
import Guide from './parser/youtube/Guide.js';
import HashtagFeed from './parser/youtube/HashtagFeed.js';
import History from './parser/youtube/History.js';
import HomeFeed from './parser/youtube/HomeFeed.js';
import Library from './parser/youtube/Library.js';
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 HashtagFeed from './parser/youtube/HashtagFeed.js';
import AccountManager from './core/AccountManager.js';
import Feed from './core/Feed.js';
import InteractionManager from './core/InteractionManager.js';
import YTKids from './core/Kids.js';
import YTMusic from './core/Music.js';
import PlaylistManager from './core/PlaylistManager.js';
import YTStudio from './core/Studio.js';
import TabbedFeed from './core/TabbedFeed.js';
import HomeFeed from './parser/youtube/HomeFeed.js';
import Guide from './parser/youtube/Guide.js';
import Proto from './proto/index.js';
import Constants from './utils/Constants.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 type Actions from './core/Actions.js';
import type Format from './parser/classes/misc/Format.js';
import * as Proto from './proto/index.js';
import * as Constants from './utils/Constants.js';
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.js';
import {
BrowseEndpoint,
GetNotificationMenuEndpoint,
GuideEndpoint,
NextEndpoint,
PlayerEndpoint,
ResolveURLEndpoint,
SearchEndpoint
} from './core/endpoints/index.js';
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 { DownloadOptions, FormatOptions } from './utils/FormatUtils.js';
import { generateRandomString, InnertubeError, throwIfMissing } from './utils/Utils.js';
import type { INextRequest } from './types/index.js';
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.js';
export type InnertubeConfig = SessionOptions;
export interface SearchFilters {
upload_date?: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';
type?: 'all' | 'video' | 'channel' | 'playlist' | 'movie';
duration?: 'all' | 'short' | 'medium' | 'long';
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count';
features?: ('hd' | 'subtitles' | 'creative_commons' | '3d' | 'live' | 'purchased' | '4k' | '360' | 'location' | 'hdr' | 'vr180')[];
}
export type InnerTubeClient = 'WEB' | 'iOS' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS';
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS'
export type SearchFilters = Partial<{
upload_date: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';
type: 'all' | 'video' | 'channel' | 'playlist' | 'movie';
duration: 'all' | 'short' | 'medium' | 'long';
sort_by: 'relevance' | 'rating' | 'upload_date' | 'view_count';
features: ('hd' | 'subtitles' | 'creative_commons' | '3d' | 'live' | 'purchased' | '4k' | '360' | 'location' | 'hdr' | 'vr180')[];
}>;
/**
* Provides access to various services and modules in the YouTube API.
@@ -67,48 +74,39 @@ export default class Innertube {
async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise<VideoInfo> {
throwIfMissing({ target });
let payload: {
videoId: string,
playlistId?: string,
params?: string,
playlistIndex?: number
};
let next_payload: INextRequest;
if (target instanceof NavigationEndpoint) {
const video_id = target.payload?.videoId;
if (!video_id)
throw new InnertubeError('Missing video id in endpoint payload.', target);
payload = {
videoId: video_id
};
if (target.payload.playlistId) {
payload.playlistId = target.payload.playlistId;
}
if (target.payload.params) {
payload.params = target.payload.params;
}
if (target.payload.index) {
payload.playlistIndex = target.payload.index;
}
next_payload = NextEndpoint.build({
video_id: target.payload?.videoId,
playlist_id: target.payload?.playlistId,
params: target.payload?.params,
playlist_index: target.payload?.index
});
} else if (typeof target === 'string') {
payload = {
videoId: target
};
next_payload = NextEndpoint.build({
video_id: target
});
} else {
throw new InnertubeError('Invalid target, expected either a video id or a valid NavigationEndpoint', target);
}
if (!next_payload.videoId)
throw new InnertubeError('Video id cannot be empty', next_payload);
const player_payload = PlayerEndpoint.build({
video_id: next_payload.videoId,
playlist_id: next_payload?.playlistId,
client: client,
sts: this.#session.player?.sts
});
const player_response = this.actions.execute(PlayerEndpoint.PATH, player_payload);
const next_response = this.actions.execute(NextEndpoint.PATH, next_payload);
const response = await Promise.all([ player_response, next_response ]);
const cpn = generateRandomString(16);
const initial_info = this.actions.getVideoInfo(payload.videoId, cpn, client);
const continuation = this.actions.execute('/next', payload);
const response = await Promise.all([ initial_info, continuation ]);
return new VideoInfo(response, this.actions, cpn);
}
@@ -120,8 +118,15 @@ export default class Innertube {
async getBasicInfo(video_id: string, client?: InnerTubeClient): Promise<VideoInfo> {
throwIfMissing({ video_id });
const response = await this.actions.execute(
PlayerEndpoint.PATH, PlayerEndpoint.build({
video_id: video_id,
client: client,
sts: this.#session.player?.sts
})
);
const cpn = generateRandomString(16);
const response = await this.actions.getVideoInfo(video_id, cpn, client);
return new VideoInfo([ response ], this.actions, cpn);
}
@@ -134,14 +139,11 @@ export default class Innertube {
async search(query: string, filters: SearchFilters = {}): Promise<Search> {
throwIfMissing({ query });
const args = {
query,
...{
params: filters ? Proto.encodeSearchFilters(filters) : undefined
}
};
const response = await this.actions.execute('/search', args);
const response = await this.actions.execute(
SearchEndpoint.PATH, SearchEndpoint.build({
query, params: filters ? Proto.encodeSearchFilters(filters) : undefined
})
);
return new Search(this.actions, response);
}
@@ -179,11 +181,13 @@ export default class Innertube {
async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise<Comments> {
throwIfMissing({ video_id });
const payload = Proto.encodeCommentsSectionParams(video_id, {
sort_by: sort_by || 'TOP_COMMENTS'
});
const response = await this.actions.execute('/next', { continuation: payload });
const response = await this.actions.execute(
NextEndpoint.PATH, NextEndpoint.build({
continuation: Proto.encodeCommentsSectionParams(video_id, {
sort_by: sort_by || 'TOP_COMMENTS'
})
})
);
return new Comments(this.actions, response.data);
}
@@ -192,7 +196,9 @@ export default class Innertube {
* Retrieves YouTube's home feed (aka recommendations).
*/
async getHomeFeed(): Promise<HomeFeed> {
const response = await this.actions.execute('/browse', { browseId: 'FEwhat_to_watch' });
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEwhat_to_watch' })
);
return new HomeFeed(this.actions, response);
}
@@ -200,7 +206,7 @@ export default class Innertube {
* Retrieves YouTube's content guide.
*/
async getGuide(): Promise<Guide> {
const response = await this.actions.execute('/guide');
const response = await this.actions.execute(GuideEndpoint.PATH);
return new Guide(response.data);
}
@@ -208,7 +214,9 @@ export default class Innertube {
* Returns the account's library.
*/
async getLibrary(): Promise<Library> {
const response = await this.actions.execute('/browse', { browseId: 'FElibrary' });
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FElibrary' })
);
return new Library(this.actions, response);
}
@@ -217,7 +225,9 @@ export default class Innertube {
* Which can also be achieved with {@link getLibrary}.
*/
async getHistory(): Promise<History> {
const response = await this.actions.execute('/browse', { browseId: 'FEhistory' });
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEhistory' })
);
return new History(this.actions, response);
}
@@ -225,7 +235,9 @@ export default class Innertube {
* Retrieves trending content.
*/
async getTrending(): Promise<TabbedFeed<IBrowseResponse>> {
const response = await this.actions.execute('/browse', { browseId: 'FEtrending', parse: true });
const response = await this.actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEtrending' }), parse: true }
);
return new TabbedFeed(this.actions, response);
}
@@ -233,7 +245,9 @@ export default class Innertube {
* Retrieves subscriptions feed.
*/
async getSubscriptionsFeed(): Promise<Feed<IBrowseResponse>> {
const response = await this.actions.execute('/browse', { browseId: 'FEsubscriptions', parse: true });
const response = await this.actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEsubscriptions' }), parse: true }
);
return new Feed(this.actions, response);
}
@@ -243,7 +257,9 @@ export default class Innertube {
*/
async getChannel(id: string): Promise<Channel> {
throwIfMissing({ id });
const response = await this.actions.execute('/browse', { browseId: id });
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: id })
);
return new Channel(this.actions, response);
}
@@ -251,7 +267,11 @@ export default class Innertube {
* Retrieves notifications.
*/
async getNotifications(): Promise<NotificationsMenu> {
const response = await this.actions.execute('/notification/get_notification_menu', { notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' });
const response = await this.actions.execute(
GetNotificationMenuEndpoint.PATH, GetNotificationMenuEndpoint.build({
notifications_menu_request_type: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX'
})
);
return new NotificationsMenu(this.actions, response);
}
@@ -259,7 +279,7 @@ export default class Innertube {
* Retrieves unseen notifications count.
*/
async getUnseenNotificationsCount(): Promise<number> {
const response = await this.actions.execute('/notification/get_unseen_count');
const response = await this.actions.execute(GetUnseenCountEndpoint.PATH);
// TODO: properly parse this
return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0;
}
@@ -275,7 +295,9 @@ export default class Innertube {
id = `VL${id}`;
}
const response = await this.actions.execute('/browse', { browseId: id });
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: id })
);
return new Playlist(this.actions, response);
}
@@ -287,8 +309,12 @@ export default class Innertube {
async getHashtag(hashtag: string): Promise<HashtagFeed> {
throwIfMissing({ hashtag });
const params = Proto.encodeHashtag(hashtag);
const response = await this.actions.execute('/browse', { browseId: 'FEhashtag', params });
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
browse_id: 'FEhashtag',
params: Proto.encodeHashtag(hashtag)
})
);
return new HashtagFeed(this.actions, response);
}
@@ -322,7 +348,9 @@ export default class Innertube {
* @param url - The URL.
*/
async resolveURL(url: string): Promise<NavigationEndpoint> {
const response = await this.actions.execute('/navigation/resolve_url', { url, parse: true });
const response = await this.actions.execute(
ResolveURLEndpoint.PATH, { ...ResolveURLEndpoint.build({ url }), parse: true }
);
return response.endpoint;
}
@@ -338,58 +366,58 @@ export default class Innertube {
}
/**
* An instance of YTMusic for interacting with the YouTube Music service.
* An interface for interacting with YouTube Music.
*/
get music(): YTMusic {
return new YTMusic(this.#session);
get music() {
return new Music(this.#session);
}
/**
* An instance of YTStudio for interacting with the YouTube Studio service.
* An interface for interacting with YouTube Studio.
*/
get studio(): YTStudio {
return new YTStudio(this.#session);
get studio() {
return new Studio(this.#session);
}
/**
* An instance of YTKids for interacting with the YouTube Kids service.
* An interface for interacting with YouTube Kids.
*/
get kids(): YTKids {
return new YTKids(this.#session);
get kids() {
return new Kids(this.#session);
}
/**
* An instance of AccountManager for managing a user's account.
* An interface for managing and retrieving account information.
*/
get account(): AccountManager {
get account() {
return new AccountManager(this.#session.actions);
}
/**
* An instance of PlaylistManager for managing playlists.
* An interface for managing playlists.
*/
get playlist(): PlaylistManager {
get playlist() {
return new PlaylistManager(this.#session.actions);
}
/**
* An instance of InteractionManager for interacting with contents in YouTube.
* An interface for directly interacting with certain YouTube features.
*/
get interact(): InteractionManager {
get interact() {
return new InteractionManager(this.#session.actions);
}
/**
* An instance of Actions.
* An internal class used to dispatch requests.
*/
get actions(): Actions {
get actions() {
return this.#session.actions;
}
/**
* Returns the InnerTube session instance.
* The session used by this instance.
*/
get session(): Session {
get session() {
return this.#session;
}
}

View File

@@ -28,7 +28,7 @@ export type ParsedResponse<T> =
T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse :
IParsedResponse;
class Actions {
export default class Actions {
#session: Session;
constructor(session: Session) {
@@ -51,57 +51,6 @@ class Actions {
};
}
/**
* Used to retrieve video info.
* @param id - The video ID.
* @param cpn - Content Playback Nonce.
* @param client - The client to use.
* @param playlist_id - The playlist ID.
*/
async getVideoInfo(id: string, cpn?: string, client?: string, playlist_id?: string): Promise<ApiResponse> {
const data: Record<string, any> = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
referer: 'https://www.youtube.com',
currentUrl: `/watch?v=${id}`,
autonavState: 'STATE_NONE',
signatureTimestamp: this.#session.player?.sts || 0,
autoCaptionsDefaultOn: false,
html5Preference: 'HTML5_PREF_WANTS',
lactMilliseconds: '-1'
}
},
attestationRequest: {
omitBotguardData: true
},
videoId: id
};
if (client) {
data.client = client;
}
if (cpn) {
data.cpn = cpn;
}
if (playlist_id) {
data.playlistId = playlist_id;
}
const response = await this.#session.http.fetch('/player', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Makes calls to the playback tracking API.
* @param url - The URL to call.
@@ -226,6 +175,4 @@ class Actions {
'SPtime_watched'
].includes(id);
}
}
export default Actions;
}

View File

@@ -1,68 +0,0 @@
import Search from '../parser/ytkids/Search.js';
import HomeFeed from '../parser/ytkids/HomeFeed.js';
import VideoInfo from '../parser/ytkids/VideoInfo.js';
import Channel from '../parser/ytkids/Channel.js';
import type Session from './Session.js';
import { generateRandomString } from '../utils/Utils.js';
class Kids {
#session: Session;
constructor(session: Session) {
this.#session = session;
}
/**
* Searches the given query.
* @param query - The query.
*/
async search(query: string): Promise<Search> {
const response = await this.#session.actions.execute('/search', { query, client: 'YTKIDS' });
return new Search(this.#session.actions, response);
}
/**
* Retrieves video info.
* @param video_id - The video id.
*/
async getInfo(video_id: string): Promise<VideoInfo> {
const cpn = generateRandomString(16);
const initial_info = this.#session.actions.execute('/player', {
cpn,
client: 'YTKIDS',
videoId: video_id,
playbackContext: {
contentPlaybackContext: {
signatureTimestamp: this.#session.player?.sts || 0
}
}
});
const continuation = this.#session.actions.execute('/next', { videoId: video_id, client: 'YTKIDS' });
const response = await Promise.all([ initial_info, continuation ]);
return new VideoInfo(response, this.#session.actions, cpn);
}
/**
* Retrieves the contents of the given channel.
* @param channel_id - The channel id.
*/
async getChannel(channel_id: string): Promise<Channel> {
const response = await this.#session.actions.execute('/browse', { browseId: channel_id, client: 'YTKIDS' });
return new Channel(this.#session.actions, response);
}
/**
* Retrieves the home feed.
*/
async getHomeFeed(): Promise<HomeFeed> {
const response = await this.#session.actions.execute('/browse', { browseId: 'FEkids_home', client: 'YTKIDS' });
return new HomeFeed(this.#session.actions, response);
}
}
export default Kids;

View File

@@ -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';
@@ -28,7 +28,7 @@ export type OAuthAuthEventHandler = (data: {
export type OAuthAuthPendingEventHandler = (data: OAuthAuthPendingData) => any;
export type OAuthAuthErrorEventHandler = (err: OAuthError) => any;
class OAuth {
export default class OAuth {
#identity?: Record<string, string>;
#session: Session;
#credentials?: Credentials;
@@ -96,7 +96,7 @@ 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), {
@@ -264,6 +264,4 @@ class OAuth {
Reflect.has(this.#credentials, 'refresh_token') &&
Reflect.has(this.#credentials, 'expires') || false;
}
}
export default OAuth;
}

View File

@@ -1,10 +1,13 @@
import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.js';
import Constants from '../utils/Constants.js';
import * as Constants from '../utils/Constants.js';
import { ICache } from '../types/Cache.js';
import { FetchFunction } from '../types/PlatformShim.js';
import type { ICache } from '../types/Cache.js';
import type { FetchFunction } from '../types/PlatformShim.js';
/**
* Represents YouTube's player script. This is required to decipher signatures.
*/
export default class Player {
#nsig_sc;
#sig_sc;
@@ -104,6 +107,29 @@ export default class Player {
url_components.searchParams.set('n', nsig);
}
const client = url_components.searchParams.get('c');
switch (client) {
case 'WEB':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB.VERSION);
break;
case 'WEB_REMIX':
url_components.searchParams.set('cver', Constants.CLIENTS.YTMUSIC.VERSION);
break;
case 'WEB_KIDS':
url_components.searchParams.set('cver', Constants.CLIENTS.WEB_KIDS.VERSION);
break;
case 'ANDROID':
url_components.searchParams.set('cver', Constants.CLIENTS.ANDROID.VERSION);
break;
case 'ANDROID_MUSIC':
url_components.searchParams.set('cver', Constants.CLIENTS.YTMUSIC_ANDROID.VERSION);
break;
case 'TVHTML5_SIMPLY_EMBEDDED_PLAYER':
url_components.searchParams.set('cver', Constants.CLIENTS.TV_EMBEDDED.VERSION);
break;
}
return url_components.toString();
}

View File

@@ -1,19 +1,22 @@
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 { ICache } from '../types/Cache.js';
import { FetchFunction } from '../types/PlatformShim.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';
import { DeviceCategory, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.js';
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
import type { DeviceCategory } from '../utils/Utils.js';
import { generateRandomString, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.js';
import type { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
import OAuth from './OAuth.js';
export enum ClientType {
WEB = 'WEB',
KIDS = 'WEB_KIDS',
MUSIC = 'WEB_REMIX',
IOS = 'iOS',
ANDROID = 'ANDROID',
ANDROID_MUSIC = 'ANDROID_MUSIC',
ANDROID_CREATOR = 'ANDROID_CREATOR',
@@ -30,7 +33,6 @@ export interface Context {
screenPixelDensity: number;
screenWidthPoints: number;
visitorData: string;
userAgent: string;
clientName: string;
clientVersion: string;
clientScreen?: string,
@@ -41,6 +43,7 @@ export interface Context {
clientFormFactor: string;
userInterfaceTheme: string;
timeZone: string;
userAgent?: string;
browserName?: string;
browserVersion?: string;
originalUrl: string;
@@ -64,9 +67,6 @@ export interface Context {
thirdParty?: {
embedUrl: string;
};
request: {
useSsl: true;
};
}
export interface SessionOptions {
@@ -135,6 +135,9 @@ export interface SessionData {
api_version: string;
}
/**
* Represents an InnerTube session. This holds all the data needed to make requests to YouTube.
*/
export default class Session extends EventEmitterLike {
#api_version: string;
#key: string;
@@ -234,7 +237,7 @@ export default class Session extends EventEmitterLike {
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
let visitor_id = generateRandomString(11);
if (options.visitor_data) {
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
@@ -273,7 +276,6 @@ export default class Session extends EventEmitterLike {
screenPixelDensity: 1,
screenWidthPoints: 1920,
visitorData: device_info[13],
userAgent: device_info[14],
clientName: options.client_name,
clientVersion: device_info[16],
osName: device_info[17],
@@ -287,14 +289,11 @@ export default class Session extends EventEmitterLike {
originalUrl: Constants.URLS.YT_BASE,
deviceMake: device_info[11],
deviceModel: device_info[12],
utcOffsetMinutes: new Date().getTimezoneOffset()
utcOffsetMinutes: -new Date().getTimezoneOffset()
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
},
request: {
useSsl: true
}
};
@@ -310,7 +309,7 @@ export default class Session extends EventEmitterLike {
enable_safety_mode: boolean;
visitor_data: string;
}): SessionData {
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
let visitor_id = generateRandomString(11);
if (options.visitor_data) {
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
@@ -326,9 +325,8 @@ export default class Session extends EventEmitterLike {
screenPixelDensity: 1,
screenWidthPoints: 1920,
visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)),
userAgent: getRandomUserAgent('desktop'),
clientName: options.client_name,
clientVersion: CLIENTS.WEB.VERSION,
clientVersion: Constants.CLIENTS.WEB.VERSION,
osName: 'Windows',
osVersion: '10.0',
platform: options.device_category.toUpperCase(),
@@ -338,18 +336,15 @@ export default class Session extends EventEmitterLike {
originalUrl: Constants.URLS.YT_BASE,
deviceMake: '',
deviceModel: '',
utcOffsetMinutes: new Date().getTimezoneOffset()
utcOffsetMinutes: -new Date().getTimezoneOffset()
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
},
request: {
useSsl: true
}
};
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> {

83
src/core/clients/Kids.ts Normal file
View File

@@ -0,0 +1,83 @@
import Channel from '../../parser/ytkids/Channel.js';
import HomeFeed from '../../parser/ytkids/HomeFeed.js';
import Search from '../../parser/ytkids/Search.js';
import VideoInfo from '../../parser/ytkids/VideoInfo.js';
import type Session from '../Session.js';
import { generateRandomString } from '../../utils/Utils.js';
import {
BrowseEndpoint, NextEndpoint,
PlayerEndpoint, SearchEndpoint
} from '../endpoints/index.js';
export default class Kids {
#session: Session;
constructor(session: Session) {
this.#session = session;
}
/**
* Searches the given query.
* @param query - The query.
*/
async search(query: string): Promise<Search> {
const response = await this.#session.actions.execute(
SearchEndpoint.PATH, SearchEndpoint.build({ client: 'YTKIDS', query })
);
return new Search(this.#session.actions, response);
}
/**
* Retrieves video info.
* @param video_id - The video id.
*/
async getInfo(video_id: string): Promise<VideoInfo> {
const player_payload = PlayerEndpoint.build({
sts: this.#session.player?.sts,
client: 'YTKIDS',
video_id
});
const next_payload = NextEndpoint.build({
video_id,
client: 'YTKIDS'
});
const player_response = this.#session.actions.execute(PlayerEndpoint.PATH, player_payload);
const next_response = this.#session.actions.execute(NextEndpoint.PATH, next_payload);
const response = await Promise.all([ player_response, next_response ]);
const cpn = generateRandomString(16);
return new VideoInfo(response, this.#session.actions, cpn);
}
/**
* Retrieves the contents of the given channel.
* @param channel_id - The channel id.
*/
async getChannel(channel_id: string): Promise<Channel> {
const response = await this.#session.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
browse_id: channel_id,
client: 'YTKIDS'
})
);
return new Channel(this.#session.actions, response);
}
/**
* Retrieves the home feed.
*/
async getHomeFeed(): Promise<HomeFeed> {
const response = await this.#session.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
browse_id: 'FEkids_home',
client: 'YTKIDS'
})
);
return new HomeFeed(this.#session.actions, response);
}
}

View File

@@ -1,38 +1,41 @@
import Album from '../../parser/ytmusic/Album.js';
import Artist from '../../parser/ytmusic/Artist.js';
import Explore from '../../parser/ytmusic/Explore.js';
import HomeFeed from '../../parser/ytmusic/HomeFeed.js';
import Library from '../../parser/ytmusic/Library.js';
import Playlist from '../../parser/ytmusic/Playlist.js';
import Recap from '../../parser/ytmusic/Recap.js';
import Search from '../../parser/ytmusic/Search.js';
import TrackInfo from '../../parser/ytmusic/TrackInfo.js';
import Album from '../parser/ytmusic/Album.js';
import Artist from '../parser/ytmusic/Artist.js';
import Explore from '../parser/ytmusic/Explore.js';
import HomeFeed from '../parser/ytmusic/HomeFeed.js';
import Library from '../parser/ytmusic/Library.js';
import Playlist from '../parser/ytmusic/Playlist.js';
import Recap from '../parser/ytmusic/Recap.js';
import Search from '../parser/ytmusic/Search.js';
import TrackInfo from '../parser/ytmusic/TrackInfo.js';
import AutomixPreviewVideo from '../../parser/classes/AutomixPreviewVideo.js';
import Message from '../../parser/classes/Message.js';
import MusicCarouselShelf from '../../parser/classes/MusicCarouselShelf.js';
import MusicDescriptionShelf from '../../parser/classes/MusicDescriptionShelf.js';
import MusicQueue from '../../parser/classes/MusicQueue.js';
import MusicTwoRowItem from '../../parser/classes/MusicTwoRowItem.js';
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 * as Proto from '../../proto/index.js';
import AutomixPreviewVideo from '../parser/classes/AutomixPreviewVideo.js';
import Message from '../parser/classes/Message.js';
import MusicCarouselShelf from '../parser/classes/MusicCarouselShelf.js';
import MusicDescriptionShelf from '../parser/classes/MusicDescriptionShelf.js';
import MusicQueue from '../parser/classes/MusicQueue.js';
import MusicTwoRowItem from '../parser/classes/MusicTwoRowItem.js';
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 type { ObservedArray, YTNode } from '../../parser/helpers.js';
import type { MusicSearchFilters } from '../../types/index.js';
import { InnertubeError, generateRandomString, throwIfMissing } from '../../utils/Utils.js';
import type Actions from '../Actions.js';
import type Session from '../Session.js';
import { observe } from '../parser/helpers.js';
import Proto from '../proto/index.js';
import { generateRandomString, InnertubeError, throwIfMissing } from '../utils/Utils.js';
import {
BrowseEndpoint,
NextEndpoint,
PlayerEndpoint,
SearchEndpoint
} from '../endpoints/index.js';
import type { ObservedArray, YTNode } from '../parser/helpers.js';
import type Actions from './Actions.js';
import type Session from './Session.js';
import { GetSearchSuggestionsEndpoint } from '../endpoints/music/index.js';
export interface MusicSearchFilters {
type?: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist';
}
class Music {
export default class Music {
#session: Session;
#actions: Actions;
@@ -56,25 +59,23 @@ class Music {
}
async #fetchInfoFromVideoId(video_id: string): Promise<TrackInfo> {
const player_payload = PlayerEndpoint.build({
video_id,
sts: this.#session.player?.sts,
client: 'YTMUSIC'
});
const next_payload = NextEndpoint.build({
video_id,
client: 'YTMUSIC'
});
const player_response = this.#actions.execute(PlayerEndpoint.PATH, player_payload);
const next_response = this.#actions.execute(NextEndpoint.PATH, next_payload);
const response = await Promise.all([ player_response, next_response ]);
const cpn = generateRandomString(16);
const initial_info = this.#actions.execute('/player', {
cpn,
client: 'YTMUSIC',
videoId: video_id,
playbackContext: {
contentPlaybackContext: {
signatureTimestamp: this.#session.player?.sts || 0
}
}
});
const continuation = this.#actions.execute('/next', {
client: 'YTMUSIC',
videoId: video_id
});
const response = await Promise.all([ initial_info, continuation ]);
return new TrackInfo(response, this.#actions, cpn);
}
@@ -85,25 +86,26 @@ class Music {
if (!list_item.endpoint)
throw new Error('This item does not have an endpoint.');
const cpn = generateRandomString(16);
const initial_info = list_item.endpoint.call(this.#actions, {
cpn,
const player_response = list_item.endpoint.call(this.#actions, {
client: 'YTMUSIC',
playbackContext: {
contentPlaybackContext: {
signatureTimestamp: this.#session.player?.sts || 0
...{
signatureTimestamp: this.#session.player?.sts
}
}
}
});
const continuation = list_item.endpoint.call(this.#actions, {
const next_response = list_item.endpoint.call(this.#actions, {
client: 'YTMUSIC',
enablePersistentPlaylistPanel: true,
override_endpoint: '/next'
});
const response = await Promise.all([ initial_info, continuation ]);
const cpn = generateRandomString(16);
const response = await Promise.all([ player_response, next_response ]);
return new TrackInfo(response, this.#actions, cpn);
}
@@ -115,17 +117,12 @@ class Music {
async search(query: string, filters: MusicSearchFilters = {}): Promise<Search> {
throwIfMissing({ query });
const payload: {
query: string;
client: string;
params?: string;
} = { query, client: 'YTMUSIC' };
if (filters.type && filters.type !== 'all') {
payload.params = Proto.encodeMusicSearchFilters(filters);
}
const response = await this.#actions.execute('/search', payload);
const response = await this.#actions.execute(
SearchEndpoint.PATH, SearchEndpoint.build({
query, client: 'YTMUSIC',
params: filters.type && filters.type !== 'all' ? Proto.encodeMusicSearchFilters(filters) : undefined
})
);
return new Search(response, this.#actions, Reflect.has(filters, 'type') && filters.type !== 'all');
}
@@ -134,10 +131,12 @@ class Music {
* Retrieves the home feed.
*/
async getHomeFeed(): Promise<HomeFeed> {
const response = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
browseId: 'FEmusic_home'
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
browse_id: 'FEmusic_home',
client: 'YTMUSIC'
})
);
return new HomeFeed(response, this.#actions);
}
@@ -146,10 +145,12 @@ class Music {
* Retrieves the Explore feed.
*/
async getExplore(): Promise<Explore> {
const response = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
browseId: 'FEmusic_explore'
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
client: 'YTMUSIC',
browse_id: 'FEmusic_explore'
})
);
return new Explore(response);
// TODO: return new Explore(response, this.#actions);
@@ -159,10 +160,12 @@ class Music {
* Retrieves the library.
*/
async getLibrary(): Promise<Library> {
const response = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
browseId: 'FEmusic_library_landing'
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
client: 'YTMUSIC',
browse_id: 'FEmusic_library_landing'
})
);
return new Library(response, this.#actions);
}
@@ -177,10 +180,12 @@ class Music {
if (!artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist'))
throw new InnertubeError('Invalid artist id', artist_id);
const response = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
browseId: artist_id
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
client: 'YTMUSIC',
browse_id: artist_id
})
);
return new Artist(response, this.#actions);
}
@@ -195,10 +200,12 @@ class Music {
if (!album_id.startsWith('MPR') && !album_id.startsWith('FEmusic_library_privately_owned_release'))
throw new InnertubeError('Invalid album id', album_id);
const response = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
browseId: album_id
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
client: 'YTMUSIC',
browse_id: album_id
})
);
return new Album(response);
}
@@ -214,10 +221,12 @@ class Music {
playlist_id = `VL${playlist_id}`;
}
const response = await this.#actions.execute('/browse', {
client: 'YTMUSIC',
browseId: playlist_id
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
client: 'YTMUSIC',
browse_id: playlist_id
})
);
return new Playlist(response, this.#actions);
}
@@ -230,13 +239,11 @@ class Music {
async getUpNext(video_id: string, automix = true): Promise<PlaylistPanel> {
throwIfMissing({ video_id });
const data = await this.#actions.execute('/next', {
videoId: video_id,
client: 'YTMUSIC',
parse: true
});
const response = await this.#actions.execute(
NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true }
);
const tabs = data.contents_memo?.getType(Tab);
const tabs = response.contents_memo?.getType(Tab);
const tab = tabs?.first();
@@ -278,13 +285,11 @@ class Music {
async getRelated(video_id: string): Promise<ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>> {
throwIfMissing({ video_id });
const data = await this.#actions.execute('/next', {
videoId: video_id,
client: 'YTMUSIC',
parse: true
});
const response = await this.#actions.execute(
NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true }
);
const tabs = data.contents_memo?.getType(Tab);
const tabs = response.contents_memo?.getType(Tab);
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
@@ -308,13 +313,11 @@ class Music {
async getLyrics(video_id: string): Promise<MusicDescriptionShelf | undefined> {
throwIfMissing({ video_id });
const data = await this.#actions.execute('/next', {
videoId: video_id,
client: 'YTMUSIC',
parse: true
});
const response = await this.#actions.execute(
NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true }
);
const tabs = data.contents_memo?.getType(Tab);
const tabs = response.contents_memo?.getType(Tab);
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
@@ -326,8 +329,8 @@ class Music {
if (!page.contents)
throw new InnertubeError('Unexpected response', page);
if (page.contents.item().key('type').string() === 'Message')
throw new InnertubeError(page.contents.item().as(Message).text, video_id);
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;
@@ -338,10 +341,12 @@ class Music {
* Retrieves recap.
*/
async getRecap(): Promise<Recap> {
const response = await this.#actions.execute('/browse', {
browseId: 'FEmusic_listening_review',
client: 'YTMUSIC_ANDROID'
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
client: 'YTMUSIC_ANDROID',
browse_id: 'FEmusic_listening_review'
})
);
return new Recap(response, this.#actions);
}
@@ -351,19 +356,16 @@ class Music {
* @param query - The query.
*/
async getSearchSuggestions(query: string): Promise<ObservedArray<YTNode>> {
const response = await this.#actions.execute('/music/get_search_suggestions', {
parse: true,
input: query,
client: 'YTMUSIC'
});
const response = await this.#actions.execute(
GetSearchSuggestionsEndpoint.PATH,
{ ...GetSearchSuggestionsEndpoint.build({ input: query }), parse: true }
);
const search_suggestions_section = response.contents_memo?.getType(SearchSuggestionsSection)?.[0];
if (!response.contents_memo)
throw new InnertubeError('Unexpected response', response);
if (!search_suggestions_section?.contents.is_array)
return observe([] as YTNode[]);
const search_suggestions_section = response.contents_memo.getType(SearchSuggestionsSection).first();
return search_suggestions_section?.contents.array();
return search_suggestions_section.contents;
}
}
export default Music;
}

View File

@@ -1,9 +1,12 @@
import Proto from '../proto/index.js';
import { Constants } from '../utils/index.js';
import { InnertubeError, MissingParamError, Platform } from '../utils/Utils.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 { ApiResponse } from './Actions.js';
import type Session from './Session.js';
import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Clients.js';
import type { ApiResponse } from '../Actions.js';
import type Session from '../Session.js';
import { CreateVideoEndpoint } from '../endpoints/upload/index.js';
interface UploadResult {
status: string;
@@ -18,25 +21,7 @@ interface InitialUploadData {
chunk_granularity: string;
}
export interface VideoMetadata {
title?: string;
description?: string;
tags?: string[];
category?: number;
license?: string;
age_restricted?: boolean;
made_for_kids?: boolean;
privacy?: 'PUBLIC' | 'PRIVATE' | 'UNLISTED';
}
export interface UploadedVideoMetadata {
title?: string;
description?: string;
privacy?: 'PUBLIC' | 'PRIVATE' | 'UNLISTED';
is_draft?: boolean;
}
class Studio {
export default class Studio {
#session: Session;
constructor(session: Session) {
@@ -69,7 +54,7 @@ class Studio {
}
/**
* Updates given video's metadata.
* Updates a given video's metadata.
* @example
* ```ts
* const response = await yt.studio.updateVideoMetadata('videoid', {
@@ -82,7 +67,7 @@ class Studio {
* });
* ```
*/
async updateVideoMetadata(video_id: string, metadata: VideoMetadata): Promise<ApiResponse> {
async updateVideoMetadata(video_id: string, metadata: UpdateVideoMetadataOptions): Promise<ApiResponse> {
if (!this.#session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
@@ -104,7 +89,7 @@ class Studio {
* const response = await yt.studio.upload(file.buffer, { title: 'Wow!' });
* ```
*/
async upload(file: BodyInit, metadata: UploadedVideoMetadata = {}): Promise<ApiResponse> {
async upload(file: BodyInit, metadata: UploadedVideoMetadataOptions = {}): Promise<ApiResponse> {
if (!this.#session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
@@ -174,38 +159,34 @@ class Studio {
return data;
}
async #setVideoMetadata(initial_data: InitialUploadData, upload_result: UploadResult, metadata: UploadedVideoMetadata) {
const metadata_payload = {
resourceId: {
scottyResourceId: {
id: upload_result.scottyResourceId
}
},
frontendUploadId: initial_data.frontend_upload_id,
initialMetadata: {
title: {
newTitle: metadata.title || new Date().toDateString()
async #setVideoMetadata(initial_data: InitialUploadData, upload_result: UploadResult, metadata: UploadedVideoMetadataOptions) {
const response = await this.#session.actions.execute(
CreateVideoEndpoint.PATH, CreateVideoEndpoint.build({
resource_id: {
scotty_resource_id: {
id: upload_result.scottyResourceId
}
},
description: {
newDescription: metadata.description || '',
shouldSegment: true
frontend_upload_id: initial_data.frontend_upload_id,
initial_metadata: {
title: {
new_title: metadata.title || new Date().toDateString()
},
description: {
new_description: metadata.description || '',
should_segment: true
},
privacy: {
new_privacy: metadata.privacy || 'PRIVATE'
},
draft_state: {
is_draft: metadata.is_draft
}
},
privacy: {
newPrivacy: metadata.privacy || 'PRIVATE'
},
draftState: {
isDraft: metadata.is_draft || false
}
}
};
const response = await this.#session.actions.execute('/upload/createvideo', {
client: 'ANDROID',
...metadata_payload
});
client: 'ANDROID'
})
);
return response;
}
}
export default Studio;
}

View File

@@ -0,0 +1,3 @@
export { default as Kids } from './Kids.js';
export { default as Music } from './Music.js';
export { default as Studio } from './Studio.js';

View File

@@ -0,0 +1,19 @@
import type { IBrowseRequest, BrowseEndpointOptions } from '../../types/index.js';
export const PATH = '/browse';
/**
* Builds a `/browse` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: BrowseEndpointOptions): IBrowseRequest {
return {
...{
browseId: opts.browse_id,
params: opts.params,
continuation: opts.continuation,
client: opts.client
}
};
}

View File

@@ -0,0 +1,16 @@
import type { IGetNotificationMenuRequest, GetNotificationMenuEndpointOptions } from '../../types/index.js';
export const PATH = '/notification/get_notification_menu';
/**
* Builds a `/get_notification_menu` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: GetNotificationMenuEndpointOptions): IGetNotificationMenuRequest {
return {
...{
notificationsMenuRequestType: opts.notifications_menu_request_type
}
};
}

View File

@@ -0,0 +1 @@
export const PATH = '/guide';

View File

@@ -0,0 +1,21 @@
import type { INextRequest, NextEndpointOptions } from '../../types/index.js';
export const PATH = '/next';
/**
* Builds a `/next` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: NextEndpointOptions): INextRequest {
return {
...{
videoId: opts.video_id,
playlistId: opts.playlist_id,
params: opts.params,
playlistIndex: opts.playlist_index,
client: opts.client,
continuation: opts.continuation
}
};
}

View File

@@ -0,0 +1,49 @@
import type { IPlayerRequest, PlayerEndpointOptions } from '../../types/index.js';
export const PATH = '/player';
/**
* Builds a `/player` request payload.
* @param opts - The options to use.
* @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: {
vis: 0,
splay: false,
referer: opts.playlist_id ?
`https://www.youtube.com/watch?v=${opts.video_id}&list=${opts.playlist_id}` :
`https://www.youtube.com/watch?v=${opts.video_id}`,
currentUrl: opts.playlist_id ?
`/watch?v=${opts.video_id}&list=${opts.playlist_id}` :
`/watch?v=${opts.video_id}`,
autonavState: 'STATE_ON',
autoCaptionsDefaultOn: false,
html5Preference: 'HTML5_PREF_WANTS',
lactMilliseconds: '-1',
...{
signatureTimestamp: opts.sts
}
}
},
attestationRequest: {
omitBotguardData: true
},
racyCheckOk: true,
contentCheckOk: true,
videoId: opts.video_id,
...{
client: opts.client,
playlistId: opts.playlist_id,
// Workaround streaming URLs returning 403 or getting throttled when using Android based clients.
params: is_android ? '2AMBCgIQBg' : opts.params
}
};
}

View File

@@ -0,0 +1,16 @@
import type { IResolveURLRequest, ResolveURLEndpointOptions } from '../../types/index.js';
export const PATH = '/navigation/resolve_url';
/**
* Builds a `/resolve_url` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: ResolveURLEndpointOptions): IResolveURLRequest {
return {
...{
url: opts.url
}
};
}

View File

@@ -0,0 +1,19 @@
import type { ISearchRequest, SearchEndpointOptions } from '../../types/index.js';
export const PATH = '/search';
/**
* Builds a `/search` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: SearchEndpointOptions): ISearchRequest {
return {
...{
query: opts.query,
params: opts.params,
continuation: opts.continuation,
client: opts.client
}
};
}

View File

@@ -0,0 +1,13 @@
import type { IAccountListRequest } from '../../../types/index.js';
export const PATH = '/account/accounts_list';
/**
* Builds a `/account/accounts_list` request payload.
* @returns The payload.
*/
export function build(): IAccountListRequest {
return {
client: 'ANDROID'
};
}

View File

@@ -0,0 +1 @@
export * as AccountListEndpoint from './AccountListEndpoint.js';

View File

@@ -0,0 +1,22 @@
import type { IEditPlaylistRequest, EditPlaylistEndpointOptions } from '../../../types/index.js';
export const PATH = '/browse/edit_playlist';
/**
* Builds a `/browse/edit_playlist` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: EditPlaylistEndpointOptions): IEditPlaylistRequest {
return {
playlistId: opts.playlist_id,
actions: opts.actions.map((action) => ({
action: action.action,
...{
addedVideoId: action.added_video_id,
setVideoId: action.set_video_id,
movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor
}
}))
};
}

View File

@@ -0,0 +1 @@
export * as EditPlaylistEndpoint from './EditPlaylistEndpoint.js';

View File

@@ -0,0 +1,15 @@
import type { IChannelEditDescriptionRequest, ChannelEditDescriptionEndpointOptions } from '../../../types/Endpoints.js';
export const PATH = '/channel/edit_description';
/**
* Builds a `/channel/edit_description` request payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: ChannelEditDescriptionEndpointOptions): IChannelEditDescriptionRequest {
return {
givenDescription: options.given_description,
client: 'ANDROID'
};
}

View File

@@ -0,0 +1,15 @@
import type { IChannelEditNameRequest, ChannelEditNameEndpointOptions } from '../../../types/index.js';
export const PATH = '/channel/edit_name';
/**
* Builds a `/channel/edit_name` request payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: ChannelEditNameEndpointOptions): IChannelEditNameRequest {
return {
givenName: options.given_name,
client: 'ANDROID'
};
}

View File

@@ -0,0 +1,2 @@
export * as EditNameEndpoint from './EditNameEndpoint.js';
export * as EditDescriptionEndpoint from './EditDescriptionEndpoint.js';

View File

@@ -0,0 +1,18 @@
import type { ICreateCommentRequest, CreateCommentEndpointOptions } from '../../../types/index.js';
export const PATH = '/comment/create_comment';
/**
* Builds a `/comment/create_comment` request payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: CreateCommentEndpointOptions): ICreateCommentRequest {
return {
commentText: options.comment_text,
createCommentParams: options.create_comment_params,
...{
client: options.client
}
};
}

View File

@@ -0,0 +1,17 @@
import type { IPerformCommentActionRequest, PerformCommentActionEndpointOptions } from '../../../types/index.js';
export const PATH = '/comment/perform_comment_action';
/**
* Builds a `/comment/perform_comment_action` request payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: PerformCommentActionEndpointOptions): IPerformCommentActionRequest {
return {
actions: options.actions,
...{
client: options.client
}
};
}

View File

@@ -0,0 +1,2 @@
export * as PerformCommentActionEndpoint from './PerformCommentActionEndpoint.js';
export * as CreateCommentEndpoint from './CreateCommentEndpoint.js';

View File

@@ -0,0 +1,18 @@
export * as BrowseEndpoint from './BrowseEndpoint.js';
export * as GetNotificationMenuEndpoint from './GetNotificationMenuEndpoint.js';
export * as GuideEndpoint from './GuideEndpoint.js';
export * as NextEndpoint from './NextEndpoint.js';
export * as PlayerEndpoint from './PlayerEndpoint.js';
export * as ResolveURLEndpoint from './ResolveURLEndpoint.js';
export * as SearchEndpoint from './SearchEndpoint.js';
export * as Account from './account/index.js';
export * as Browse from './browse/index.js';
export * as Channel from './channel/index.js';
export * as Comment from './comment/index.js';
export * as Like from './like/index.js';
export * as Music from './music/index.js';
export * as Notification from './notification/index.js';
export * as Playlist from './playlist/index.js';
export * as Subscription from './subscription/index.js';
export * as Upload from './upload/index.js';

View File

@@ -0,0 +1,19 @@
import type { IDislikeRequest, DislikeEndpointOptions } from '../../../types/index.js';
export const PATH = '/like/dislike';
/**
* Builds a `/like/dislike` endpoint payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: DislikeEndpointOptions): IDislikeRequest {
return {
target: {
videoId: options.target.video_id
},
...{
client: options.client
}
};
}

View File

@@ -0,0 +1,19 @@
import type { ILikeRequest, LikeEndpointOptions } from '../../../types/index.js';
export const PATH = '/like/like';
/**
* Builds a `/like/like` endpoint payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: LikeEndpointOptions): ILikeRequest {
return {
target: {
videoId: options.target.video_id
},
...{
client: options.client
}
};
}

View File

@@ -0,0 +1,19 @@
import type { IRemoveLikeRequest, RemoveLikeEndpointOptions } from '../../../types/index.js';
export const PATH = '/like/removelike';
/**
* Builds a `/like/removelike` endpoint payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: RemoveLikeEndpointOptions): IRemoveLikeRequest {
return {
target: {
videoId: options.target.video_id
},
...{
client: options.client
}
};
}

View File

@@ -0,0 +1,3 @@
export * as LikeEndpoint from './LikeEndpoint.js';
export * as DislikeEndpoint from './DislikeEndpoint.js';
export * as RemoveLikeEndpoint from './RemoveLikeEndpoint.js';

View File

@@ -0,0 +1,16 @@
import type { IMusicGetSearchSuggestionsRequest, MusicGetSearchSuggestionsEndpointOptions } from '../../../types/index.js';
export const PATH = '/music/get_search_suggestions';
/**
* Builds a `/music/get_search_suggestions` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: MusicGetSearchSuggestionsEndpointOptions): IMusicGetSearchSuggestionsRequest {
return {
input: opts.input,
client: 'YTMUSIC'
};
}

View File

@@ -0,0 +1 @@
export * as GetSearchSuggestionsEndpoint from './GetSearchSuggestionsEndpoint.js';

View File

@@ -0,0 +1 @@
export const PATH = '/notification/get_unseen_count';

View File

@@ -0,0 +1,17 @@
import type { IModifyChannelPreferenceRequest, ModifyChannelPreferenceEndpointOptions } from '../../../types/index.js';
export const PATH = '/notification/modify_channel_preference';
/**
* Builds a `/notification/modify_channel_preference` request payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: ModifyChannelPreferenceEndpointOptions): IModifyChannelPreferenceRequest {
return {
params: options.params,
...{
client: options.client
}
};
}

View File

@@ -0,0 +1,2 @@
export * as GetUnseenCountEndpoint from './GetUnseenCountEndpoint.js';
export * as ModifyChannelPreferenceEndpoint from './ModifyChannelPreferenceEndpoint.js';

View File

@@ -0,0 +1,15 @@
import type { ICreatePlaylistRequest, CreatePlaylistEndpointOptions } from '../../../types/index.js';
export const PATH = '/playlist/create';
/**
* Builds a `/playlist/create` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: CreatePlaylistEndpointOptions): ICreatePlaylistRequest {
return {
title: opts.title,
ids: opts.ids
};
}

View File

@@ -0,0 +1,14 @@
import type { IDeletePlaylistRequest, DeletePlaylistEndpointOptions } from '../../../types/index.js';
export const PATH = '/playlist/delete';
/**
* Builds a `/playlist/delete` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: DeletePlaylistEndpointOptions): IDeletePlaylistRequest {
return {
playlistId: opts.playlist_id
};
}

View File

@@ -0,0 +1,2 @@
export * as CreateEndpoint from './CreateEndpoint.js';
export * as DeleteEndpoint from './DeleteEndpoint.js';

View File

@@ -0,0 +1,18 @@
import type { ISubscribeRequest, SubscribeEndpointOptions } from '../../../types/index.js';
export const PATH = '/subscription/subscribe';
/**
* Builds a `/subscription/subscribe` endpoint payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: SubscribeEndpointOptions): ISubscribeRequest {
return {
channelIds: options.channel_ids,
...{
client: options.client,
params: options.params
}
};
}

View File

@@ -0,0 +1,18 @@
import type { IUnsubscribeRequest, UnsubscribeEndpointOptions } from '../../../types/index.js';
export const PATH = '/subscription/unsubscribe';
/**
* Builds a `/subscription/unsubscribe` endpoint payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: UnsubscribeEndpointOptions): IUnsubscribeRequest {
return {
channelIds: options.channel_ids,
...{
client: options.client,
params: options.params
}
};
}

View File

@@ -0,0 +1,2 @@
export * as SubscribeEndpoint from './SubscribeEndpoint.js';
export * as UnsubscribeEndpoint from './UnsubscribeEndpoint.js';

View File

@@ -0,0 +1,37 @@
import type { ICreateVideoRequest, CreateVideoEndpointOptions } from '../../../types/index.js';
export const PATH = '/upload/createvideo';
/**
* Builds a `/upload/createvideo` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: CreateVideoEndpointOptions): ICreateVideoRequest {
return {
resourceId: {
scottyResourceId: {
id: opts.resource_id.scotty_resource_id.id
}
},
frontendUploadId: opts.frontend_upload_id,
initialMetadata: {
title: {
newTitle: opts.initial_metadata.title.new_title
},
description: {
newDescription: opts.initial_metadata.description.new_description,
shouldSegment: opts.initial_metadata.description.should_segment
},
privacy: {
newPrivacy: opts.initial_metadata.privacy.new_privacy
},
draftState: {
isDraft: !!opts.initial_metadata.draft_state.is_draft
}
},
...{
client: opts.client
}
};
}

View File

@@ -0,0 +1 @@
export * as CreateVideoEndpoint from './CreateVideoEndpoint.js';

View File

@@ -1,38 +1,16 @@
export { default as AccountManager } from './AccountManager.js';
export * from './AccountManager.js';
export { default as Session } from './Session.js';
export * from './Session.js';
export { default as Actions } from './Actions.js';
export * from './Actions.js';
export { default as Feed } from './Feed.js';
export * from './Feed.js';
export { default as FilterableFeed } from './FilterableFeed.js';
export * from './FilterableFeed.js';
export { default as InteractionManager } from './InteractionManager.js';
export * from './InteractionManager.js';
export { default as Kids } from './Kids.js';
export * from './Kids.js';
export { default as Music } from './Music.js';
export * from './Music.js';
export { default as Player } from './Player.js';
export * from './Player.js';
export { default as OAuth } from './OAuth.js';
export * from './OAuth.js';
export { default as Player } from './Player.js';
export * from './Player.js';
export { default as PlaylistManager } from './PlaylistManager.js';
export * from './PlaylistManager.js';
export { default as Session } from './Session.js';
export * from './Session.js';
export { default as Studio } from './Studio.js';
export * from './Studio.js';
export { default as TabbedFeed } from './TabbedFeed.js';
export * from './TabbedFeed.js';
export * as Clients from './clients/index.js';
export * as Endpoints from './endpoints/index.js';
export * as Managers from './managers/index.js';
export * as Mixins from './mixins/index.js';

View File

@@ -1,15 +1,16 @@
import Proto from '../proto/index.js';
import type Actions from './Actions.js';
import type { ApiResponse } from './Actions.js';
import AccountInfo from '../../parser/youtube/AccountInfo.js';
import Analytics from '../../parser/youtube/Analytics.js';
import Settings from '../../parser/youtube/Settings.js';
import TimeWatched from '../../parser/youtube/TimeWatched.js';
import Analytics from '../parser/youtube/Analytics.js';
import TimeWatched from '../parser/youtube/TimeWatched.js';
import AccountInfo from '../parser/youtube/AccountInfo.js';
import Settings from '../parser/youtube/Settings.js';
import * as Proto from '../../proto/index.js';
import { InnertubeError } from '../../utils/Utils.js';
import { Account, BrowseEndpoint, Channel } from '../endpoints/index.js';
import { InnertubeError } from '../utils/Utils.js';
import type Actions from '../Actions.js';
import type { ApiResponse } from '../Actions.js';
class AccountManager {
export default class AccountManager {
#actions: Actions;
channel: {
@@ -30,10 +31,12 @@ class AccountManager {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
return this.#actions.execute('/channel/edit_name', {
givenName: new_name,
client: 'ANDROID'
});
return this.#actions.execute(
Channel.EditNameEndpoint.PATH,
Channel.EditNameEndpoint.build({
given_name: new_name
})
);
},
/**
* Edits channel description.
@@ -43,10 +46,12 @@ class AccountManager {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
return this.#actions.execute('/channel/edit_description', {
givenDescription: new_description,
client: 'ANDROID'
});
return this.#actions.execute(
Channel.EditDescriptionEndpoint.PATH,
Channel.EditDescriptionEndpoint.build({
given_description: new_description
})
);
},
/**
* Retrieves basic channel analytics.
@@ -62,7 +67,11 @@ class AccountManager {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const response = await this.#actions.execute('/account/accounts_list', { client: 'ANDROID' });
const response = await this.#actions.execute(
Account.AccountListEndpoint.PATH,
Account.AccountListEndpoint.build()
);
return new AccountInfo(response);
}
@@ -70,10 +79,12 @@ class AccountManager {
* Retrieves time watched statistics.
*/
async getTimeWatched(): Promise<TimeWatched> {
const response = await this.#actions.execute('/browse', {
browseId: 'SPtime_watched',
client: 'ANDROID'
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
browse_id: 'SPtime_watched',
client: 'ANDROID'
})
);
return new TimeWatched(response);
}
@@ -82,10 +93,11 @@ class AccountManager {
* Opens YouTube settings.
*/
async getSettings(): Promise<Settings> {
const response = await this.#actions.execute('/browse', {
browseId: 'SPaccount_overview'
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
browse_id: 'SPaccount_overview'
})
);
return new Settings(this.#actions, response);
}
@@ -95,16 +107,14 @@ class AccountManager {
async getAnalytics(): Promise<Analytics> {
const info = await this.getInfo();
const params = Proto.encodeChannelAnalyticsParams(info.footers?.endpoint.payload.browseId);
const response = await this.#actions.execute('/browse', {
browseId: 'FEanalytics_screen',
client: 'ANDROID',
params
});
const response = await this.#actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({
browse_id: 'FEanalytics_screen',
params: Proto.encodeChannelAnalyticsParams(info.footers?.endpoint.payload.browseId),
client: 'ANDROID'
})
);
return new Analytics(response);
}
}
export default AccountManager;
}

View File

@@ -1,9 +1,14 @@
import Proto from '../proto/index.js';
import type Actions from './Actions.js';
import type { ApiResponse } from './Actions.js';
import { throwIfMissing } from '../utils/Utils.js';
import * as Proto from '../../proto/index.js';
import type Actions from '../Actions.js';
import type { ApiResponse } from '../Actions.js';
class InteractionManager {
import { throwIfMissing } from '../../utils/Utils.js';
import { LikeEndpoint, DislikeEndpoint, RemoveLikeEndpoint } from '../endpoints/like/index.js';
import { SubscribeEndpoint, UnsubscribeEndpoint } from '../endpoints/subscription/index.js';
import { CreateCommentEndpoint, PerformCommentActionEndpoint } from '../endpoints/comment/index.js';
import { ModifyChannelPreferenceEndpoint } from '../endpoints/notification/index.js';
export default class InteractionManager {
#actions: Actions;
constructor(actions: Actions) {
@@ -20,12 +25,12 @@ class InteractionManager {
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const action = await this.#actions.execute('/like/like', {
client: 'ANDROID',
target: {
videoId: video_id
}
});
const action = await this.#actions.execute(
LikeEndpoint.PATH, LikeEndpoint.build({
client: 'ANDROID',
target: { video_id }
})
);
return action;
}
@@ -40,12 +45,12 @@ class InteractionManager {
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const action = await this.#actions.execute('/like/dislike', {
client: 'ANDROID',
target: {
videoId: video_id
}
});
const action = await this.#actions.execute(
DislikeEndpoint.PATH, DislikeEndpoint.build({
client: 'ANDROID',
target: { video_id }
})
);
return action;
}
@@ -60,12 +65,12 @@ class InteractionManager {
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const action = await this.#actions.execute('/like/removelike', {
client: 'ANDROID',
target: {
videoId: video_id
}
});
const action = await this.#actions.execute(
RemoveLikeEndpoint.PATH, RemoveLikeEndpoint.build({
client: 'ANDROID',
target: { video_id }
})
);
return action;
}
@@ -80,11 +85,13 @@ class InteractionManager {
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const action = await this.#actions.execute('/subscription/subscribe', {
client: 'ANDROID',
channelIds: [ channel_id ],
params: 'EgIIAhgA'
});
const action = await this.#actions.execute(
SubscribeEndpoint.PATH, SubscribeEndpoint.build({
client: 'ANDROID',
channel_ids: [ channel_id ],
params: 'EgIIAhgA'
})
);
return action;
}
@@ -93,17 +100,19 @@ class InteractionManager {
* Unsubscribes from a given channel.
* @param channel_id - The channel ID
*/
async unsubscribe(channel_id: string): Promise<ApiResponse>{
async unsubscribe(channel_id: string): Promise<ApiResponse> {
throwIfMissing({ channel_id });
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const action = await this.#actions.execute('/subscription/unsubscribe', {
client: 'ANDROID',
channelIds: [ channel_id ],
params: 'CgIIAhgA'
});
const action = await this.#actions.execute(
UnsubscribeEndpoint.PATH, UnsubscribeEndpoint.build({
client: 'ANDROID',
channel_ids: [ channel_id ],
params: 'CgIIAhgA'
})
);
return action;
}
@@ -119,11 +128,13 @@ class InteractionManager {
if (!this.#actions.session.logged_in)
throw new Error('You must be signed in to perform this operation.');
const action = await this.#actions.execute('/comment/create_comment', {
client: 'ANDROID',
commentText: text,
createCommentParams: Proto.encodeCommentParams(video_id)
});
const action = await this.#actions.execute(
CreateCommentEndpoint.PATH, CreateCommentEndpoint.build({
comment_text: text,
create_comment_params: Proto.encodeCommentParams(video_id),
client: 'ANDROID'
})
);
return action;
}
@@ -139,10 +150,12 @@ class InteractionManager {
const target_action = Proto.encodeCommentActionParams(22, { text, target_language, ...args });
const response = await this.#actions.execute('/comment/perform_comment_action', {
client: 'ANDROID',
actions: [ target_action ]
});
const response = await this.#actions.execute(
PerformCommentActionEndpoint.PATH, PerformCommentActionEndpoint.build({
client: 'ANDROID',
actions: [ target_action ]
})
);
const mutation = response.data.frameworkUpdates.entityBatchUpdate.mutations[0].payload.commentEntityPayload;
@@ -175,13 +188,13 @@ class InteractionManager {
if (!Object.keys(pref_types).includes(type.toUpperCase()))
throw new Error(`Invalid notification preference type: ${type}`);
const action = await this.#actions.execute('/notification/modify_channel_preference', {
client: 'WEB',
params: Proto.encodeNotificationPref(channel_id, pref_types[type.toUpperCase() as keyof typeof pref_types])
});
const action = await this.#actions.execute(
ModifyChannelPreferenceEndpoint.PATH, ModifyChannelPreferenceEndpoint.build({
client: 'WEB',
params: Proto.encodeNotificationPref(channel_id, pref_types[type.toUpperCase() as keyof typeof pref_types])
})
);
return action;
}
}
export default InteractionManager;
}

View File

@@ -1,10 +1,14 @@
import type Feed from './Feed.js';
import type Actions from './Actions.js';
import Playlist from '../parser/youtube/Playlist.js';
import Playlist from '../../parser/youtube/Playlist.js';
import type Actions from '../Actions.js';
import type Feed from '../mixins/Feed.js';
import { InnertubeError, throwIfMissing } from '../utils/Utils.js';
import type { EditPlaylistEndpointOptions } from '../../types/index.js';
import { InnertubeError, throwIfMissing } from '../../utils/Utils.js';
import { EditPlaylistEndpoint } from '../endpoints/browse/index.js';
import { BrowseEndpoint } from '../endpoints/index.js';
import { CreateEndpoint, DeleteEndpoint } from '../endpoints/playlist/index.js';
class PlaylistManager {
export default class PlaylistManager {
#actions: Actions;
constructor(actions: Actions) {
@@ -22,11 +26,12 @@ class PlaylistManager {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const response = await this.#actions.execute('/playlist/create', {
title,
ids: video_ids,
parse: false
});
const response = await this.#actions.execute(
CreateEndpoint.PATH, CreateEndpoint.build({
ids: video_ids,
title
})
);
return {
success: response.success,
@@ -46,7 +51,11 @@ class PlaylistManager {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const response = await this.#actions.execute('playlist/delete', { playlistId: playlist_id });
const response = await this.#actions.execute(
DeleteEndpoint.PATH, DeleteEndpoint.build({
playlist_id
})
);
return {
playlist_id,
@@ -67,14 +76,15 @@ class PlaylistManager {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const response = await this.#actions.execute('/browse/edit_playlist', {
playlistId: playlist_id,
actions: video_ids.map((id) => ({
action: 'ACTION_ADD_VIDEO',
addedVideoId: id
})),
parse: false
});
const response = await this.#actions.execute(
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build({
actions: video_ids.map((id) => ({
action: 'ACTION_ADD_VIDEO',
added_video_id: id
})),
playlist_id
})
);
return {
playlist_id,
@@ -93,23 +103,16 @@ class PlaylistManager {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const info = await this.#actions.execute('/browse', {
browseId: `VL${playlist_id}`,
parse: true
});
const info = await this.#actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: `VL${playlist_id}` }), parse: true }
);
const playlist = new Playlist(this.#actions, info, true);
if (!playlist.info.is_editable)
throw new InnertubeError('This playlist cannot be edited.', playlist_id);
const payload = {
playlistId: playlist_id,
actions: [] as {
action: string;
setVideoId: string;
}[]
};
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
const getSetVideoIds = async (pl: Feed): Promise<void> => {
const videos = pl.videos.filter((video) => video_ids.includes(video.key('id').string()));
@@ -117,7 +120,7 @@ class PlaylistManager {
videos.forEach((video) =>
payload.actions.push({
action: 'ACTION_REMOVE_VIDEO',
setVideoId: video.key('set_video_id').string()
set_video_id: video.key('set_video_id').string()
})
);
@@ -132,7 +135,9 @@ class PlaylistManager {
if (!payload.actions.length)
throw new InnertubeError('Given video ids were not found in this playlist.', video_ids);
const response = await this.#actions.execute('/browse/edit_playlist', { ...payload, parse: false });
const response = await this.#actions.execute(
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
);
return {
playlist_id,
@@ -152,24 +157,16 @@ class PlaylistManager {
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const info = await this.#actions.execute('/browse', {
browseId: `VL${playlist_id}`,
parse: true
});
const info = await this.#actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: `VL${playlist_id}` }), parse: true }
);
const playlist = new Playlist(this.#actions, info, true);
if (!playlist.info.is_editable)
throw new InnertubeError('This playlist cannot be edited.', playlist_id);
const payload = {
playlistId: playlist_id,
actions: [] as {
action: string,
setVideoId?: string,
movedSetVideoIdPredecessor?: string
}[]
};
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
let set_video_id_0: string | undefined, set_video_id_1: string | undefined;
@@ -190,20 +187,17 @@ class PlaylistManager {
payload.actions.push({
action: 'ACTION_MOVE_VIDEO_AFTER',
setVideoId: set_video_id_0,
movedSetVideoIdPredecessor: set_video_id_1
set_video_id: set_video_id_0,
moved_set_video_id_predecessor: set_video_id_1
});
const response = await this.#actions.execute('/browse/edit_playlist', {
...payload,
parse: false
});
const response = await this.#actions.execute(
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
);
return {
playlist_id,
action_result: response.data.actions // TODO: implement actions in the parser
};
}
}
export default PlaylistManager;
}

View File

@@ -0,0 +1,3 @@
export { default as AccountManager } from './AccountManager.js';
export { default as PlaylistManager } from './PlaylistManager.js';
export { default as InteractionManager } from './InteractionManager.js';

View File

@@ -1,40 +1,40 @@
import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../parser/helpers.js';
import Parser, { ReloadContinuationItemsCommand } from '../parser/index.js';
import { concatMemos, InnertubeError } from '../utils/Utils.js';
import type Actions from './Actions.js';
import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../../parser/helpers.js';
import Parser, { ReloadContinuationItemsCommand } from '../../parser/index.js';
import { concatMemos, InnertubeError } from '../../utils/Utils.js';
import type Actions from '../Actions.js';
import BackstagePost from '../parser/classes/BackstagePost.js';
import SharedPost from '../parser/classes/SharedPost.js';
import Channel from '../parser/classes/Channel.js';
import CompactVideo from '../parser/classes/CompactVideo.js';
import GridChannel from '../parser/classes/GridChannel.js';
import GridPlaylist from '../parser/classes/GridPlaylist.js';
import GridVideo from '../parser/classes/GridVideo.js';
import Playlist from '../parser/classes/Playlist.js';
import PlaylistPanelVideo from '../parser/classes/PlaylistPanelVideo.js';
import PlaylistVideo from '../parser/classes/PlaylistVideo.js';
import Post from '../parser/classes/Post.js';
import ReelItem from '../parser/classes/ReelItem.js';
import ReelShelf from '../parser/classes/ReelShelf.js';
import RichShelf from '../parser/classes/RichShelf.js';
import Shelf from '../parser/classes/Shelf.js';
import Tab from '../parser/classes/Tab.js';
import Video from '../parser/classes/Video.js';
import BackstagePost from '../../parser/classes/BackstagePost.js';
import SharedPost from '../../parser/classes/SharedPost.js';
import Channel from '../../parser/classes/Channel.js';
import CompactVideo from '../../parser/classes/CompactVideo.js';
import GridChannel from '../../parser/classes/GridChannel.js';
import GridPlaylist from '../../parser/classes/GridPlaylist.js';
import GridVideo from '../../parser/classes/GridVideo.js';
import Playlist from '../../parser/classes/Playlist.js';
import PlaylistPanelVideo from '../../parser/classes/PlaylistPanelVideo.js';
import PlaylistVideo from '../../parser/classes/PlaylistVideo.js';
import Post from '../../parser/classes/Post.js';
import ReelItem from '../../parser/classes/ReelItem.js';
import ReelShelf from '../../parser/classes/ReelShelf.js';
import RichShelf from '../../parser/classes/RichShelf.js';
import Shelf from '../../parser/classes/Shelf.js';
import Tab from '../../parser/classes/Tab.js';
import Video from '../../parser/classes/Video.js';
import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction.js';
import ContinuationItem from '../parser/classes/ContinuationItem.js';
import TwoColumnBrowseResults from '../parser/classes/TwoColumnBrowseResults.js';
import TwoColumnSearchResults from '../parser/classes/TwoColumnSearchResults.js';
import WatchCardCompactVideo from '../parser/classes/WatchCardCompactVideo.js';
import AppendContinuationItemsAction from '../../parser/classes/actions/AppendContinuationItemsAction.js';
import ContinuationItem from '../../parser/classes/ContinuationItem.js';
import TwoColumnBrowseResults from '../../parser/classes/TwoColumnBrowseResults.js';
import TwoColumnSearchResults from '../../parser/classes/TwoColumnSearchResults.js';
import WatchCardCompactVideo from '../../parser/classes/WatchCardCompactVideo.js';
import type MusicQueue from '../parser/classes/MusicQueue.js';
import type RichGrid from '../parser/classes/RichGrid.js';
import type SectionList from '../parser/classes/SectionList.js';
import type MusicQueue from '../../parser/classes/MusicQueue.js';
import type RichGrid from '../../parser/classes/RichGrid.js';
import type SectionList from '../../parser/classes/SectionList.js';
import type { IParsedResponse } from '../parser/types/index.js';
import type { ApiResponse } from './Actions.js';
import type { IParsedResponse } from '../../parser/types/index.js';
import type { ApiResponse } from '../Actions.js';
class Feed<T extends IParsedResponse = IParsedResponse> {
export default class Feed<T extends IParsedResponse = IParsedResponse> {
#page: T;
#continuation?: ObservedArray<ContinuationItem>;
#actions: Actions;
@@ -210,6 +210,4 @@ class Feed<T extends IParsedResponse = IParsedResponse> {
throw new InnertubeError('Could not get continuation data');
return new Feed<T>(this.actions, continuation_data, true);
}
}
export default Feed;
}

View File

@@ -1,14 +1,14 @@
import ChipCloudChip from '../parser/classes/ChipCloudChip.js';
import FeedFilterChipBar from '../parser/classes/FeedFilterChipBar.js';
import { InnertubeError } from '../utils/Utils.js';
import ChipCloudChip from '../../parser/classes/ChipCloudChip.js';
import FeedFilterChipBar from '../../parser/classes/FeedFilterChipBar.js';
import { InnertubeError } from '../../utils/Utils.js';
import Feed from './Feed.js';
import type { ObservedArray } from '../parser/helpers.js';
import type { IParsedResponse } from '../parser/types/ParsedResponse.js';
import type Actions from './Actions.js';
import type { ApiResponse } from './Actions.js';
import type { ObservedArray } from '../../parser/helpers.js';
import type { IParsedResponse } from '../../parser/types/ParsedResponse.js';
import type Actions from '../Actions.js';
import type { ApiResponse } from '../Actions.js';
class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
export default class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
#chips?: ObservedArray<ChipCloudChip>;
constructor(actions: Actions, data: ApiResponse | T, already_parsed = false) {
@@ -69,6 +69,4 @@ class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
return new Feed(this.actions, response, true);
}
}
export default FilterableFeed;
}

View File

@@ -1,11 +1,17 @@
import Actions, { ApiResponse } from './Actions.js';
import Constants from '../utils/Constants.js';
import FormatUtils, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../utils/FormatUtils.js';
import { InnertubeError } from '../utils/Utils.js';
import Format from '../parser/classes/misc/Format.js';
import Parser, { INextResponse, IPlayerResponse } from '../parser/index.js';
import type { ApiResponse } from '../Actions.js';
import type Actions from '../Actions.js';
import * as Constants from '../../utils/Constants.js';
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../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 class MediaInfo {
export default class MediaInfo {
#page: [IPlayerResponse, INextResponse?];
#actions: Actions;
#cpn: string;
@@ -34,10 +40,32 @@ export 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
);
}
/**

View File

@@ -1,13 +1,13 @@
import Tab from '../parser/classes/Tab.js';
import Tab from '../../parser/classes/Tab.js';
import Feed from './Feed.js';
import { InnertubeError } from '../utils/Utils.js';
import { InnertubeError } from '../../utils/Utils.js';
import type Actions from './Actions.js';
import type { ObservedArray } from '../parser/helpers.js';
import type { IParsedResponse } from '../parser/types/ParsedResponse.js';
import type { ApiResponse } from './Actions.js';
import type Actions from '../Actions.js';
import type { ObservedArray } from '../../parser/helpers.js';
import type { IParsedResponse } from '../../parser/types/ParsedResponse.js';
import type { ApiResponse } from '../Actions.js';
class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
export default class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
#tabs?: ObservedArray<Tab>;
#actions: Actions;
@@ -56,6 +56,4 @@ class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
get title(): string | undefined {
return this.page.contents_memo?.getType(Tab)?.find((tab) => tab.selected)?.title.toString();
}
}
export default TabbedFeed;
}

4
src/core/mixins/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export { default as Feed } from './Feed.js';
export { default as FilterableFeed } from './FilterableFeed.js';
export { default as TabbedFeed } from './TabbedFeed.js';
export { default as MediaInfo } from './MediaInfo.js';

View File

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

View File

@@ -3,7 +3,7 @@ import NavigationEndpoint from './NavigationEndpoint.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class AccountChannel extends YTNode {
export default class AccountChannel extends YTNode {
static type = 'AccountChannel';
title: Text;
@@ -14,6 +14,4 @@ class AccountChannel extends YTNode {
this.title = new Text(data.title);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
}
}
export default AccountChannel;
}

View File

@@ -1,14 +1,16 @@
import Parser from '../index.js';
import AccountItemSectionHeader from './AccountItemSectionHeader.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import AccountItemSectionHeader from './AccountItemSectionHeader.js';
import { YTNode } from '../helpers.js';
import { YTNode, observe, type ObservedArray } from '../helpers.js';
import type { RawNode } from '../index.js';
class AccountItem {
/**
* Not a real renderer but we treat it as one to keep things organized.
*/
export class AccountItem extends YTNode {
static type = 'AccountItem';
account_name: Text;
@@ -20,27 +22,26 @@ class AccountItem {
account_byline: Text;
constructor(data: RawNode) {
super();
this.account_name = new Text(data.accountName);
this.account_photo = Thumbnail.fromResponse(data.accountPhoto);
this.is_selected = data.isSelected;
this.is_disabled = data.isDisabled;
this.has_channel = data.hasChannel;
this.is_selected = !!data.isSelected;
this.is_disabled = !!data.isDisabled;
this.has_channel = !!data.hasChannel;
this.endpoint = new NavigationEndpoint(data.serviceEndpoint);
this.account_byline = new Text(data.accountByline);
}
}
class AccountItemSection extends YTNode {
export default class AccountItemSection extends YTNode {
static type = 'AccountItemSection';
contents;
header;
contents: ObservedArray<AccountItem>;
header: AccountItemSectionHeader | null;
constructor(data: RawNode) {
super();
this.contents = data.contents.map((ac: any) => new AccountItem(ac.accountItem));
this.contents = observe<AccountItem>(data.contents.map((ac: RawNode) => new AccountItem(ac.accountItem)));
this.header = Parser.parseItem(data.header, AccountItemSectionHeader);
}
}
export default AccountItemSection;
}

View File

@@ -1,7 +1,8 @@
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class AccountItemSectionHeader extends YTNode {
export default class AccountItemSectionHeader extends YTNode {
static type = 'AccountItemSectionHeader';
title: Text;
@@ -10,6 +11,4 @@ class AccountItemSectionHeader extends YTNode {
super();
this.title = new Text(data.title);
}
}
export default AccountItemSectionHeader;
}

View File

@@ -4,7 +4,8 @@ import AccountItemSection from './AccountItemSection.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class AccountSectionList extends YTNode {
export default class AccountSectionList extends YTNode {
static type = 'AccountSectionList';
contents;
@@ -15,6 +16,4 @@ class AccountSectionList extends YTNode {
this.contents = Parser.parseItem(data.contents[0], AccountItemSection);
this.footers = Parser.parseItem(data.footers[0], AccountChannel);
}
}
export default AccountSectionList;
}

View File

@@ -1,7 +1,8 @@
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class Alert extends YTNode {
export default class Alert extends YTNode {
static type = 'Alert';
text: Text;
@@ -12,6 +13,4 @@ class Alert extends YTNode {
this.text = new Text(data.text);
this.alert_type = data.type;
}
}
export default Alert;
}

View File

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

View File

@@ -1,6 +1,7 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class AudioOnlyPlayability extends YTNode {
export default class AudioOnlyPlayability extends YTNode {
static type = 'AudioOnlyPlayability';
audio_only_availability: string;
@@ -9,6 +10,4 @@ class AudioOnlyPlayability extends YTNode {
super();
this.audio_only_availability = data.audioOnlyAvailability;
}
}
export default AudioOnlyPlayability;
}

View File

@@ -1,7 +1,8 @@
import { YTNode } from '../helpers.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import type { RawNode } from '../index.js';
class AutomixPreviewVideo extends YTNode {
export default class AutomixPreviewVideo extends YTNode {
static type = 'AutomixPreviewVideo';
playlist_video?: { endpoint: NavigationEndpoint };
@@ -14,6 +15,4 @@ class AutomixPreviewVideo extends YTNode {
};
}
}
}
export default AutomixPreviewVideo;
}

View File

@@ -1,18 +1,17 @@
import Thumbnail from './misc/Thumbnail.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class BackstageImage extends YTNode {
export default class BackstageImage extends YTNode {
static type = 'BackstageImage';
image: Thumbnail[];
endpoint: NavigationEndpoint;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.image = Thumbnail.fromResponse(data.image);
this.endpoint = new NavigationEndpoint(data.command);
}
}
export default BackstageImage;
}

View File

@@ -1,13 +1,12 @@
import Parser from '../index.js';
import Author from './misc/Author.js';
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import CommentActionButtons from './comments/CommentActionButtons.js';
import Menu from './menus/Menu.js';
import Author from './misc/Author.js';
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
class BackstagePost extends YTNode {
export default class BackstagePost extends YTNode {
static type = 'BackstagePost';
id: string;
@@ -18,13 +17,13 @@ class BackstagePost extends YTNode {
vote_status?: string;
vote_count?: Text;
menu?: Menu | null;
action_buttons;
vote_button;
action_buttons?: CommentActionButtons | null;
vote_button?: CommentActionButtons | null;
surface: string;
endpoint?: NavigationEndpoint;
attachment;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.id = data.postId;
@@ -36,40 +35,38 @@ class BackstagePost extends YTNode {
this.content = new Text(data.contentText);
this.published = new Text(data.publishedTimeText);
if (data.pollStatus) {
if (Reflect.has(data, 'pollStatus')) {
this.poll_status = data.pollStatus;
}
if (data.voteStatus) {
if (Reflect.has(data, 'voteStatus')) {
this.vote_status = data.voteStatus;
}
if (data.voteCount) {
if (Reflect.has(data, 'voteCount')) {
this.vote_count = new Text(data.voteCount);
}
if (data.actionMenu) {
if (Reflect.has(data, 'actionMenu')) {
this.menu = Parser.parseItem(data.actionMenu, Menu);
}
if (data.actionButtons) {
if (Reflect.has(data, 'actionButtons')) {
this.action_buttons = Parser.parseItem(data.actionButtons, CommentActionButtons);
}
if (data.voteButton) {
if (Reflect.has(data, 'voteButton')) {
this.vote_button = Parser.parseItem(data.voteButton, CommentActionButtons);
}
if (data.navigationEndpoint) {
if (Reflect.has(data, 'navigationEndpoint')) {
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
}
if (data.backstageAttachment) {
if (Reflect.has(data, 'backstageAttachment')) {
this.attachment = Parser.parseItem(data.backstageAttachment);
}
this.surface = data.surface;
}
}
export default BackstagePost;
}

View File

@@ -1,15 +1,13 @@
import Parser from '../index.js';
import Parser, { type RawNode } from '../index.js';
import { YTNode } from '../helpers.js';
class BackstagePostThread extends YTNode {
export default class BackstagePostThread extends YTNode {
static type = 'BackstagePostThread';
post;
post: YTNode;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.post = Parser.parseItem(data.post);
}
}
export default BackstagePostThread;
}

View File

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

View File

@@ -1,18 +1,17 @@
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class BrowserMediaSession extends YTNode {
export default class BrowserMediaSession extends YTNode {
static type = 'BrowserMediaSession';
album;
thumbnails;
album: Text;
thumbnails: Thumbnail[];
constructor (data: any) {
constructor (data: RawNode) {
super();
this.album = new Text(data.album);
this.thumbnails = Thumbnail.fromResponse(data.thumbnailDetails);
}
}
export default BrowserMediaSession;
}

View File

@@ -1,45 +1,35 @@
import Text from './misc/Text.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class Button extends YTNode {
export default class Button extends YTNode {
static type = 'Button';
text?: string;
label?: string;
tooltip?: string;
icon_type?: string;
is_disabled?: boolean;
endpoint: NavigationEndpoint;
constructor(data: any) {
constructor(data: RawNode) {
super();
if (data.text) {
if (Reflect.has(data, 'text'))
this.text = new Text(data.text).toString();
}
if (data.accessibility?.label) {
this.label = data.accessibility?.label;
}
if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'label'))
this.label = data.accessibility.label;
if (data.tooltip) {
if (Reflect.has(data, 'tooltip'))
this.tooltip = data.tooltip;
}
if (data.icon?.iconType) {
this.icon_type = 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);
}
}
export default Button;
}

View File

@@ -1,15 +1,14 @@
import Parser from '../index.js';
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';
import Thumbnail from './misc/Thumbnail.js';
import Button from './Button.js';
import ChannelHeaderLinks from './ChannelHeaderLinks.js';
import SubscribeButton from './SubscribeButton.js';
import { YTNode } from '../helpers.js';
class C4TabbedHeader extends YTNode {
export default class C4TabbedHeader extends YTNode {
static type = 'C4TabbedHeader';
author: Author;
@@ -20,57 +19,55 @@ 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;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.author = new Author({
simpleText: data.title,
navigationEndpoint: data.navigationEndpoint
}, data.badges, data.avatar);
if (data.banner) {
if (Reflect.has(data, 'banner')) {
this.banner = Thumbnail.fromResponse(data.banner);
}
if (data.tv_banner) {
if (Reflect.has(data, 'tv_banner')) {
this.tv_banner = Thumbnail.fromResponse(data.tvBanner);
}
if (data.mobile_banner) {
if (Reflect.has(data, 'mobile_banner')) {
this.mobile_banner = Thumbnail.fromResponse(data.mobileBanner);
}
if (data.subscriberCountText) {
if (Reflect.has(data, 'subscriberCountText')) {
this.subscribers = new Text(data.subscriberCountText);
}
if (data.videosCountText) {
if (Reflect.has(data, 'videosCountText')) {
this.videos_count = new Text(data.videosCountText);
}
if (data.sponsorButton) {
if (Reflect.has(data, 'sponsorButton')) {
this.sponsor_button = Parser.parseItem(data.sponsorButton, Button);
}
if (data.subscribeButton) {
if (Reflect.has(data, 'subscribeButton')) {
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
}
if (data.headerLinks) {
this.header_links = Parser.parseItem(data.headerLinks, ChannelHeaderLinks);
if (Reflect.has(data, 'headerLinks')) {
this.header_links = Parser.parseItem(data.headerLinks, [ ChannelHeaderLinks, ChannelHeaderLinksView ]);
}
if (data.channelHandleText) {
if (Reflect.has(data, 'channelHandleText')) {
this.channel_handle = new Text(data.channelHandleText);
}
if (data.channelId) {
if (Reflect.has(data, 'channelId')) {
this.channel_id = data.channelId;
}
}
}
export default C4TabbedHeader;
}

View File

@@ -1,19 +1,18 @@
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class CallToActionButton extends YTNode {
export default class CallToActionButton extends YTNode {
static type = 'CallToActionButton';
label: Text;
icon_type: string;
style: string;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.label = new Text(data.label);
this.icon_type = data.icon.iconType;
this.style = data.style;
}
}
export default CallToActionButton;
}

View File

@@ -1,13 +1,13 @@
import Parser from '../index.js';
import Parser, { type RawNode } from '../index.js';
import { YTNode } from '../helpers.js';
class Card extends YTNode {
export default class Card extends YTNode {
static type = 'Card';
teaser;
content;
card_id: string | null;
feature: string | null;
teaser: YTNode;
content: YTNode;
card_id?: string;
feature?: string;
cue_ranges: {
start_card_active_ms: string;
@@ -16,12 +16,18 @@ class Card extends YTNode {
icon_after_teaser_ms: string;
}[];
constructor(data: any) {
constructor(data: RawNode) {
super();
this.teaser = Parser.parseItem(data.teaser);
this.content = Parser.parseItem(data.content);
this.card_id = data.cardId || null;
this.feature = data.feature || null;
if (Reflect.has(data, 'cardId')) {
this.card_id = data.cardId;
}
if (Reflect.has(data, 'feature')) {
this.feature = data.feature;
}
this.cue_ranges = data.cueRanges.map((cr: any) => ({
start_card_active_ms: cr.startCardActiveMs,
@@ -30,6 +36,4 @@ class Card extends YTNode {
icon_after_teaser_ms: cr.iconAfterTeaserMs
}));
}
}
export default Card;
}

View File

@@ -1,20 +1,18 @@
import Parser from '../index.js';
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
class CardCollection extends YTNode {
export default class CardCollection extends YTNode {
static type = 'CardCollection';
cards;
cards: ObservedArray<YTNode>;
header: Text;
allow_teaser_dismiss: boolean;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.cards = Parser.parseArray(data.cards);
this.header = new Text(data.headerText);
this.allow_teaser_dismiss = data.allowTeaserDismiss;
}
}
export default CardCollection;
}

View File

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

View File

@@ -1,18 +1,17 @@
import Parser from '../index.js';
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import { type ObservedArray, YTNode } from '../helpers.js';
import Thumbnail from './misc/Thumbnail.js';
class CarouselItem extends YTNode {
export default class CarouselItem extends YTNode {
static type = 'CarouselItem';
items: YTNode[];
items: ObservedArray<YTNode>;
background_color: string;
layout_style: string;
pagination_thumbnails: Thumbnail[];
paginator_alignment: string;
constructor (data: any) {
constructor (data: RawNode) {
super();
this.items = Parser.parseArray(data.carouselItems);
this.background_color = data.backgroundColor;
@@ -20,6 +19,9 @@ class CarouselItem extends YTNode {
this.pagination_thumbnails = Thumbnail.fromResponse(data.paginationThumbnails);
this.paginator_alignment = data.paginatorAlignment;
}
}
export default CarouselItem;
// XXX: For consistency.
get contents() {
return this.items;
}
}

View 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);
}
}

View File

@@ -1,28 +1,25 @@
import Parser from '../index.js';
import Text from './misc/Text.js';
import Author from './misc/Author.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import SubscribeButton from './SubscribeButton.js';
import Button from './Button.js';
import { YTNode } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import Button from './Button.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import SubscribeButton from './SubscribeButton.js';
import Author from './misc/Author.js';
import Text from './misc/Text.js';
class Channel extends YTNode {
export default class Channel extends YTNode {
static type = 'Channel';
id: string;
author: Author;
subscribers: Text;
videos: Text;
subscriber_count: Text;
video_count: Text;
long_byline: Text;
short_byline: Text;
endpoint: NavigationEndpoint;
subscribe_button: SubscribeButton | Button | null;
description_snippet: Text;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.id = data.channelId;
@@ -31,15 +28,31 @@ class Channel extends YTNode {
navigationEndpoint: data.navigationEndpoint
}, data.ownerBadges, data.thumbnail);
// TODO: subscriberCountText is now the channel's handle and videoCountText is the subscriber count. Why haven't they renamed the properties?
this.subscribers = new Text(data.subscriberCountText);
this.videos = new Text(data.videoCountText);
// XXX: `subscriberCountText` is now the channel's handle and `videoCountText` is the subscriber count.
this.subscriber_count = new Text(data.subscriberCountText);
this.video_count = new Text(data.videoCountText);
this.long_byline = new Text(data.longBylineText);
this.short_byline = new Text(data.shortBylineText);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
this.description_snippet = new Text(data.descriptionSnippet);
}
}
export default Channel;
/**
* @deprecated
* This will be removed in a future release.
* Please use {@link Channel.subscriber_count} instead.
*/
get subscribers(): Text {
return this.subscriber_count;
}
/**
* @deprecated
* This will be removed in a future release.
* Please use {@link Channel.video_count} instead.
*/
get videos(): Text {
return this.video_count;
}
}

View File

@@ -1,14 +1,11 @@
import Parser from '../index.js';
import { YTNode, type ObservedArray } from '../helpers.js';
import Parser, { type RawNode } from '../index.js';
import Button from './Button.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Button from './Button.js';
import { YTNode } from '../helpers.js';
class ChannelAboutFullMetadata extends YTNode {
export default class ChannelAboutFullMetadata extends YTNode {
static type = 'ChannelAboutFullMetadata';
id: string;
@@ -22,15 +19,15 @@ class ChannelAboutFullMetadata extends YTNode {
title: Text;
}[];
views: Text;
joined: Text;
view_count: Text;
joined_date: Text;
description: Text;
email_reveal: NavigationEndpoint;
can_reveal_email: boolean;
country: Text;
buttons: Button[];
buttons: ObservedArray<Button>;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.id = data.channelId;
this.name = new Text(data.title);
@@ -43,14 +40,30 @@ class ChannelAboutFullMetadata extends YTNode {
title: new Text(link.title)
})) ?? [];
this.views = new Text(data.viewCountText);
this.joined = new Text(data.joinedDateText);
this.view_count = new Text(data.viewCountText);
this.joined_date = new Text(data.joinedDateText);
this.description = new Text(data.description);
this.email_reveal = new NavigationEndpoint(data.onBusinessEmailRevealClickCommand);
this.can_reveal_email = !data.signInForBusinessEmail;
this.country = new Text(data.country);
this.buttons = Parser.parseArray(data.actionButtons, Button);
}
}
export default ChannelAboutFullMetadata;
/**
* @deprecated
* This will be removed in a future release.
* Please use {@link Channel.view_count} instead.
*/
get views() {
return this.view_count;
}
/**
* @deprecated
* This will be removed in a future release.
* Please use {@link Channel.joined_date} instead.
*/
get joined(): Text {
return this.joined_date;
}
}

View File

@@ -1,12 +1,11 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import { Parser } from '../index.js';
import Button from './Button.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
class ChannelAgeGate extends YTNode {
export default class ChannelAgeGate extends YTNode {
static type = 'ChannelAgeGate';
channel_title: string;
@@ -25,6 +24,4 @@ class ChannelAgeGate extends YTNode {
this.sign_in_button = Parser.parseItem(data.signInButton, Button);
this.secondary_text = new Text(data.secondaryText);
}
}
export default ChannelAgeGate;
}

View File

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

View File

@@ -1,31 +1,34 @@
import { YTNode, observe, type ObservedArray } from '../helpers.js';
import type { RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import { YTNode } from '../helpers.js';
class HeaderLink {
// XXX (LuanRT): This is not a real YTNode, but we treat it as one to keep things clean.
export class HeaderLink extends YTNode {
static type = 'HeaderLink';
endpoint: NavigationEndpoint;
icon: Thumbnail[];
title: Text;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.icon = Thumbnail.fromResponse(data.icon);
this.title = new Text(data.title);
}
}
class ChannelHeaderLinks extends YTNode {
export default class ChannelHeaderLinks extends YTNode {
static type = 'ChannelHeaderLinks';
primary: HeaderLink[];
secondary: HeaderLink[];
primary: ObservedArray<HeaderLink>;
secondary: ObservedArray<HeaderLink>;
constructor(data: any) {
constructor(data: RawNode) {
super();
this.primary = data.primaryLinks?.map((link: any) => new HeaderLink(link)) || [];
this.secondary = data.secondaryLinks?.map((link: any) => new HeaderLink(link)) || [];
this.primary = observe(data.primaryLinks?.map((link: RawNode) => new HeaderLink(link)) || []);
this.secondary = observe(data.secondaryLinks?.map((link: RawNode) => new HeaderLink(link)) || []);
}
}
export default ChannelHeaderLinks;
}

View File

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

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