Compare commits

...

63 Commits

Author SHA1 Message Date
github-actions[bot]
4fca6c354e chore(main): release 10.4.0 (#721)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-26 23:41:20 -03:00
Luan
93906e0539 chore(docs): Mention BgUtils for generating PoTokens
https://github.com/LuanRT/BgUtils
2024-08-26 23:37:35 -03:00
dependabot[bot]
aaebcca90b chore(deps-dev): bump vite from 3.2.8 to 3.2.10 in /examples/browser/web (#739)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.8 to 3.2.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.10/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.10/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 21:52:34 -03:00
Luan
36121878b1 chore(examples): Fix proxy 2024-08-26 20:32:30 -03:00
Luan
d89909a19a fix(examples): Use BgUtils to generate pot [skip ci]
+ Improve readme.
2024-08-26 18:42:14 -03:00
Luan
367a6f7ec5 chore(package): Revert last commit
This reverts commit 05a663710d.
2024-08-23 14:44:00 -03:00
Luan
05a663710d chore(package): Use prepack instead of prepare 2024-08-23 14:22:54 -03:00
Luan
cc0fc7145a chore(package): Fix typo in exports 2024-08-23 13:17:50 -03:00
Luan
bdff3eae1c chore: lint [skip ci] 2024-08-23 02:07:41 -03:00
Luan
5a66d0ba93 chore: clean up some types [skip ci] 2024-08-23 02:04:28 -03:00
Luan
370cb0b29e chore(eslint): Update ignores 2024-08-23 01:12:17 -03:00
Luan
fcd00b0fb0 fix(FormatOptions): client missing some values 2024-08-23 01:05:37 -03:00
Luan
2dae5634f3 chore(docs): Minor rewording 2024-08-23 00:51:52 -03:00
Luan
2c43a5705f chore: update tests 2024-08-22 22:40:42 -03:00
Luan
83801ebc37 chore: fix linter config 2024-08-22 22:36:55 -03:00
Luan
c14a687e65 chore: bump eslint to v9 2024-08-22 15:43:59 -03:00
Dave Nicolson
f9ccba4af5 fix(ThumbnailOverlayResumePlayback): Update percent_duration_watched type (#737) 2024-08-19 20:18:06 -03:00
absidue
4b60b97132 feat(parser): Add VideoAttributesSectionView node (#732) 2024-08-12 14:49:20 -03:00
absidue
7afc3da80e fix(Session): Fix remote visitor data not gettting used (#731) 2024-08-12 00:21:02 -03:00
슈리튬
bb6e647b8c fix(Session): PoToken not being set correctly (#729) 2024-08-11 04:10:23 -03:00
Luan
f1973c11d9 fix(Session): Visitor data not being used properly 2024-08-10 11:12:13 -03:00
Luan
4942992630 refactor: Throw an error if an invalid client is specified 2024-08-08 10:11:38 -03:00
Luan
0e91a08ae2 fix(PlayerEndpoint): Don't set undefined fields 2024-08-08 09:01:14 -03:00
Luan
261f2ac12b feat(Utils): Add UMP parser
Currently not used anywhere in the project, but I figured I'd add it in case anyone wants to make their playback requests look more genuine by using UMP/SABR.
2024-08-08 07:57:14 -03:00
Luan
041aebc358 chore: Rephrase PoToken description 2024-08-08 07:42:20 -03:00
Émilien (perso)
c9f0ddd573 feat(Player): Add support for Proof of Identity tokens (#708)
* Fix different usages of potoken.

* Fix linting.

* Add mention about invidious youtube-trusted-session-generator.

---------

Co-authored-by: Luan <luan.lrt4@gmail.com>
2024-08-08 07:28:42 -03:00
Luan
25d268beba chore: update livechat example 2024-08-06 18:48:34 -03:00
absidue
2c0bb237e1 fix(Search): Fix it occasionally returning only a small number of results (#720) 2024-08-03 09:40:02 -03:00
github-actions[bot]
4f5635ad07 chore(main): release 10.3.0 (#704)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-01 06:21:32 -03:00
Luan
3b3cf1b2aa refactor(Player): Generate and parse player script's AST (#713)
Notes:
- The Syntax Tree is generated by Jinter (which is built on top of `Acorn`).
- While doing this may be slightly slower than using a regular exp, it is much more reliable (plus we already cache the player functions anyway).
2024-08-01 06:09:27 -03:00
Dave Nicolson
d85fbc56cf feat(PlaylistManager): Add ability to remove videos by set ID (#715) 2024-08-01 06:07:47 -03:00
Luan
e55d4af100 chore: lint 2024-08-01 06:03:51 -03:00
Armel Chesnais
04369be620 fix(Player): Address changes introduced by player id 20dfca59 (#712)
Fixes the nSig extract for YT player id `20dfca59 `
Handles:

String.prototype.split.call(a,("",""))

and

Array.prototype.join.call(b,
("",""))};

Note the newline

Co-authored-by: Luan <luan.lrt4@gmail.com>
2024-07-31 06:38:32 -03:00
Luan
a89a5ac2dd refactor(Player.ts): Handle nsig failure gracefully
Preping for future changes.
2024-07-31 06:18:18 -03:00
Luan
5529a6aca0 chore(Player): Don't throw an error if nsig extraction fails
This is called when an InnerTube instance is created, so throwing here breaks the entire library.
2024-07-30 19:05:29 -03:00
Luan
94a6765c97 chore: update tests 2024-07-30 18:50:03 -03:00
Luan
9b9fb82131 refactor: Clean up & fix old code
Other changes:
- Renamed "getShortsWatchItem" to "getShortsVideoInfo".
- Fixed `ShortFormVideoInfo`. This never worked for me ever since it was introduced. Turned out it was just implemented incorrectly.
- Moved `basic_info` extraction to `MediaInfo`. Less of a pain to maintain as we only have to modify one file.
- Removed unneeded tsdoc comments.
- Fixed `Innertube#getStreamingData()`. Now it actually returns a deciphered format.
- Simplified some types.
2024-07-30 18:49:24 -03:00
absidue
3153375bca fix(HTTPClient): Adjust more context fields for the iOS client (#705) 2024-07-26 11:15:12 -03:00
Dave Nicolson
a9bf225a62 feat(parser): Add EomSettingsDisclaimer node (#703) 2024-07-25 22:12:57 -03:00
github-actions[bot]
1e29019a07 chore(main): release 10.2.0 (#688)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-25 10:52:50 -03:00
Luan
6765f4e0d7 fix(Player): Bump cache version (#702)
We should always do this after updating the sig/nsig code, it's so that the old cache gets ignored : ).
2024-07-25 10:48:24 -03:00
absidue
3048f70f60 fix(Player): Fix extracting the n-token decipher algorithm again (#701) 2024-07-25 10:07:00 -03:00
Brahim Hadriche
090539b28f feat(parser): add classdata to unhandled parse errors (#691) 2024-07-24 15:55:20 -03:00
Brahim Hadriche
6d0bc89be1 fix(parser): ignore MiniGameCardView node (#692) 2024-07-24 15:54:37 -03:00
GurumNyang
a5f62093a1 feature(proto): Add comment_id to commentSectionParams (#693) 2024-07-24 15:54:14 -03:00
absidue
a352ddeb9d feat(Format): Add is_secondary for detecting secondary audio tracks (#697) 2024-07-24 15:53:27 -03:00
Brahim Hadriche
0f8f92a28a fix(parser): ThumbnailView background color (#686) 2024-07-11 14:31:27 -03:00
github-actions[bot]
7d03469e64 chore(main): release 10.1.0 (#669)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-10 03:44:43 -03:00
Luan
62ac2f6f32 fix(proto): Update Context message
Closes #681
2024-07-10 03:41:16 -03:00
absidue
142a7d0428 fix(Player): Fix extracting the n-token decipher algorithm (#682)
* fix(Player): Fix extracting the n-token decipher algorithm

* fix: bump Jinter to v2

---------

Co-authored-by: Luan <luan.lrt4@gmail.com>
2024-07-10 02:21:39 -03:00
Luan
efa7205723 Merge branch 'main' of https://github.com/LuanRT/YouTube.js 2024-07-01 08:53:45 -03:00
Luan
84f90aaf29 fix(Session): Round UTC offset minutes 2024-07-01 08:53:08 -03:00
absidue
858cdd197c feat(toDash): Add the "dub" role to translated captions (#677) 2024-06-30 23:05:08 -03:00
Luan
5a8fd3ad37 feat(Session): Add configInfo to InnerTube context
Minor addition. It's needed for certain UMP requests.
2024-06-30 22:51:02 -03:00
슈리튬
a19511de24 fix(FormatUtils): Throw an error if download requests fails 2024-06-28 16:45:39 -03:00
absidue
bd9f6ac64c feat(toDash): Add option to include WebVTT or TTML captions (#673) 2024-06-25 01:22:11 -03:00
absidue
e5aab9a9b3 fix(toDash): Fix image representations not being spec compliant (#672) 2024-06-24 15:48:38 -03:00
Luan
d6fa134c3d chore(Playlist): Add MusicResponsiveHeader to header types
Oops! I forgot this one also existed : ).
2024-06-21 21:12:54 -03:00
Luan
fe953072a2 chore: fix tests 2024-06-21 20:57:38 -03:00
Luan
055fa33403 chore: lint 2024-06-21 19:32:50 -03:00
Luan
14c3a06d40 fix(YTMusic): Add support for new header layouts
This is due to a minor page redesign by YouTube Music. See https://9to5google.com/2024/06/20/youtube-music-web-album-playlist-redesign/.
2024-06-21 19:31:40 -03:00
Luan
67376afae6 chore(Format): Clean up and add some extra fields 2024-06-16 16:22:33 -03:00
Luan
4cbaa7983f fix(InfoPanelContent): Update InfoPanelContent node to also support paragraphs
This would fail when `attributedParagraphs` was missing, so we still need `paragraphs` there.
2024-06-16 15:39:47 -03:00
112 changed files with 2335 additions and 1535 deletions

View File

@@ -1,9 +0,0 @@
.git
.github
test/
cache/
src/proto/youtube.ts
coverage/
node_modules/
dist/
src/proto/generated/

View File

@@ -1,93 +0,0 @@
plugins:
[ '@typescript-eslint', 'eslint-plugin-tsdoc' ]
env:
commonjs: true
es2021: true
node: true
extends: [ eslint:recommended, 'plugin:@typescript-eslint/recommended' ]
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: latest
project:
- tsconfig.json
overrides:
-
files:
- '**/*.js'
- '**/*.mjs'
rules:
'tsdoc/syntax': 'off'
rules:
max-len:
- error
-
code: 200
ignoreComments: true
ignoreTrailingComments: true
ignoreStrings: true
ignoreTemplateLiterals: true
ignoreRegExpLiterals: true
quotes: [error, single]
'@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
no-unused-private-class-members: 'off'
no-prototype-builtins: 'off'
no-async-promise-executor: 'off'
no-case-declarations: 'off'
no-return-assign: 'off'
no-floating-decimal: error
no-implied-eval: error
arrow-spacing: error
no-invalid-this: error
no-lone-blocks: 'off'
no-new-func: error
no-new-wrappers: error
no-new: error
no-void: error
no-octal-escape: error
no-self-compare: error
no-sequences: error
no-throw-literal: error
no-unmodified-loop-condition: error
no-useless-call: error
no-useless-concat: error
no-useless-escape: error
no-useless-return: error
no-else-return: error
no-lonely-if: error
no-undef-init: error
no-unneeded-ternary: error
no-var: error
no-multi-spaces: error
no-multiple-empty-lines: ["error", { "max": 2, "maxEOF": 0 }]
no-tabs: error
no-trailing-spaces: error
brace-style: error
new-parens: error
space-infix-ops: error
template-curly-spacing: error
wrap-regex: error
capitalized-comments: error
prefer-template: error
keyword-spacing: ["error", { "before": true } ]
object-curly-spacing: ["warn", "always"]
array-bracket-spacing: ["error", "always"]
arrow-parens: ["error", "always"]
comma-dangle: ["error", "never"]
comma-spacing: ["error", { "before": false, "after": true }]
computed-property-spacing: ["error", "never"]
func-call-spacing: ["error", "never"]
indent: ["error", 2, { "SwitchCase": 1 }]
key-spacing: ["error", { "beforeColon": false }]
semi: ["error", "always"]
operator-assignment: ["error", "always"]

3
.gitignore vendored
View File

@@ -56,6 +56,7 @@ pnpm-lock.yaml
# Downloaded assets
*.mp4
*.m4a
*.webm
*.mkv
@@ -75,3 +76,5 @@ deno/
# MacOS
.DS_Store
*.bin

View File

@@ -1,5 +1,76 @@
# Changelog
## [10.4.0](https://github.com/LuanRT/YouTube.js/compare/v10.3.0...v10.4.0) (2024-08-27)
### Features
* **parser:** Add `VideoAttributesSectionView` node ([#732](https://github.com/LuanRT/YouTube.js/issues/732)) ([4b60b97](https://github.com/LuanRT/YouTube.js/commit/4b60b97132b0ee42b41838f3336c582a7f7f7aec))
* **Player:** Add support for Proof of Identity tokens ([#708](https://github.com/LuanRT/YouTube.js/issues/708)) ([c9f0ddd](https://github.com/LuanRT/YouTube.js/commit/c9f0ddd573de297c0b384e422e6c1737454926e2))
* **Utils:** Add `UMP` parser ([261f2ac](https://github.com/LuanRT/YouTube.js/commit/261f2ac12b6a9a5bd5f7a43557018de333f7bec3))
### Bug Fixes
* **examples:** Use BgUtils to generate pot [skip ci] ([d89909a](https://github.com/LuanRT/YouTube.js/commit/d89909a19a1486bee7e3275014725b4e3dc2cbe2))
* **FormatOptions:** `client` missing some values ([fcd00b0](https://github.com/LuanRT/YouTube.js/commit/fcd00b0fb0f88a16c27da1ed89e9a2c4887e5c52))
* **PlayerEndpoint:** Don't set `undefined` fields ([0e91a08](https://github.com/LuanRT/YouTube.js/commit/0e91a08ae2194a07defc4b1e12ff3edbe13b72df))
* **Search:** Fix it occasionally returning only a small number of results ([#720](https://github.com/LuanRT/YouTube.js/issues/720)) ([2c0bb23](https://github.com/LuanRT/YouTube.js/commit/2c0bb237e1d0eb160dc3f879f5cab2022d9b5b04))
* **Session:** `PoToken` not being set correctly ([#729](https://github.com/LuanRT/YouTube.js/issues/729)) ([bb6e647](https://github.com/LuanRT/YouTube.js/commit/bb6e647b8c88753669acde43d0d648aaf11caba6))
* **Session:** Fix remote visitor data not gettting used ([#731](https://github.com/LuanRT/YouTube.js/issues/731)) ([7afc3da](https://github.com/LuanRT/YouTube.js/commit/7afc3da80ee3b5aa6edd2a899be82c1a21e03556))
* **Session:** Visitor data not being used properly ([f1973c1](https://github.com/LuanRT/YouTube.js/commit/f1973c11d9a492898b5e72b1f2b79291b674e229))
* **ThumbnailOverlayResumePlayback:** Update `percent_duration_watched` type ([#737](https://github.com/LuanRT/YouTube.js/issues/737)) ([f9ccba4](https://github.com/LuanRT/YouTube.js/commit/f9ccba4af5268d67d8610a1a5d623964f56d170d))
## [10.3.0](https://github.com/LuanRT/YouTube.js/compare/v10.2.0...v10.3.0) (2024-08-01)
### Features
* **parser:** Add `EomSettingsDisclaimer` node ([#703](https://github.com/LuanRT/YouTube.js/issues/703)) ([a9bf225](https://github.com/LuanRT/YouTube.js/commit/a9bf225a62108e47a50316235a83a814c682d745))
* **PlaylistManager:** Add ability to remove videos by set ID ([#715](https://github.com/LuanRT/YouTube.js/issues/715)) ([d85fbc5](https://github.com/LuanRT/YouTube.js/commit/d85fbc56cf0fd7367b182ae36e65c1701bc5e62d))
### Bug Fixes
* **HTTPClient:** Adjust more context fields for the iOS client ([#705](https://github.com/LuanRT/YouTube.js/issues/705)) ([3153375](https://github.com/LuanRT/YouTube.js/commit/3153375bcaa6c03afba9da8474e6a9d37471ed29))
## [10.2.0](https://github.com/LuanRT/YouTube.js/compare/v10.1.0...v10.2.0) (2024-07-25)
### Features
* **Format:** Add `is_secondary` for detecting secondary audio tracks ([#697](https://github.com/LuanRT/YouTube.js/issues/697)) ([a352dde](https://github.com/LuanRT/YouTube.js/commit/a352ddeb9db001e99f49025048ad0942d84f1b3e))
* **parser:** add classdata to unhandled parse errors ([#691](https://github.com/LuanRT/YouTube.js/issues/691)) ([090539b](https://github.com/LuanRT/YouTube.js/commit/090539b28f9bc3387d01e37325ba5741b33b1765))
* **proto:** Add `comment_id` to commentSectionParams ([#693](https://github.com/LuanRT/YouTube.js/issues/693)) ([a5f6209](https://github.com/LuanRT/YouTube.js/commit/a5f62093a18705fc822abd86beaa81788b6535ce))
### Bug Fixes
* **parser:** ignore MiniGameCardView node ([#692](https://github.com/LuanRT/YouTube.js/issues/692)) ([6d0bc89](https://github.com/LuanRT/YouTube.js/commit/6d0bc89be18f27a8ce74517f5cab5020d6790328))
* **parser:** ThumbnailView background color ([#686](https://github.com/LuanRT/YouTube.js/issues/686)) ([0f8f92a](https://github.com/LuanRT/YouTube.js/commit/0f8f92a28a5b6143e890626b22ce570730a0cf09))
* **Player:** Bump cache version ([#702](https://github.com/LuanRT/YouTube.js/issues/702)) ([6765f4e](https://github.com/LuanRT/YouTube.js/commit/6765f4e0d791c657fc7411e9cdd2c0f9284e9982))
* **Player:** Fix extracting the n-token decipher algorithm again ([#701](https://github.com/LuanRT/YouTube.js/issues/701)) ([3048f70](https://github.com/LuanRT/YouTube.js/commit/3048f70f60756884bd7b591d770f7b6343cfa259))
## [10.1.0](https://github.com/LuanRT/YouTube.js/compare/v10.0.0...v10.1.0) (2024-07-10)
### Features
* **Session:** Add `configInfo` to InnerTube context ([5a8fd3a](https://github.com/LuanRT/YouTube.js/commit/5a8fd3ad37bce1decad28ec3727453ddd430a561))
* **toDash:** Add option to include WebVTT or TTML captions ([#673](https://github.com/LuanRT/YouTube.js/issues/673)) ([bd9f6ac](https://github.com/LuanRT/YouTube.js/commit/bd9f6ac64ca9ba96e856aabe5fcc175fd9c294dc))
* **toDash:** Add the "dub" role to translated captions ([#677](https://github.com/LuanRT/YouTube.js/issues/677)) ([858cdd1](https://github.com/LuanRT/YouTube.js/commit/858cdd197cb2bb1e1d7a7285966cb56043ad8961))
### Bug Fixes
* **FormatUtils:** Throw an error if download requests fails ([a19511d](https://github.com/LuanRT/YouTube.js/commit/a19511de24bb82007aab072844efe64bbb8698da))
* **InfoPanelContent:** Update InfoPanelContent node to also support `paragraphs` ([4cbaa79](https://github.com/LuanRT/YouTube.js/commit/4cbaa7983f35a82b9907197769672ac3b300bfbe))
* **Player:** Fix extracting the n-token decipher algorithm ([#682](https://github.com/LuanRT/YouTube.js/issues/682)) ([142a7d0](https://github.com/LuanRT/YouTube.js/commit/142a7d042885188605bdc0655d3733502d1e20fa))
* **proto:** Update `Context` message ([62ac2f6](https://github.com/LuanRT/YouTube.js/commit/62ac2f6f32d35fec3c31b5f5d556bd4569fa49f9)), closes [#681](https://github.com/LuanRT/YouTube.js/issues/681)
* **Session:** Round UTC offset minutes ([84f90aa](https://github.com/LuanRT/YouTube.js/commit/84f90aaf2908ecacb9dfb6ce5497c4c4d14a72c3))
* **toDash:** Fix image representations not being spec compliant ([#672](https://github.com/LuanRT/YouTube.js/issues/672)) ([e5aab9a](https://github.com/LuanRT/YouTube.js/commit/e5aab9a9b35f0752cd5ca50bfa25936dce4718c6))
* **YTMusic:** Add support for new header layouts ([14c3a06](https://github.com/LuanRT/YouTube.js/commit/14c3a06d402989e98a9d32c79b2dc26f74fb0219))
## [10.0.0](https://github.com/LuanRT/YouTube.js/compare/v9.4.0...v10.0.0) (2024-06-09)

View File

@@ -97,17 +97,19 @@ const youtube = await Innertube.create(/* options */);
| `location` | `string` | Geolocation. | `US` |
| `account_index` | `number` | The account index to use. This is useful if you have multiple accounts logged in. **NOTE:** Only works if you are signed in with cookies. | `0` |
| `visitor_data` | `string` | Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in. A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint. | `undefined` |
| `po_token` | `string` | Proof of Origin Token. This is an attestation token generated by BotGuard/DroidGuard. It is used to confirm that the request is coming from a genuine client. Valid tokens can be generated using [BgUtils](https://github.com/LuanRT/BgUtils) or [Invidious' tool](https://github.com/iv-org/youtube-trusted-session-generator). | `undefined` |
| `retrieve_player` | `boolean` | Specifies whether to retrieve the JS player. Disabling this will make session creation faster. **NOTE:** Deciphering formats is not possible without the JS player. | `true` |
| `enable_safety_mode` | `boolean` | Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content. | `false` |
| `generate_session_locally` | `boolean` | Specifies whether to generate the session data locally or retrieve it from YouTube. This can be useful if you need more performance. **NOTE:** If you are using the cache option and a session has already been generated, this will be ignored. If you want to force a new session to be generated, you must clear the cache or disable session caching. | `false` |
| `enable_session_cache` | `boolean` | Specifies whether to cache the session data. | `true` |
| `device_category` | `DeviceCategory` | Platform to use for the session. | `DESKTOP` |
| `client_type` | `ClientType` | InnerTube client type. | `WEB` |
| `client_type` | `ClientType` | InnerTube client type. It is not recommended to change this unless you know what you are doing. | `WEB` |
| `timezone` | `string` | The time zone. | `*` |
| `cache` | `ICache` | Used to cache algorithms, session data, and OAuth2 tokens. | `undefined` |
| `cookie` | `string` | YouTube cookies. | `undefined` |
| `fetch` | `FetchFunction` | Fetch function to use. | `fetch` |
</details>
### Browser Usage
@@ -269,7 +271,7 @@ Retrieves video info.
| Param | Type | Description |
| --- | --- | --- |
| target | `string` \| `NavigationEndpoint` | If `string`, the id of the video. If `NavigationEndpoint`, the endpoint of watchable elements such as `Video`, `Mix` and `Playlist`. To clarify, valid endpoints have payloads containing at least `videoId` and optionally `playlistId`, `params` and `index`. |
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC`, `YTMUSIC_ANDROID` or `TV_EMBEDDED` |
| client? | `InnerTubeClient` | InnerTube client to use. |
<details>
<summary>Methods & Getters</summary>
@@ -336,7 +338,7 @@ Suitable for cases where you only need basic video metadata. Also, it is faster
| Param | Type | Description |
| --- | --- | --- |
| video_id | `string` | The id of the video |
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC_ANDROID`, `YTMUSIC`, `TV_EMBEDDED` |
| client? | `InnerTubeClient` | InnerTube client to use. |
<a name="search"></a>
### `search(query, filters?)`
@@ -686,7 +688,7 @@ import { Innertube } from 'youtubei.js';
const videoInfo = await yt.actions.execute('/player', {
// You can add any additional payloads here, and they'll merge with the default payload sent to InnerTube.
videoId,
client: 'YTMUSIC', // InnerTube client options: ANDROID, YTMUSIC, YTMUSIC_ANDROID, WEB, or TV_EMBEDDED.
client: 'YTMUSIC', // InnerTube client to use.
parse: true // tells YouTube.js to parse the response (not sent to InnerTube).
});
@@ -781,4 +783,4 @@ Distributed under the [MIT](https://choosealicense.com/licenses/mit/) License.
<p align="right">
(<a href="#top">back to top</a>)
</p>
</p>

111
eslint.config.js Normal file
View File

@@ -0,0 +1,111 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
ignores: [
"**/dist/",
"**/test/",
"**/cache/",
"**/bundle/",
"**/examples/",
"**/src/proto/generated/",
"**/*.{js,mjs,cjs}",
"**/*.d.ts",
"*.ts",
],
}, {
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
globals: {
...globals.node,
...globals.browser,
}
},
rules: {
"max-len": ["error", {
code: 200,
ignoreComments: true,
ignoreTrailingComments: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreRegExpLiterals: true,
}], quotes: ["error", "single"],
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/consistent-type-exports": "error",
'no-sparse-arrays': 'off',
"no-template-curly-in-string": "error",
"no-unreachable-loop": "error",
"no-unused-private-class-members": "off",
"no-prototype-builtins": "off",
"no-async-promise-executor": "off",
"no-case-declarations": "off",
"no-return-assign": "off",
"no-floating-decimal": "error",
"no-implied-eval": "error",
"arrow-spacing": "error",
"no-invalid-this": "error",
"no-lone-blocks": "off",
"no-new-func": "off",
"no-new-wrappers": "error",
"no-new": "error",
"no-void": "error",
"no-octal-escape": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-useless-call": "error",
"no-useless-concat": "error",
"no-useless-escape": "error",
"no-useless-return": "error",
"no-else-return": "error",
"no-lonely-if": "error",
"no-undef-init": "error",
"no-unneeded-ternary": "error",
"no-var": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", {
max: 1,
maxEOF: 0,
}],
"no-tabs": "error",
"brace-style": "error",
"new-parens": "error",
"space-infix-ops": "error",
"template-curly-spacing": "error",
"wrap-regex": "error",
"capitalized-comments": "error",
"prefer-template": "error",
"keyword-spacing": ["error", {
before: true,
}],
"object-curly-spacing": ["warn", "always"],
"array-bracket-spacing": ["error", "always"],
"arrow-parens": ["error", "always"],
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", {
before: false,
after: true,
}],
"computed-property-spacing": ["error", "never"],
"func-call-spacing": ["error", "never"],
indent: ["error", 2, {
SwitchCase: 1,
}],
"key-spacing": ["error", {
beforeColon: false,
}],
semi: ["error", "always"],
"operator-assignment": ["error", "always"],
},
}
];

View File

@@ -1,61 +1,19 @@
# Browser Usage Example
YouTube.js works in the browser!
## Usage
# Browser Usage
To use YouTube.js in the browser you must proxy requests through your own server. You can see our simple reference implementation in Deno in `examples/browser/proxy/deno.ts`.
We'll use our own fetch implementation to proxy requests through our server. This is a simple example, but you can use any fetch implementation you want.
## Example
**NOTE**: Build the library before running the examples.
```ts
import { Innertube } from "youtubei.js/web.bundle.min";
Web application:
const yt = await Innertube.create({
fetch: async (input, init) => {
// 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
url.searchParams.set('__host', url.host);
url.host = 'localhost:8080';
url.protocol = 'http';
const headers = init?.headers
? new Headers(init.headers)
: input instanceof Request
? input.headers
: new Headers();
// now serialize the headers
url.searchParams.set('__headers', JSON.stringify([...headers]));
// copy over the request
const request = new Request(
url,
input instanceof Request ? input : undefined,
);
headers.delete('user-agent');
// fetch the url
return fetch(request, init ? {
...init,
headers
} : {
headers
});
},
cache: new UniversalCache(),
});
```shell
cd examples/browser/web
npm install
npm run dev
```
After that, you can use the library as normal.
## Example
We've got a full example in `examples/browser/web` using vite.
Proxy:
```shell
deno run --allow-net --allow-read examples/browser/proxy/deno.ts
```

View File

@@ -10,7 +10,7 @@ function copyHeader(headerName: string, to: Headers, from: Headers) {
}
const handler = async (request: Request): Promise<Response> => {
// if options send do CORS preflight
// If options send do CORS preflight
if (request.method === 'OPTIONS') {
const response = new Response('', {
status: 200,
@@ -18,19 +18,19 @@ const handler = async (request: Request): Promise<Response> => {
'Access-Control-Allow-Origin': request.headers.get('origin') || '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-origin, x-youtube-client-version, x-youtube-client-name, x-goog-api-format-version, x-user-agent, Accept-Language, Range, Referer',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-goog-api-key, x-origin, x-youtube-client-version, x-youtube-client-name, x-goog-api-format-version, x-user-agent, Accept-Language, Range, Referer',
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Credentials': 'true',
}),
'Access-Control-Allow-Credentials': 'true'
})
});
return response;
}
const url = new URL(request.url, `http://localhost/`);
const url = new URL(request.url, 'http://localhost/');
if (!url.searchParams.has('__host')) {
return new Response(
'Request is formatted incorrectly. Please include __host in the query string.',
{ status: 400 },
{ status: 400 }
);
}
@@ -42,34 +42,36 @@ const handler = async (request: Request): Promise<Response> => {
// Copy headers from the request to the new request
const request_headers = new Headers(
JSON.parse(url.searchParams.get('__headers') || '{}'),
JSON.parse(url.searchParams.get('__headers') || '{}')
);
copyHeader('range', request_headers, request.headers);
!request_headers.has('user-agent') && copyHeader('user-agent', request_headers, request.headers);
if (!request_headers.has('user-agent'))
copyHeader('user-agent', request_headers, request.headers);
url.searchParams.delete('__headers');
// Make the request to YouTube
const fetchRes = await fetch(url, {
method: request.method,
headers: request_headers,
body: request.body,
body: request.body
});
// Construct the return headers
const headers = new Headers();
// copy content headers
// Copy content headers
copyHeader('content-length', headers, fetchRes.headers);
copyHeader('content-type', headers, fetchRes.headers);
copyHeader('content-disposition', headers, fetchRes.headers);
copyHeader('accept-ranges', headers, fetchRes.headers);
copyHeader('content-range', headers, fetchRes.headers);
// add cors headers
// Add cors headers
headers.set(
'Access-Control-Allow-Origin',
request.headers.get('origin') || '*',
request.headers.get('origin') || '*'
);
headers.set('Access-Control-Allow-Headers', '*');
headers.set('Access-Control-Allow-Methods', '*');
@@ -78,8 +80,8 @@ const handler = async (request: Request): Promise<Response> => {
// Return the proxied response
return new Response(fetchRes.body, {
status: fetchRes.status,
headers: headers,
headers: headers
});
};
await serve(handler, { port });
await serve(handler, { port });

298
examples/browser/web/package-lock.json generated Normal file
View File

@@ -0,0 +1,298 @@
{
"name": "web",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "web",
"version": "0.0.0",
"dependencies": {
"bgutils-js": "^1.1.0",
"shaka-player": "^4.3.8"
},
"devDependencies": {
"typescript": "^4.6.4",
"vite": "^3.2.10"
}
},
"node_modules/bgutils-js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bgutils-js/-/bgutils-js-1.1.0.tgz",
"integrity": "sha512-+v+MWO02VAfSKuuh9gpjxBTllFGkIiqzZT7ELwScOtm2UWk6MOm7lqkVtzctcjCrG0sgRZccfEbgaEWHozXLSQ==",
"funding": [
"https://github.com/sponsors/LuanRT"
]
},
"node_modules/eme-encryption-scheme-polyfill": {
"version": "2.1.1",
"license": "Apache-2.0"
},
"node_modules/esbuild": {
"version": "0.15.18",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.15.18",
"@esbuild/linux-loong64": "0.15.18",
"esbuild-android-64": "0.15.18",
"esbuild-android-arm64": "0.15.18",
"esbuild-darwin-64": "0.15.18",
"esbuild-darwin-arm64": "0.15.18",
"esbuild-freebsd-64": "0.15.18",
"esbuild-freebsd-arm64": "0.15.18",
"esbuild-linux-32": "0.15.18",
"esbuild-linux-64": "0.15.18",
"esbuild-linux-arm": "0.15.18",
"esbuild-linux-arm64": "0.15.18",
"esbuild-linux-mips64le": "0.15.18",
"esbuild-linux-ppc64le": "0.15.18",
"esbuild-linux-riscv64": "0.15.18",
"esbuild-linux-s390x": "0.15.18",
"esbuild-netbsd-64": "0.15.18",
"esbuild-openbsd-64": "0.15.18",
"esbuild-sunos-64": "0.15.18",
"esbuild-windows-32": "0.15.18",
"esbuild-windows-64": "0.15.18",
"esbuild-windows-arm64": "0.15.18"
}
},
"node_modules/esbuild-windows-64": {
"version": "0.15.18",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.0",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.13.1",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.0.0",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.33",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/resolve": {
"version": "1.22.8",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "2.79.1",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/shaka-player": {
"version": "4.7.7",
"license": "Apache-2.0",
"dependencies": {
"eme-encryption-scheme-polyfill": "^2.1.1"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/vite": {
"version": "3.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz",
"integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==",
"dev": true,
"dependencies": {
"esbuild": "^0.15.9",
"postcss": "^8.4.18",
"resolve": "^1.22.1",
"rollup": "^2.79.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

View File

@@ -10,9 +10,10 @@
},
"devDependencies": {
"typescript": "^4.6.4",
"vite": "^3.0.0"
"vite": "^3.2.10"
},
"dependencies": {
"bgutils-js": "^1.1.0",
"shaka-player": "^4.3.8"
}
}
}

View File

@@ -1,8 +1,8 @@
import { Innertube, UniversalCache } from '../../../../bundle/browser';
import { Innertube, Proto, UniversalCache, Utils } from '../../../../bundle/browser';
import BG from 'bgutils-js';
// @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;
@@ -11,51 +11,17 @@ const metadata = document.getElementById('metadata') as HTMLDivElement;
const loader = document.getElementById('loader') as HTMLDivElement;
const form = document.querySelector('form') as HTMLFormElement;
async function main() {
const visitorData = Proto.encodeVisitorData(Utils.generateRandomString(11), Math.floor(Date.now() / 1000));
const poToken = await getPo(visitorData);
const yt = await Innertube.create({
po_token: poToken,
visitor_data: visitorData,
generate_session_locally: true,
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === 'string'
? new URL(input)
: input instanceof URL
? input
: new URL(input.url);
// Transform the url for use with our proxy.
url.searchParams.set('__host', url.host);
url.host = 'localhost:8080';
url.protocol = 'http';
const headers = init?.headers
? new Headers(init.headers)
: input instanceof Request
? input.headers
: new Headers();
// Now serialize the headers.
url.searchParams.set('__headers', JSON.stringify([...headers]));
if (input instanceof Request) {
// @ts-ignore
input.duplex = 'half';
}
// Copy over the request.
const request = new Request(
url,
input instanceof Request ? input : undefined,
);
headers.delete('user-agent');
return fetch(request, init ? {
...init,
headers
} : {
headers
});
},
cache: new UniversalCache(false),
fetch: fetchFn,
cache: new UniversalCache(true),
});
form.animate({ opacity: [0, 1] }, { duration: 300, easing: 'ease-in-out' });
@@ -258,6 +224,80 @@ async function main() {
});
}
async function getPo(identity: string): Promise<string | undefined> {
const requestKey = 'O43z0dpjhgX20SCx4KAo';
const bgConfig = {
fetch: fetchFn,
globalObj: window,
requestKey,
identity
};
const challenge = await BG.Challenge.create(bgConfig);
if (!challenge)
throw new Error('Could not get challenge');
if (challenge.script) {
const script = challenge.script.find((sc) => sc !== null);
if (script)
new Function(script)();
} else {
console.warn('Unable to load VM.');
}
const poToken = await BG.PoToken.generate({
program: challenge.challenge,
globalName: challenge.globalName,
bgConfig
});
return poToken;
}
function fetchFn(input: RequestInfo | URL, init?: RequestInit) {
const url = typeof input === 'string'
? new URL(input)
: input instanceof URL
? input
: new URL(input.url);
// Transform the url for use with our proxy.
url.searchParams.set('__host', url.host);
url.host = 'localhost:8080';
url.protocol = 'http';
const headers = init?.headers
? new Headers(init.headers)
: input instanceof Request
? input.headers
: new Headers();
// Now serialize the headers.
url.searchParams.set('__headers', JSON.stringify([...headers]));
if (input instanceof Request) {
// @ts-expect-error - x
input.duplex = 'half';
}
// Copy over the request.
const request = new Request(
url,
input instanceof Request ? input : undefined
);
headers.delete('user-agent');
return fetch(request, init ? {
...init,
headers
} : {
headers
});
}
function showUI(args: { hidePlayer?: boolean } = {
hidePlayer: true,
}) {

View File

@@ -14,13 +14,14 @@ import { existsSync, mkdirSync, createWriteStream } from 'fs';
if (!album.contents)
throw new Error('Album appears to be empty');
console.info(`Album "${album.header?.title.toString()}" by ${album.header?.author?.name}`, '\n');
console.info(`Album "${album.header?.title.toString()}"`, '\n');
for (const song of album.contents) {
const stream = await yt.download(song.id as string, {
type: 'audio', // audio, video or video+audio
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on.
format: 'mp4' // media container format
format: 'mp4', // media container format,
client: 'YTMUSIC'
});
console.info(`Downloading ${song.title} (${song.id})`);
@@ -40,5 +41,5 @@ import { existsSync, mkdirSync, createWriteStream } from 'fs';
console.info(`${song.id} - Done!`, '\n');
}
console.info(`Downloaded ${album.header?.song_count}!`);
console.info(`Done!`);
})();

View File

@@ -1,15 +1,14 @@
import { Innertube, UniversalCache, YTNodes, LiveChatContinuation } from 'youtubei.js';
import { ChatAction, LiveMetadata } from 'youtubei.js/dist/src/parser/youtube/LiveChat';
import { Innertube, UniversalCache, YTNodes } from 'youtubei.js';
(async () => {
const yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true });
const yt = await Innertube.create({ cache: new UniversalCache(false) });
const search = await yt.search('lofi hip hop radio - beats to relax/study to');
const info = await yt.getInfo(search.videos[0].as(YTNodes.Video).id);
const livechat = info.getLiveChat();
livechat.on('start', (initial_data: LiveChatContinuation) => {
livechat.on('start', (initial_data) => {
/**
* Initial info is what you see when you first open a a live chat — this is; initial actions (pinned messages, top donations..), account's info and so forth.
*/
@@ -32,7 +31,7 @@ import { ChatAction, LiveMetadata } from 'youtubei.js/dist/src/parser/youtube/Li
livechat.on('end', () => console.info('This live stream has ended.'));
livechat.on('chat-update', (action: ChatAction) => {
livechat.on('chat-update', (action) => {
/**
* An action represents what is being added to
* the live chat. All actions have a `type` property,
@@ -94,7 +93,7 @@ import { ChatAction, LiveMetadata } from 'youtubei.js/dist/src/parser/youtube/Li
}
});
livechat.on('metadata-update', (metadata: LiveMetadata) => {
livechat.on('metadata-update', (metadata) => {
console.info(`
VIEWS: ${metadata.views?.view_count.toString()}
LIKES: ${metadata.likes?.default_text}

678
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "10.0.0",
"version": "10.4.0",
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
"type": "module",
"types": "./dist/src/platform/lib.d.ts",
@@ -74,11 +74,6 @@
"akkadaska (https://github.com/akkadaska)",
"Absidue (https://github.com/absidue)"
],
"directories": {
"test": "./test",
"examples": "./examples",
"dist": "./dist"
},
"scripts": {
"test": "npx jest --verbose",
"lint": "npx eslint ./src",
@@ -103,7 +98,7 @@
},
"license": "MIT",
"dependencies": {
"jintr": "^1.1.0",
"jintr": "^2.1.1",
"tslib": "^2.5.0",
"undici": "^5.19.1"
},
@@ -111,23 +106,23 @@
"typescript": "^5.0.0"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/glob": "^8.1.0",
"@types/jest": "^28.1.7",
"@types/node": "^17.0.45",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"cpy-cli": "^4.2.0",
"esbuild": "^0.14.49",
"eslint": "^8.19.0",
"eslint-plugin-tsdoc": "^0.2.16",
"eslint": "^9.9.0",
"glob": "^8.0.3",
"globals": "^15.9.0",
"jest": "^29.7.0",
"pbkit": "^0.0.59",
"replace": "^1.2.2",
"ts-jest": "^29.1.4",
"ts-patch": "^3.0.2",
"ts-transformer-inline-file": "^0.2.0",
"typescript": "^5.0.0"
"typescript": "^5.0.0",
"typescript-eslint": "^8.2.0"
},
"bugs": {
"url": "https://github.com/LuanRT/YouTube.js/issues"

View File

@@ -29,7 +29,7 @@ import {
VideoInfo
} from './parser/youtube/index.js';
import { VideoInfo as ShortsVideoInfo } from './parser/ytshorts/index.js';
import { ShortFormVideoInfo } from './parser/ytshorts/index.js';
import NavigationEndpoint from './parser/classes/NavigationEndpoint.js';
@@ -37,26 +37,12 @@ import * as Proto from './proto/index.js';
import * as Constants from './utils/Constants.js';
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.js';
import type { ApiResponse } from './core/Actions.js';
import type { INextRequest } from './types/index.js';
import type { InnerTubeConfig, InnerTubeClient, SearchFilters, INextRequest } from './types/index.js';
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js';
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.js';
import type { SessionOptions } from './core/Session.js';
import type Format from './parser/classes/misc/Format.js';
export type InnertubeConfig = SessionOptions;
export type InnerTubeClient = 'WEB' | 'iOS' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS';
export type SearchFilters = Partial<{
upload_date: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';
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,17 +53,12 @@ export default class Innertube {
this.#session = session;
}
static async create(config: InnertubeConfig = {}): Promise<Innertube> {
static async create(config: InnerTubeConfig = {}): Promise<Innertube> {
return new Innertube(await Session.create(config));
}
/**
* Retrieves video info.
* @param target - The video id or `NavigationEndpoint`.
* @param client - The client to use.
*/
async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise<VideoInfo> {
throwIfMissing({ target });
throwIfMissing({ target: target });
let next_payload: INextRequest;
@@ -93,7 +74,7 @@ export default class Innertube {
video_id: target
});
} else {
throw new InnertubeError('Invalid target, expected either a video id or a valid NavigationEndpoint', target);
throw new InnertubeError('Invalid target. Expected a video id or NavigationEndpoint.', target);
}
if (!next_payload.videoId)
@@ -103,7 +84,8 @@ export default class Innertube {
video_id: next_payload.videoId,
playlist_id: next_payload?.playlistId,
client: client,
sts: this.#session.player?.sts
sts: this.#session.player?.sts,
po_token: this.#session.po_token
});
const player_response = this.actions.execute(PlayerEndpoint.PATH, player_payload);
@@ -115,11 +97,6 @@ export default class Innertube {
return new VideoInfo(response, this.actions, cpn);
}
/**
* Retrieves basic video info.
* @param video_id - The video id.
* @param client - The client to use.
*/
async getBasicInfo(video_id: string, client?: InnerTubeClient): Promise<VideoInfo> {
throwIfMissing({ video_id });
@@ -127,7 +104,8 @@ export default class Innertube {
PlayerEndpoint.PATH, PlayerEndpoint.build({
video_id: video_id,
client: client,
sts: this.#session.player?.sts
sts: this.#session.player?.sts,
po_token: this.#session.po_token
})
);
@@ -136,37 +114,26 @@ export default class Innertube {
return new VideoInfo([ response ], this.actions, cpn);
}
/**
* Retrieves shorts info.
* @param short_id - The short id.
* @param client - The client to use.
*/
async getShortsWatchItem(short_id: string, client?: InnerTubeClient): Promise<ShortsVideoInfo> {
throwIfMissing({ short_id });
async getShortsVideoInfo(video_id: string, client?: InnerTubeClient): Promise<ShortFormVideoInfo> {
throwIfMissing({ video_id });
const watchResponse = this.actions.execute(
Reel.WatchEndpoint.PATH, Reel.WatchEndpoint.build({
short_id: short_id,
client: client
const watch_response = this.actions.execute(
Reel.ReelItemWatchEndpoint.PATH, Reel.ReelItemWatchEndpoint.build({ video_id, client })
);
const sequence_response = this.actions.execute(
Reel.ReelWatchSequenceEndpoint.PATH, Reel.ReelWatchSequenceEndpoint.build({
sequence_params: Proto.encodeReelSequence(video_id)
})
);
const sequenceResponse = this.actions.execute(
Reel.WatchSequenceEndpoint.PATH, Reel.WatchSequenceEndpoint.build({
sequenceParams: Proto.encodeReelSequence(short_id)
})
);
const response = await Promise.all([ watch_response, sequence_response ]);
const response = await Promise.all([ watchResponse, sequenceResponse ]);
const cpn = generateRandomString(16);
return new ShortsVideoInfo(response, this.actions);
return new ShortFormVideoInfo([ response[0] ], this.actions, cpn, response[1]);
}
/**
* Searches a given query.
* @param query - The search query.
* @param filters - Search filters.
*/
async search(query: string, filters: SearchFilters = {}): Promise<Search> {
throwIfMissing({ query });
@@ -179,10 +146,6 @@ export default class Innertube {
return new Search(this.actions, response);
}
/**
* Retrieves search suggestions for a given query.
* @param query - The search query.
*/
async getSearchSuggestions(query: string): Promise<string[]> {
throwIfMissing({ query });
@@ -204,11 +167,6 @@ export default class Innertube {
return suggestions;
}
/**
* Retrieves comments for a video.
* @param video_id - The video id.
* @param sort_by - Sorting options.
*/
async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise<Comments> {
throwIfMissing({ video_id });
@@ -223,9 +181,6 @@ export default class Innertube {
return new Comments(this.actions, response.data);
}
/**
* Retrieves YouTube's home feed (aka recommendations).
*/
async getHomeFeed(): Promise<HomeFeed> {
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEwhat_to_watch' })
@@ -241,9 +196,6 @@ export default class Innertube {
return new Guide(response.data);
}
/**
* Returns the account's library.
*/
async getLibrary(): Promise<Library> {
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FElibrary' })
@@ -251,10 +203,6 @@ export default class Innertube {
return new Library(this.actions, response);
}
/**
* Retrieves watch history.
* Which can also be achieved with {@link getLibrary}.
*/
async getHistory(): Promise<History> {
const response = await this.actions.execute(
BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEhistory' })
@@ -262,9 +210,6 @@ export default class Innertube {
return new History(this.actions, response);
}
/**
* Retrieves Trending content.
*/
async getTrending(): Promise<TabbedFeed<IBrowseResponse>> {
const response = await this.actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEtrending' }), parse: true }
@@ -272,9 +217,6 @@ export default class Innertube {
return new TabbedFeed(this.actions, response);
}
/**
* Retrieves Subscriptions feed.
*/
async getSubscriptionsFeed(): Promise<Feed<IBrowseResponse>> {
const response = await this.actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEsubscriptions' }), parse: true }
@@ -282,9 +224,6 @@ export default class Innertube {
return new Feed(this.actions, response);
}
/**
* Retrieves Channels feed.
*/
async getChannelsFeed(): Promise<Feed<IBrowseResponse>> {
const response = await this.actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEchannels' }), parse: true }
@@ -292,10 +231,6 @@ export default class Innertube {
return new Feed(this.actions, response);
}
/**
* Retrieves contents for a given channel.
* @param id - Channel id
*/
async getChannel(id: string): Promise<Channel> {
throwIfMissing({ id });
const response = await this.actions.execute(
@@ -304,9 +239,6 @@ export default class Innertube {
return new Channel(this.actions, response);
}
/**
* Retrieves notifications.
*/
async getNotifications(): Promise<NotificationsMenu> {
const response = await this.actions.execute(
GetNotificationMenuEndpoint.PATH, GetNotificationMenuEndpoint.build({
@@ -316,17 +248,14 @@ export default class Innertube {
return new NotificationsMenu(this.actions, response);
}
/**
* Retrieves unseen notifications count.
*/
async getUnseenNotificationsCount(): Promise<number> {
const response = await this.actions.execute(Notification.GetUnseenCountEndpoint.PATH);
// TODO: properly parse this
// FIXME: properly parse this.
return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0;
}
/**
* Retrieves playlists.
* Retrieves the user's playlists.
*/
async getPlaylists(): Promise<Feed<IBrowseResponse>> {
const response = await this.actions.execute(
@@ -335,10 +264,6 @@ export default class Innertube {
return new Feed(this.actions, response);
}
/**
* Retrieves playlist contents.
* @param id - Playlist id
*/
async getPlaylist(id: string): Promise<Playlist> {
throwIfMissing({ id });
@@ -353,10 +278,6 @@ export default class Innertube {
return new Playlist(this.actions, response);
}
/**
* Retrieves a given hashtag's page.
* @param hashtag - The hashtag to fetch.
*/
async getHashtag(hashtag: string): Promise<HashtagFeed> {
throwIfMissing({ hashtag });
@@ -380,11 +301,15 @@ export default class Innertube {
*/
async getStreamingData(video_id: string, options: FormatOptions = {}): Promise<Format> {
const info = await this.getBasicInfo(video_id);
return info.chooseFormat(options);
const format = info.chooseFormat(options);
format.url = format.decipher(this.#session.player);
return format;
}
/**
* Downloads a given video. If you only need the direct download link see {@link getStreamingData}.
* Downloads a given video. If all you need the direct download link, see {@link getStreamingData}.
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
* @param video_id - The video id.
* @param options - Download options.
@@ -402,6 +327,10 @@ export default class Innertube {
const response = await this.actions.execute(
ResolveURLEndpoint.PATH, { ...ResolveURLEndpoint.build({ url }), parse: true }
);
if (!response.endpoint)
throw new InnertubeError('Failed to resolve URL. Expected a NavigationEndpoint but got undefined', response);
return response.endpoint;
}

View File

@@ -1,5 +1,5 @@
import { Log, LZW, Constants } from '../utils/index.js';
import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.js';
import { Platform, getRandomUserAgent, getStringBetweenStrings, findFunction, PlayerError } from '../utils/Utils.js';
import type { ICache, FetchFunction } from '../types/index.js';
const TAG = 'Player';
@@ -8,19 +8,20 @@ const TAG = 'Player';
* Represents YouTube's player script. This is required to decipher signatures.
*/
export default class Player {
nsig_sc;
sig_sc;
sts;
player_id;
player_id: string;
sts: number;
nsig_sc?: string;
sig_sc?: string;
po_token?: string;
constructor(signature_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) {
constructor(player_id: string, signature_timestamp: number, sig_sc?: string, nsig_sc?: string) {
this.player_id = player_id;
this.sts = signature_timestamp;
this.nsig_sc = nsig_sc;
this.sig_sc = sig_sc;
this.sts = signature_timestamp;
this.player_id = player_id;
}
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise<Player> {
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch, po_token?: string): Promise<Player> {
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
const res = await fetch(url);
@@ -41,6 +42,7 @@ export default class Player {
const cached_player = await Player.fromCache(cache, player_id);
if (cached_player) {
Log.info(TAG, 'Found up-to-date player data in cache.');
cached_player.po_token = po_token;
return cached_player;
}
}
@@ -67,7 +69,10 @@ export default class Player {
Log.info(TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`);
return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id);
const player = await Player.fromSource(player_id, sig_timestamp, cache, sig_sc, nsig_sc);
player.po_token = po_token;
return player;
}
decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map<string, string>): string {
@@ -79,7 +84,7 @@ export default class Player {
const args = new URLSearchParams(url);
const url_components = new URL(args.get('url') || url);
if (signature_cipher || cipher) {
if (this.sig_sc && (signature_cipher || cipher)) {
const signature = Platform.shim.eval(this.sig_sc, {
sig: args.get('s')
});
@@ -91,14 +96,16 @@ export default class Player {
const sp = args.get('sp');
sp ?
url_components.searchParams.set(sp, signature) :
if (sp) {
url_components.searchParams.set(sp, signature);
} else {
url_components.searchParams.set('signature', signature);
}
}
const n = url_components.searchParams.get('n');
if (n) {
if (this.nsig_sc && n) {
let nsig;
if (this_response_nsig_cache && this_response_nsig_cache.has(n)) {
@@ -123,6 +130,10 @@ export default class Player {
url_components.searchParams.set('n', nsig);
}
// @NOTE: SABR requests should include the PoToken (not base64d, but as bytes!) in the payload.
if (url_components.searchParams.get('sabr') !== '1' && this.po_token)
url_components.searchParams.set('pot', this.po_token);
const client = url_components.searchParams.get('c');
switch (client) {
@@ -174,17 +185,18 @@ export default class Player {
const sig_sc = LZW.decompress(new TextDecoder().decode(sig_buf));
const nsig_sc = LZW.decompress(new TextDecoder().decode(nsig_buf));
return new Player(sig_timestamp, sig_sc, nsig_sc, player_id);
return new Player(player_id, sig_timestamp, sig_sc, nsig_sc);
}
static async fromSource(cache: ICache | undefined, sig_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string): Promise<Player> {
const player = new Player(sig_timestamp, sig_sc, nsig_sc, player_id);
static async fromSource(player_id: string, sig_timestamp: number, cache?: ICache, sig_sc?: string, nsig_sc?: string): Promise<Player> {
const player = new Player(player_id, sig_timestamp, sig_sc, nsig_sc);
await player.cache(cache);
return player;
}
async cache(cache?: ICache): Promise<void> {
if (!cache) return;
if (!cache || !this.sig_sc || !this.nsig_sc)
return;
const encoder = new TextEncoder();
@@ -219,13 +231,11 @@ export default class Player {
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`;
}
static extractNSigSourceCode(data: string): string {
const sc = `function descramble_nsig(a) { let b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join(""); } descramble_nsig(nsig)`;
if (!sc)
Log.warn(TAG, 'Failed to extract n-token decipher algorithm');
return sc;
static extractNSigSourceCode(data: string): string | undefined {
const nsig_function = findFunction(data, { includes: 'enhanced_except' });
if (nsig_function) {
return `${nsig_function.result} ${nsig_function.name}(nsig);`;
}
}
get url(): string {
@@ -233,6 +243,6 @@ export default class Player {
}
static get LIBRARY_VERSION(): number {
return 10;
return 11;
}
}
}

View File

@@ -59,6 +59,9 @@ export type Context = {
isWebNativeShareAvailable: boolean;
};
memoryTotalKbytes?: string;
configInfo?: {
appInstallData: string;
},
kidsAppInfo?: {
categorySettings: {
enabledCategories: string[];
@@ -97,6 +100,7 @@ type ContextData = {
enable_safety_mode: boolean;
browser_name?: string;
browser_version?: string;
app_install_data?: string;
device_make: string;
device_model: string;
on_behalf_of_user?: string;
@@ -172,6 +176,10 @@ export type SessionOptions = {
* Fetch function to use.
*/
fetch?: FetchFunction;
/**
* Proof of Origin Token. This is an attestation token generated by BotGuard/DroidGuard. It is used to confirm that the request is coming from a genuine client.
*/
po_token?: string;
}
export type SessionData = {
@@ -213,8 +221,9 @@ export default class Session extends EventEmitter {
key: string;
api_version: string;
account_index: number;
po_token?: string;
constructor(context: Context, api_key: string, api_version: string, account_index: number, player?: Player, cookie?: string, fetch?: FetchFunction, cache?: ICache) {
constructor(context: Context, api_key: string, api_version: string, account_index: number, player?: Player, cookie?: string, fetch?: FetchFunction, cache?: ICache, po_token?: string) {
super();
this.http = new HTTPClient(this, cookie, fetch);
this.actions = new Actions(this);
@@ -226,6 +235,7 @@ export default class Session extends EventEmitter {
this.api_version = api_version;
this.context = context;
this.player = player;
this.po_token = po_token;
}
on(type: 'auth', listener: OAuth2AuthEventHandler): void;
@@ -259,13 +269,14 @@ export default class Session extends EventEmitter {
options.fetch,
options.on_behalf_of_user,
options.cache,
options.enable_session_cache
options.enable_session_cache,
options.po_token
);
return new Session(
context, api_key, api_version, account_index,
options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch),
options.cookie, options.fetch, options.cache
options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch, options.po_token),
options.cookie, options.fetch, options.cache, options.po_token
);
}
@@ -323,9 +334,10 @@ export default class Session extends EventEmitter {
fetch: FetchFunction = Platform.shim.fetch,
on_behalf_of_user?: string,
cache?: ICache,
enable_session_cache = true
enable_session_cache = true,
po_token?: string
) {
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user };
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user, po_token };
let session_data: SessionData | undefined;
@@ -435,11 +447,14 @@ export default class Session extends EventEmitter {
const [ [ device_info ], api_key ] = ytcfg;
const config_info = device_info[61];
const app_install_data = config_info[config_info.length - 1];
const context_info = {
hl: options.lang || device_info[0],
gl: options.location || device_info[2],
remote_host: device_info[3],
visitor_data: device_info[13],
visitor_data: options.visitor_data || device_info[13],
client_name: options.client_name,
client_version: device_info[16],
os_name: device_info[17],
@@ -450,6 +465,7 @@ export default class Session extends EventEmitter {
browser_version: device_info[87],
device_make: device_info[11],
device_model: device_info[12],
app_install_data: app_install_data,
enable_safety_mode: options.enable_safety_mode
};
@@ -480,7 +496,7 @@ export default class Session extends EventEmitter {
deviceModel: args.device_model,
browserName: args.browser_name,
browserVersion: args.browser_version,
utcOffsetMinutes: -new Date().getTimezoneOffset(),
utcOffsetMinutes: -Math.floor((new Date()).getTimezoneOffset()),
memoryTotalKbytes: '8000000',
mainAppWebInfo: {
graftUrl: Constants.URLS.YT_BASE,
@@ -499,6 +515,9 @@ export default class Session extends EventEmitter {
}
};
if (args.app_install_data)
context.client.configInfo = { appInstallData: args.app_install_data };
if (args.on_behalf_of_user)
context.user.onBehalfOfUser = args.on_behalf_of_user;
@@ -554,4 +573,4 @@ export default class Session extends EventEmitter {
get lang(): string {
return this.context.client.hl;
}
}
}

View File

@@ -3,7 +3,7 @@ import { Constants } from '../../utils/index.js';
import { InnertubeError, MissingParamError, Platform } from '../../utils/Utils.js';
import { CreateVideoEndpoint } from '../endpoints/upload/index.js';
import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Clients.js';
import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Misc.js';
import type { ApiResponse, Session } from '../index.js';
interface UploadResult {

View File

@@ -8,7 +8,7 @@ export const PATH = '/player';
* @returns The payload.
*/
export function build(opts: PlayerEndpointOptions): IPlayerRequest {
return {
const payload: IPlayerRequest = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
@@ -33,11 +33,22 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
},
racyCheckOk: true,
contentCheckOk: true,
videoId: opts.video_id,
...{
client: opts.client,
playlistId: opts.playlist_id,
params: opts.params
}
videoId: opts.video_id
};
if (opts.client)
payload.client = opts.client;
if (opts.playlist_id)
payload.playlistId = opts.playlist_id;
if (opts.params)
payload.params = opts.params;
if (opts.po_token)
payload.serviceIntegrityDimensions = {
poToken: opts.po_token
};
return payload;
}

View File

@@ -1,6 +1,5 @@
import type { IMusicGetSearchSuggestionsRequest, MusicGetSearchSuggestionsEndpointOptions } from '../../../types/index.js';
export const PATH = '/music/get_search_suggestions';
/**

View File

@@ -0,0 +1,20 @@
import type { IReelItemWatchRequest, ReelItemWatchEndpointOptions } from '../../../types/index.js';
export const PATH = '/reel/reel_item_watch';
/**
* Builds a `/reel/reel_watch_sequence` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: ReelItemWatchEndpointOptions): IReelItemWatchRequest {
return {
disablePlayerResponse: false,
playerRequest: {
videoId: opts.video_id,
params: opts.params ?? 'CAUwAg%3D%3D'
},
params: opts.params ?? 'CAUwAg%3D%3D',
client: opts.client
};
}

View File

@@ -1,4 +1,4 @@
import type { IReelSequenceRequest, ReelWatchSequenceEndpointOptions } from '../../../types/index.js';
import type { IReelWatchSequenceRequest, ReelWatchSequenceEndpointOptions } from '../../../types/index.js';
export const PATH = '/reel/reel_watch_sequence';
@@ -7,8 +7,8 @@ export const PATH = '/reel/reel_watch_sequence';
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: ReelWatchSequenceEndpointOptions): IReelSequenceRequest {
export function build(opts: ReelWatchSequenceEndpointOptions): IReelWatchSequenceRequest {
return {
sequenceParams: opts.sequenceParams
sequenceParams: opts.sequence_params
};
}

View File

@@ -1,18 +0,0 @@
import type { IReelWatchRequest, ReelWatchEndpointOptions } from '../../../types/index.js';
export const PATH = '/reel/reel_item_watch';
/**
* Builds a `/reel/reel_watch_sequence` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: ReelWatchEndpointOptions): IReelWatchRequest {
return {
playerRequest: {
videoId: opts.short_id,
params: opts.params ?? 'CAUwAg%3D%3D'
},
params: opts.params ?? 'CAUwAg%3D%3D'
};
}

View File

@@ -1,2 +1,2 @@
export * as WatchEndpoint from './WatchEndpoint.js';
export * as WatchSequenceEndpoint from './WatchSequenceEndpoint.js';
export * as ReelItemWatchEndpoint from './ReelItemWatchEndpoint.js';
export * as ReelWatchSequenceEndpoint from './ReelWatchSequenceEndpoint.js';

View File

@@ -96,8 +96,9 @@ export default class PlaylistManager {
* Removes videos from a given playlist.
* @param playlist_id - The playlist ID.
* @param video_ids - An array of video IDs to remove from the playlist.
* @param use_set_video_ids - Option to remove videos using set video IDs.
*/
async removeVideos(playlist_id: string, video_ids: string[]): Promise<{ playlist_id: string; action_result: any }> {
async removeVideos(playlist_id: string, video_ids: string[], use_set_video_ids = false): Promise<{ playlist_id: string; action_result: any }> {
throwIfMissing({ playlist_id, video_ids });
if (!this.#actions.session.logged_in)
@@ -115,7 +116,8 @@ export default class PlaylistManager {
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()));
const key_id = use_set_video_ids ? 'set_video_id' : 'id';
const videos = pl.videos.filter((video) => video_ids.includes(video.key(key_id).string()));
videos.forEach((video) =>
payload.actions.push({

View File

@@ -5,27 +5,43 @@ import { getStreamingInfo } from '../../utils/StreamingInfo.js';
import { Parser } from '../../parser/index.js';
import { TranscriptInfo } from '../../parser/youtube/index.js';
import ContinuationItem from '../../parser/classes/ContinuationItem.js';
import PlayerMicroformat from '../../parser/classes/PlayerMicroformat.js';
import MicroformatData from '../../parser/classes/MicroformatData.js';
import type { ApiResponse, Actions } from '../index.js';
import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.js';
import type { INextResponse, IPlayabilityStatus, IPlaybackTracking, IPlayerConfig, IPlayerResponse, IStreamingData } from '../../parser/index.js';
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/FormatUtils.js';
import type Format from '../../parser/classes/misc/Format.js';
import type { DashOptions } from '../../types/DashOptions.js';
import type { ObservedArray } from '../../parser/helpers.js';
import type CardCollection from '../../parser/classes/CardCollection.js';
import type Endscreen from '../../parser/classes/Endscreen.js';
import type PlayerAnnotationsExpanded from '../../parser/classes/PlayerAnnotationsExpanded.js';
import type PlayerCaptionsTracklist from '../../parser/classes/PlayerCaptionsTracklist.js';
import type PlayerLiveStoryboardSpec from '../../parser/classes/PlayerLiveStoryboardSpec.js';
import type PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.js';
export default class MediaInfo {
#page: [IPlayerResponse, INextResponse?];
#actions: Actions;
#cpn: string;
#playback_tracking;
streaming_data;
playability_status;
player_config: IPlayerConfig;
#playback_tracking?: IPlaybackTracking;
basic_info;
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
endscreen?: Endscreen;
captions?: PlayerCaptionsTracklist;
cards?: CardCollection;
streaming_data?: IStreamingData;
playability_status?: IPlayabilityStatus;
player_config?: IPlayerConfig;
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
this.#actions = actions;
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
const info = Parser.parseResponse<IPlayerResponse>(data[0].data.playerResponse ? data[0].data.playerResponse : data[0].data);
const next = data[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
this.#page = [ info, next ];
this.#cpn = cpn;
@@ -33,6 +49,38 @@ export default class MediaInfo {
if (info.playability_status?.status === 'ERROR')
throw new InnertubeError('This video is unavailable', info.playability_status);
if (info.microformat && !info.microformat?.is(PlayerMicroformat, MicroformatData))
throw new InnertubeError('Unsupported microformat', info.microformat);
this.basic_info = { // This type is inferred so no need for an explicit type
...info.video_details,
/**
* Microformat is a bit redundant, so only
* a few things there are interesting to us.
*/
...{
embed: info.microformat?.is(PlayerMicroformat) ? info.microformat?.embed : null,
channel: info.microformat?.is(PlayerMicroformat) ? info.microformat?.channel : null,
is_unlisted: info.microformat?.is_unlisted,
is_family_safe: info.microformat?.is_family_safe,
category: info.microformat?.is(PlayerMicroformat) ? info.microformat?.category : null,
has_ypc_metadata: info.microformat?.is(PlayerMicroformat) ? info.microformat?.has_ypc_metadata : null,
start_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.start_timestamp : null,
end_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.end_timestamp : null,
view_count: info.microformat?.is(PlayerMicroformat) && isNaN(info.video_details?.view_count as number) ? info.microformat.view_count : info.video_details?.view_count,
url_canonical: info.microformat?.is(MicroformatData) ? info.microformat?.url_canonical : null,
tags: info.microformat?.is(MicroformatData) ? info.microformat?.tags : null
},
like_count: undefined as number | undefined,
is_liked: undefined as boolean | undefined,
is_disliked: undefined as boolean | undefined
};
this.annotations = info.annotations;
this.storyboards = info.storyboards;
this.endscreen = info.endscreen;
this.captions = info.captions;
this.cards = info.cards;
this.streaming_data = info.streaming_data;
this.playability_status = info.playability_status;
this.player_config = info.player_config;
@@ -54,11 +102,16 @@ export default class MediaInfo {
}
let storyboards;
let captions;
if (options.include_thumbnails && player_response.storyboards) {
storyboards = player_response.storyboards;
}
if (typeof options.captions_format === 'string' && player_response.captions?.caption_tracks) {
captions = player_response.captions.caption_tracks;
}
return FormatUtils.toDash(
this.streaming_data,
this.page[0].video_details?.is_post_live_dvr,
@@ -68,6 +121,7 @@ export default class MediaInfo {
this.#actions.session.player,
this.#actions,
storyboards,
captions,
options
);
}

View File

@@ -6,7 +6,7 @@ export default class CollectionThumbnailView extends YTNode {
static type = 'CollectionThumbnailView';
primary_thumbnail: ThumbnailView | null;
stack_color: {
stack_color?: {
light_theme: number;
dark_theme: number;
};
@@ -15,9 +15,11 @@ export default class CollectionThumbnailView extends YTNode {
super();
this.primary_thumbnail = Parser.parseItem(data.primaryThumbnail, ThumbnailView);
this.stack_color = {
light_theme: data.stackColor.lightTheme,
dark_theme: data.stackColor.darkTheme
};
if (data.stackColor) {
this.stack_color = {
light_theme: data.stackColor.lightTheme,
dark_theme: data.stackColor.darkTheme
};
}
}
}

View File

@@ -1,14 +0,0 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
import NavigationEndpoint from './NavigationEndpoint.js';
export default class Command extends YTNode {
static type = 'Command';
endpoint: NavigationEndpoint;
constructor(data: RawNode) {
super();
this.endpoint = new NavigationEndpoint(data);
}
}

View File

@@ -1,5 +1,5 @@
import { timeToSeconds } from '../../utils/Utils.js';
import { YTNode, type ObservedArray, type SuperParsedResult } from '../helpers.js';
import { YTNode, type ObservedArray } from '../helpers.js';
import { Parser, type RawNode } from '../index.js';
import Menu from './menus/Menu.js';
import MetadataBadge from './MetadataBadge.js';
@@ -13,7 +13,7 @@ export default class CompactVideo extends YTNode {
id: string;
thumbnails: Thumbnail[];
rich_thumbnail?: SuperParsedResult<YTNode>;
rich_thumbnail?: YTNode;
title: Text;
author: Author;
view_count: Text;
@@ -36,7 +36,7 @@ export default class CompactVideo extends YTNode {
this.thumbnails = Thumbnail.fromResponse(data.thumbnail) || null;
if (Reflect.has(data, 'richThumbnail')) {
this.rich_thumbnail = Parser.parse(data.richThumbnail);
this.rich_thumbnail = Parser.parseItem(data.richThumbnail);
}
this.title = new Text(data.title);

View File

@@ -0,0 +1,22 @@
import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import { type RawNode } from '../index.js';
export default class EomSettingsDisclaimer extends YTNode {
static type = 'EomSettingsDisclaimer';
disclaimer: Text;
info_icon: {
icon_type: string
};
usage_scenario: string;
constructor(data: RawNode) {
super();
this.disclaimer = new Text(data.disclaimer);
this.info_icon = {
icon_type: data.infoIcon.iconType
};
this.usage_scenario = data.usageScenario;
}
}

View File

@@ -7,7 +7,7 @@ export default class Factoid extends YTNode {
label: Text;
value: Text;
accessibility_text: String;
accessibility_text: string;
constructor(data: RawNode) {
super();

View File

@@ -10,7 +10,8 @@ export default class InfoPanelContent extends YTNode {
title: Text;
source: Text;
attributed_paragraphs: Text[];
paragraphs?: Text[];
attributed_paragraphs?: Text[];
thumbnail: Thumbnail[];
source_endpoint: NavigationEndpoint;
truncate_paragraphs: boolean;
@@ -21,7 +22,13 @@ export default class InfoPanelContent extends YTNode {
super();
this.title = new Text(data.title);
this.source = new Text(data.source);
this.attributed_paragraphs = data.attributedParagraphs.map((p: AttributedText) => Text.fromAttributed(p));
if (Reflect.has(data, 'paragraphs'))
this.paragraphs = data.paragraphs.map((p: RawNode) => new Text(p));
if (Reflect.has(data, 'attributedParagraphs'))
this.attributed_paragraphs = data.attributedParagraphs.map((p: AttributedText) => Text.fromAttributed(p));
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
this.source_endpoint = new NavigationEndpoint(data.sourceEndpoint);
this.truncate_paragraphs = !!data.truncateParagraphs;

View File

@@ -8,7 +8,7 @@ export default class InfoRow extends YTNode {
title: Text;
default_metadata?: Text;
expanded_metadata?: Text;
info_row_expand_status_key?: String;
info_row_expand_status_key?: string;
constructor(data: RawNode) {
super();

View File

@@ -5,11 +5,13 @@ export default class MusicEditablePlaylistDetailHeader extends YTNode {
static type = 'MusicEditablePlaylistDetailHeader';
header: YTNode;
edit_header: YTNode;
playlist_id: string;
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header);
// TODO: Parse data.editHeader.musicPlaylistEditHeaderRenderer.
this.edit_header = Parser.parseItem(data.editHeader);
this.playlist_id = data.playlistId;
}
}

View File

@@ -0,0 +1,28 @@
import { Parser, type RawNode } from '../index.js';
import { YTNode } from '../helpers.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Dropdown from './Dropdown.js';
import Text from './misc/Text.js';
export default class MusicPlaylistEditHeader extends YTNode {
static type = 'MusicPlaylistEditHeader';
title: Text;
edit_title: Text;
edit_description: Text;
privacy: string;
playlist_id: string;
endpoint: NavigationEndpoint;
privacy_dropdown: Dropdown | null;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.edit_title = new Text(data.editTitle);
this.edit_description = new Text(data.editDescription);
this.privacy = data.privacy;
this.playlist_id = data.playlistId;
this.endpoint = new NavigationEndpoint(data.collaborationSettingsCommand);
this.privacy_dropdown = Parser.parseItem(data.privacyDropdown, Dropdown);
}
}

View File

@@ -7,6 +7,8 @@ import MusicPlayButton from './MusicPlayButton.js';
import ToggleButton from './ToggleButton.js';
import Menu from './menus/Menu.js';
import Text from './misc/Text.js';
import Button from './Button.js';
import DownloadButton from './DownloadButton.js';
import type { ObservedArray } from '../helpers.js';
@@ -14,7 +16,7 @@ export default class MusicResponsiveHeader extends YTNode {
static type = 'MusicResponsiveHeader';
thumbnail: MusicThumbnail | null;
buttons: ObservedArray<ToggleButton | MusicPlayButton | Menu> | null;
buttons: ObservedArray<DownloadButton | ToggleButton | MusicPlayButton | Button | Menu>;
title: Text;
subtitle: Text;
strapline_text_one: Text;
@@ -26,7 +28,7 @@ export default class MusicResponsiveHeader extends YTNode {
constructor(data: RawNode) {
super();
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
this.buttons = Parser.parseArray(data.buttons, [ ToggleButton, MusicPlayButton, Menu ]);
this.buttons = Parser.parseArray(data.buttons, [ DownloadButton, ToggleButton, MusicPlayButton, Button, Menu ]);
this.title = new Text(data.title);
this.subtitle = new Text(data.subtitle);
this.strapline_text_one = new Text(data.straplineTextOne);

View File

@@ -27,8 +27,8 @@ export default class NavigationEndpoint extends YTNode {
constructor(data: RawNode) {
super();
if (Reflect.has(data || {}, 'innertubeCommand'))
data = data.innertubeCommand;
if (data && (data.innertubeCommand || data.command))
data = data.innertubeCommand || data.command;
if (Reflect.has(data || {}, 'openPopupAction'))
this.open_popup = new OpenPopupAction(data.openPopupAction);
@@ -92,13 +92,14 @@ export default class NavigationEndpoint extends YTNode {
case 'browseEndpoint':
return '/browse';
case 'watchEndpoint':
case 'reelWatchEndpoint':
return '/player';
case 'searchEndpoint':
return '/search';
case 'watchPlaylistEndpoint':
return '/next';
case 'liveChatItemContextMenuEndpoint':
return 'live_chat/get_item_context_menu';
return '/live_chat/get_item_context_menu';
}
}

View File

@@ -2,17 +2,19 @@ import Text from './misc/Text.js';
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';
export interface CaptionTrackData {
base_url: string;
name: Text;
vss_id: string;
language_code: string;
kind?: 'asr' | 'frc';
is_translatable: boolean;
}
export default class PlayerCaptionsTracklist extends YTNode {
static type = 'PlayerCaptionsTracklist';
caption_tracks?: {
base_url: string;
name: Text;
vss_id: string;
language_code: string;
kind?: 'asr' | 'frc';
is_translatable: boolean;
}[];
caption_tracks?: CaptionTrackData[];
audio_tracks?: {
audio_track_id: string;

View File

@@ -6,6 +6,7 @@ import InfoPanelContainer from './InfoPanelContainer.js';
import LikeButton from './LikeButton.js';
import ReelPlayerHeader from './ReelPlayerHeader.js';
import PivotButton from './PivotButton.js';
import SubscribeButton from './SubscribeButton.js';
export default class ReelPlayerOverlay extends YTNode {
static type = 'ReelPlayerOverlay';
@@ -29,7 +30,7 @@ export default class ReelPlayerOverlay extends YTNode {
this.menu = Parser.parseItem(data.menu, Menu);
this.next_item_button = Parser.parseItem(data.nextItemButton, Button);
this.prev_item_button = Parser.parseItem(data.prevItemButton, Button);
this.subscribe_button_renderer = Parser.parseItem(data.subscribeButtonRenderer, Button);
this.subscribe_button_renderer = Parser.parseItem(data.subscribeButtonRenderer, [ Button, SubscribeButton ]);
this.style = data.style;
this.view_comments_button = Parser.parseItem(data.viewCommentsButton, Button);
this.share_button = Parser.parseItem(data.shareButton, Button);

View File

@@ -8,6 +8,7 @@ import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js';
import VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.js';
import VideoDescriptionCourseSection from './VideoDescriptionCourseSection.js';
import ReelShelf from './ReelShelf.js';
import VideoAttributesSectionView from './VideoAttributesSectionView.js';
export default class StructuredDescriptionContent extends YTNode {
static type = 'StructuredDescriptionContent';
@@ -15,7 +16,7 @@ export default class StructuredDescriptionContent extends YTNode {
items: ObservedArray<
VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection |
VideoDescriptionInfocardsSection | VideoDescriptionTranscriptSection | VideoDescriptionTranscriptSection |
VideoDescriptionCourseSection | HorizontalCardList | ReelShelf
VideoDescriptionCourseSection | HorizontalCardList | ReelShelf | VideoAttributesSectionView
>;
constructor(data: RawNode) {
@@ -23,7 +24,7 @@ export default class StructuredDescriptionContent extends YTNode {
this.items = Parser.parseArray(data.items, [
VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection,
VideoDescriptionInfocardsSection, VideoDescriptionCourseSection, VideoDescriptionTranscriptSection,
VideoDescriptionTranscriptSection, HorizontalCardList, ReelShelf
VideoDescriptionTranscriptSection, HorizontalCardList, ReelShelf, VideoAttributesSectionView
]);
}
}

View File

@@ -7,7 +7,7 @@ export default class ThumbnailBadgeView extends YTNode {
icon_name: string;
text: string;
badge_style: string;
background_color: {
background_color?: {
light_theme: number;
dark_theme: number;
};
@@ -18,9 +18,11 @@ export default class ThumbnailBadgeView extends YTNode {
this.icon_name = data.icon.sources[0].clientResource.imageName;
this.text = data.text;
this.badge_style = data.badgeStyle;
this.background_color = {
light_theme: data.backgroundColor.lightTheme,
dark_theme: data.backgroundColor.darkTheme
};
if (data.backgroundColor) {
this.background_color = {
light_theme: data.backgroundColor.lightTheme,
dark_theme: data.backgroundColor.darkTheme
};
}
}
}

View File

@@ -4,10 +4,10 @@ import type { RawNode } from '../index.js';
export default class ThumbnailOverlayResumePlayback extends YTNode {
static type = 'ThumbnailOverlayResumePlayback';
percent_duration_watched: string; // TODO: is this a number?
percent_duration_watched: number;
constructor(data: RawNode) {
super();
this.percent_duration_watched = data.percentDurationWatched;
}
}
}

View File

@@ -17,8 +17,8 @@ export default class ThumbnailOverlayToggleButton extends YTNode {
untoggled: string;
};
toggled_endpoint: NavigationEndpoint;
untoggled_endpoint: NavigationEndpoint;
toggled_endpoint?: NavigationEndpoint;
untoggled_endpoint?: NavigationEndpoint;
constructor(data: RawNode) {
super();
@@ -36,7 +36,10 @@ export default class ThumbnailOverlayToggleButton extends YTNode {
untoggled: data.untoggledTooltip
};
this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint);
this.untoggled_endpoint = new NavigationEndpoint(data.untoggledServiceEndpoint);
if (data.toggledServiceEndpoint)
this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint);
if (data.untoggledServiceEndpoint)
this.untoggled_endpoint = new NavigationEndpoint(data.untoggledServiceEndpoint);
}
}

View File

@@ -9,7 +9,7 @@ export default class ThumbnailView extends YTNode {
image: Thumbnail[];
overlays: (ThumbnailOverlayBadgeView | ThumbnailHoverOverlayView)[];
background_color: {
background_color?: {
light_theme: number;
dark_theme: number;
};
@@ -19,9 +19,11 @@ export default class ThumbnailView extends YTNode {
this.image = Thumbnail.fromResponse(data.image);
this.overlays = Parser.parseArray(data.overlays, [ ThumbnailOverlayBadgeView, ThumbnailHoverOverlayView ]);
this.background_color = {
light_theme: data.backgroundColor.lightTheme,
dark_theme: data.backgroundColor.darkTheme
};
if (data.backgroundColor) {
this.background_color = {
light_theme: data.backgroundColor.lightTheme,
dark_theme: data.backgroundColor.darkTheme
};
}
}
}

View File

@@ -0,0 +1,24 @@
import { Parser, type RawNode } from '../index.js';
import { YTNode, type ObservedArray } from '../helpers.js';
import ButtonView from './ButtonView.js';
import VideoAttributeView from './VideoAttributeView.js';
export default class VideoAttributesSectionView extends YTNode {
static type = 'VideoAttributesSectionView';
header_title: string;
header_subtitle: string;
video_attributes: ObservedArray<VideoAttributeView>;
previous_button: ButtonView | null;
next_button: ButtonView | null;
constructor(data: RawNode) {
super();
this.header_title = data.headerTitle;
this.header_subtitle = data.headerSubtitle;
this.video_attributes = Parser.parseArray(data.videoAttributeViewModels, VideoAttributeView);
this.previous_button = Parser.parseItem(data.previousButton, ButtonView);
this.next_button = Parser.parseItem(data.nextButton, ButtonView);
}
}

View File

@@ -20,9 +20,10 @@ export default class AuthorCommentBadge extends YTNode {
this.tooltip = data.iconTooltip;
// *** For consistency
this.tooltip === 'Verified' &&
(this.style = 'BADGE_STYLE_TYPE_VERIFIED') &&
(data.style = 'BADGE_STYLE_TYPE_VERIFIED');
if (this.tooltip === 'Verified') {
this.style = 'BADGE_STYLE_TYPE_VERIFIED';
data.style = 'BADGE_STYLE_TYPE_VERIFIED';
}
this.#data = data;
}

View File

@@ -1,7 +1,8 @@
import Text from '../misc/Text.js';
import { YTNode } from '../../helpers.js';
import type { RawNode } from '../../index.js';
class UpdateToggleButtonTextAction extends YTNode {
export default class UpdateToggleButtonTextAction extends YTNode {
static type = 'UpdateToggleButtonTextAction';
default_text: string;
@@ -14,6 +15,4 @@ class UpdateToggleButtonTextAction extends YTNode {
this.toggled_text = new Text(data.toggledText).toString();
this.button_id = data.buttonId;
}
}
export default UpdateToggleButtonTextAction;
}

View File

@@ -7,7 +7,7 @@ export default class UpdateViewershipAction extends YTNode {
view_count: Text;
extra_short_view_count: Text;
original_view_count: Number;
original_view_count: number;
unlabeled_view_count_value: Text;
is_live: boolean;

View File

@@ -1,18 +1,19 @@
import { YTNode, type SuperParsedResult } from '../../helpers.js';
import type { ObservedArray } from '../../helpers.js';
import { YTNode } from '../../helpers.js';
import type { RawNode } from '../../index.js';
import { Parser } from '../../index.js';
export default class MultiPageMenu extends YTNode {
static type = 'MultiPageMenu';
header: SuperParsedResult<YTNode>;
sections: SuperParsedResult<YTNode>;
header: YTNode;
sections: ObservedArray<YTNode>;
style: string;
constructor(data: RawNode) {
super();
this.header = Parser.parse(data.header);
this.sections = Parser.parse(data.sections);
this.header = Parser.parseItem(data.header);
this.sections = Parser.parseArray(data.sections);
this.style = data.style;
}
}

View File

@@ -1,19 +1,19 @@
import type { SuperParsedResult } from '../../helpers.js';
import type { ObservedArray } from '../../helpers.js';
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';
export default class SimpleMenuHeader extends YTNode {
static type = 'SimpleMenuHeader';
title: Text;
buttons: SuperParsedResult<YTNode>;
buttons: ObservedArray<Button>;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
// @TODO: Check if this is of type `Button`.
this.buttons = Parser.parse(data.buttons);
this.buttons = Parser.parseArray(data.buttons, Button);
}
}

View File

@@ -6,61 +6,64 @@ export default class Format {
#this_response_nsig_cache?: Map<string, string>;
itag: number;
url?: string;
width?: number;
height?: number;
last_modified: Date;
content_length?: number;
quality?: string;
xtags?: string;
drm_families?: string[];
fps?: number;
quality_label?: string;
projection_type?: 'RECTANGULAR' | 'EQUIRECTANGULAR' | 'EQUIRECTANGULAR_THREED_TOP_BOTTOM' | 'MESH';
average_bitrate?: number;
bitrate: number;
spatial_audio_type?: 'AMBISONICS_5_1' | 'AMBISONICS_QUAD' | 'FOA_WITH_NON_DIEGETIC';
target_duration_dec?: number;
fair_play_key_uri?: string;
stereo_layout?: 'LEFT_RIGHT' | 'TOP_BOTTOM';
max_dvr_duration_sec?: number;
high_replication?: boolean;
audio_quality?: string;
approx_duration_ms: number;
audio_sample_rate?: number;
audio_channels?: number;
loudness_db?: number;
signature_cipher?: string;
is_drc?: boolean;
drm_track_type?: string;
distinct_params?: string;
track_absolute_loudness_lkfs?: number;
mime_type: string;
is_type_otf: boolean;
bitrate: number;
average_bitrate?: number;
width: number;
height: number;
projection_type?: 'RECTANGULAR' | 'EQUIRECTANGULAR' | 'EQUIRECTANGULAR_THREED_TOP_BOTTOM' | 'MESH';
stereo_layout?: 'LEFT_RIGHT' | 'TOP_BOTTOM';
init_range?: {
start: number;
end: number;
};
index_range?: {
start: number;
end: number;
};
last_modified: Date;
content_length?: number;
quality?: string;
quality_label?: string;
fps?: number;
url?: string;
cipher?: string;
signature_cipher?: string;
audio_quality?: string;
audio_track?: {
audio_is_default: boolean;
display_name: string;
id: string;
};
approx_duration_ms: number;
audio_sample_rate?: number;
audio_channels?: number;
loudness_db?: number;
spatial_audio_type?: 'AMBISONICS_5_1' | 'AMBISONICS_QUAD' | 'FOA_WITH_NON_DIEGETIC';
max_dvr_duration_sec?: number;
target_duration_dec?: number;
has_audio: boolean;
has_video: boolean;
has_text: boolean;
language?: string | null;
is_dubbed?: boolean;
is_descriptive?: boolean;
is_secondary?: boolean;
is_original?: boolean;
is_drc?: boolean;
color_info?: {
primaries?: string;
transfer_characteristics?: string;
matrix_coefficients?: string;
};
caption_track?: {
display_name: string;
vss_id: string;
@@ -79,56 +82,116 @@ export default class Format {
this.is_type_otf = data.type === 'FORMAT_STREAM_TYPE_OTF';
this.bitrate = data.bitrate;
this.average_bitrate = data.averageBitrate;
this.width = data.width;
this.height = data.height;
this.projection_type = data.projectionType;
this.stereo_layout = data.stereoLayout?.replace('STEREO_LAYOUT_', '');
this.init_range = data.initRange ? {
start: parseInt(data.initRange.start),
end: parseInt(data.initRange.end)
} : undefined;
if (Reflect.has(data, 'width') && Reflect.has(data, 'height')) {
this.width = parseInt(data.width);
this.height = parseInt(data.height);
}
this.index_range = data.indexRange ? {
start: parseInt(data.indexRange.start),
end: parseInt(data.indexRange.end)
} : undefined;
if (Reflect.has(data, 'projectionType'))
this.projection_type = data.projectionType;
if (Reflect.has(data, 'stereoLayout'))
this.stereo_layout = data.stereoLayout?.replace('STEREO_LAYOUT_', '');
if (Reflect.has(data, 'initRange'))
this.init_range = {
start: parseInt(data.initRange.start),
end: parseInt(data.initRange.end)
};
if (Reflect.has(data, 'indexRange'))
this.index_range = {
start: parseInt(data.indexRange.start),
end: parseInt(data.indexRange.end)
};
this.last_modified = new Date(Math.floor(parseInt(data.lastModified) / 1000));
this.content_length = parseInt(data.contentLength);
this.quality = data.quality;
this.quality_label = data.qualityLabel;
this.fps = data.fps;
this.url = data.url;
this.cipher = data.cipher;
this.signature_cipher = data.signatureCipher;
this.audio_quality = data.audioQuality;
if (Reflect.has(data, 'contentLength'))
this.content_length = parseInt(data.contentLength);
if (Reflect.has(data, 'quality'))
this.quality = data.quality;
if (Reflect.has(data, 'qualityLabel'))
this.quality_label = data.qualityLabel;
if (Reflect.has(data, 'fps'))
this.fps = data.fps;
if (Reflect.has(data, 'url'))
this.url = data.url;
if (Reflect.has(data, 'cipher'))
this.cipher = data.cipher;
if (Reflect.has(data, 'signatureCipher'))
this.signature_cipher = data.signatureCipher;
if (Reflect.has(data, 'audioQuality'))
this.audio_quality = data.audioQuality;
this.approx_duration_ms = parseInt(data.approxDurationMs);
this.audio_sample_rate = parseInt(data.audioSampleRate);
this.audio_channels = data.audioChannels;
this.loudness_db = data.loudnessDb;
this.spatial_audio_type = data.spatialAudioType?.replace('SPATIAL_AUDIO_TYPE_', '');
this.max_dvr_duration_sec = data.maxDvrDurationSec;
this.target_duration_dec = data.targetDurationSec;
if (Reflect.has(data, 'audioSampleRate'))
this.audio_sample_rate = parseInt(data.audioSampleRate);
if (Reflect.has(data, 'audioChannels'))
this.audio_channels = data.audioChannels;
if (Reflect.has(data, 'loudnessDb'))
this.loudness_db = data.loudnessDb;
if (Reflect.has(data, 'spatialAudioType'))
this.spatial_audio_type = data.spatialAudioType?.replace('SPATIAL_AUDIO_TYPE_', '');
if (Reflect.has(data, 'maxDvrDurationSec'))
this.max_dvr_duration_sec = data.maxDvrDurationSec;
if (Reflect.has(data, 'targetDurationSec'))
this.target_duration_dec = data.targetDurationSec;
this.has_audio = !!data.audioBitrate || !!data.audioQuality;
this.has_video = !!data.qualityLabel;
this.has_text = !!data.captionTrack;
this.color_info = data.colorInfo ? {
primaries: data.colorInfo.primaries?.replace('COLOR_PRIMARIES_', ''),
transfer_characteristics: data.colorInfo.transferCharacteristics?.replace('COLOR_TRANSFER_CHARACTERISTICS_', ''),
matrix_coefficients: data.colorInfo.matrixCoefficients?.replace('COLOR_MATRIX_COEFFICIENTS_', '')
} : undefined;
if (Reflect.has(data, 'xtags'))
this.xtags = data.xtags;
if (Reflect.has(data, 'audioTrack')) {
if (Reflect.has(data, 'fairPlayKeyUri'))
this.fair_play_key_uri = data.fairPlayKeyUri;
if (Reflect.has(data, 'drmFamilies'))
this.drm_families = data.drmFamilies;
if (Reflect.has(data, 'drmTrackType'))
this.drm_track_type = data.drmTrackType;
if (Reflect.has(data, 'distinctParams'))
this.distinct_params = data.distinctParams;
if (Reflect.has(data, 'trackAbsoluteLoudnessLkfs'))
this.track_absolute_loudness_lkfs = data.trackAbsoluteLoudnessLkfs;
if (Reflect.has(data, 'highReplication'))
this.high_replication = data.highReplication;
if (Reflect.has(data, 'colorInfo'))
this.color_info = {
primaries: data.colorInfo.primaries?.replace('COLOR_PRIMARIES_', ''),
transfer_characteristics: data.colorInfo.transferCharacteristics?.replace('COLOR_TRANSFER_CHARACTERISTICS_', ''),
matrix_coefficients: data.colorInfo.matrixCoefficients?.replace('COLOR_MATRIX_COEFFICIENTS_', '')
};
if (Reflect.has(data, 'audioTrack'))
this.audio_track = {
audio_is_default: data.audioTrack.audioIsDefault,
display_name: data.audioTrack.displayName,
id: data.audioTrack.id
};
}
if (Reflect.has(data, 'captionTrack')) {
if (Reflect.has(data, 'captionTrack'))
this.caption_track = {
display_name: data.captionTrack.displayName,
vss_id: data.captionTrack.vssId,
@@ -136,7 +199,6 @@ export default class Format {
kind: data.captionTrack.kind,
id: data.captionTrack.id
};
}
if (this.has_audio || this.has_text) {
const args = new URLSearchParams(this.cipher || this.signature_cipher);
@@ -152,7 +214,8 @@ export default class Format {
const audio_content = xtags?.find((x) => x.startsWith('acont='))?.split('=')[1];
this.is_dubbed = audio_content === 'dubbed';
this.is_descriptive = audio_content === 'descriptive';
this.is_original = audio_content === 'original' || (!this.is_dubbed && !this.is_descriptive && !this.is_drc);
this.is_secondary = audio_content === 'secondary';
this.is_original = audio_content === 'original' || (!this.is_dubbed && !this.is_descriptive && !this.is_secondary && !this.is_drc);
}
// Some text tracks don't have xtags while others do

View File

@@ -154,7 +154,7 @@ export class Maybe {
return this.#checkPrimative('object');
}
/* eslint-ignore */
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function(): Function {
return this.#assertPrimative('function');
}
@@ -352,7 +352,6 @@ export class SuperParsedResult<T extends YTNode = YTNode> {
}
}
export type ObservedArray<T extends YTNode = YTNode> = Array<T> & {
/**
* Returns the first object to match the rule.
@@ -442,7 +441,6 @@ export function observe<T extends YTNode>(obj: Array<T>): ObservedArray<T> {
};
}
if (prop == 'firstOfType') {
return (...types: YTNodeConstructor<YTNode>[]) => {
return target.find((node: YTNode) => {

View File

@@ -1,6 +1,4 @@
export * as Parser from './parser.js';
export * from './continuations.js';
export * from './types/index.js';
export * as Misc from './misc.js';
export * as YTNodes from './nodes.js';
export * as YT from './youtube/index.js';
@@ -8,4 +6,9 @@ export * as YTMusic from './ytmusic/index.js';
export * as YTKids from './ytkids/index.js';
export * as YTShorts from './ytshorts/index.js';
export * as Helpers from './helpers.js';
export * as Generator from './generator.js';
export * as Generator from './generator.js';
export * as APIResponseTypes from './types/index.js';
export * from './continuations.js';
// @TODO: Remove this when files are updated to use APIResponseTypes or /types/index.js directly.
export * from './types/index.js';

View File

@@ -67,7 +67,6 @@ export { default as ClipSection } from './classes/ClipSection.js';
export { default as CollaboratorInfoCardContent } from './classes/CollaboratorInfoCardContent.js';
export { default as CollageHeroImage } from './classes/CollageHeroImage.js';
export { default as CollectionThumbnailView } from './classes/CollectionThumbnailView.js';
export { default as Command } from './classes/Command.js';
export { default as AuthorCommentBadge } from './classes/comments/AuthorCommentBadge.js';
export { default as Comment } from './classes/comments/Comment.js';
export { default as CommentActionButtons } from './classes/comments/CommentActionButtons.js';
@@ -120,6 +119,7 @@ export { default as EndScreenPlaylist } from './classes/EndScreenPlaylist.js';
export { default as EndScreenVideo } from './classes/EndScreenVideo.js';
export { default as EngagementPanelSectionList } from './classes/EngagementPanelSectionList.js';
export { default as EngagementPanelTitleHeader } from './classes/EngagementPanelTitleHeader.js';
export { default as EomSettingsDisclaimer } from './classes/EomSettingsDisclaimer.js';
export { default as ExpandableMetadata } from './classes/ExpandableMetadata.js';
export { default as ExpandableTab } from './classes/ExpandableTab.js';
export { default as ExpandableVideoDescriptionBody } from './classes/ExpandableVideoDescriptionBody.js';
@@ -261,6 +261,7 @@ export { default as MusicLargeCardItemCarousel } from './classes/MusicLargeCardI
export { default as MusicMultiRowListItem } from './classes/MusicMultiRowListItem.js';
export { default as MusicNavigationButton } from './classes/MusicNavigationButton.js';
export { default as MusicPlayButton } from './classes/MusicPlayButton.js';
export { default as MusicPlaylistEditHeader } from './classes/MusicPlaylistEditHeader.js';
export { default as MusicPlaylistShelf } from './classes/MusicPlaylistShelf.js';
export { default as MusicQueue } from './classes/MusicQueue.js';
export { default as MusicResponsiveHeader } from './classes/MusicResponsiveHeader.js';
@@ -414,6 +415,7 @@ export { default as UpsellDialog } from './classes/UpsellDialog.js';
export { default as VerticalList } from './classes/VerticalList.js';
export { default as VerticalWatchCardList } from './classes/VerticalWatchCardList.js';
export { default as Video } from './classes/Video.js';
export { default as VideoAttributesSectionView } from './classes/VideoAttributesSectionView.js';
export { default as VideoAttributeView } from './classes/VideoAttributeView.js';
export { default as VideoCard } from './classes/VideoCard.js';
export { default as VideoDescriptionCourseSection } from './classes/VideoDescriptionCourseSection.js';

View File

@@ -26,6 +26,7 @@ import Format from './classes/misc/Format.js';
import VideoDetails from './classes/misc/VideoDetails.js';
import NavigationEndpoint from './classes/NavigationEndpoint.js';
import CommentView from './classes/comments/CommentView.js';
import MusicThumbnail from './classes/MusicThumbnail.js';
import type { KeyInfo } from './generator.js';
import type { ObservedArray, YTNodeConstructor, YTNode } from './helpers.js';
@@ -76,7 +77,8 @@ const IGNORED_LIST = new Set([
'BrandVideoSingleton',
'StatementBanner',
'GuideSigninPromo',
'AdsEngagementPanelContent'
'AdsEngagementPanelContent',
'MiniGameCardView'
]);
const RUNTIME_NODES = new Map<string, YTNodeConstructor>(Object.entries(YTNodes));
@@ -93,7 +95,8 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError)
new InnertubeError(
`Something went wrong at ${classname}!\n` +
`This is a bug, please report it at ${Platform.shim.info.bugs_url}`, {
stack: context.error.stack
stack: context.error.stack,
classdata: JSON.stringify(context.classdata, null, 2)
}
)
);
@@ -308,14 +311,6 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
}
_clearMemo();
_createMemo();
const entries = parse(data.entries);
if (entries) {
parsed_data.entries = entries;
parsed_data.entries_memo = _getMemo();
}
_clearMemo();
applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
if (on_response_received_endpoints_memo) {
@@ -367,6 +362,11 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
parsed_data.player_overlays = player_overlays;
}
const background = parseItem(data.background, MusicThumbnail);
if (background) {
parsed_data.background = background;
}
const playback_tracking = data.playbackTracking ? {
videostats_watchtime_url: data.playbackTracking.videostatsWatchtimeUrl.baseUrl,
videostats_playback_url: data.playbackTracking.videostatsPlaybackUrl.baseUrl
@@ -473,6 +473,29 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
parsed_data.engagement_panels = engagement_panels;
}
if (data.playerResponse) {
const player_response = parseResponse(data.playerResponse);
parsed_data.player_response = player_response;
}
if (data.watchNextResponse) {
const watch_next_response = parseResponse(data.watchNextResponse);
parsed_data.watch_next_response = watch_next_response;
}
if (data.cpnInfo) {
const cpn_info = {
cpn: data.cpnInfo.cpn,
cpn_source: data.cpnInfo.cpnSource
};
parsed_data.cpn_info = cpn_info;
}
if (data.entries) {
parsed_data.entries = data.entries.map((entry) => new NavigationEndpoint(entry));
}
return parsed_data;
}

View File

@@ -1,11 +1,11 @@
import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../helpers.js';
import type {
ReloadContinuationItemsCommand, Continuation, GridContinuation,
ItemSectionContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation,
PlaylistPanelContinuation, SectionListContinuation, ContinuationCommand
} from '../index.js';
import type { CpnSource } from './RawResponse.js';
import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js';
import type CardCollection from '../classes/CardCollection.js';
import type Endscreen from '../classes/Endscreen.js';
@@ -19,9 +19,10 @@ import type AlertWithButton from '../classes/AlertWithButton.js';
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.js';
import type EngagementPanelSectionList from '../classes/EngagementPanelSectionList.js';
import type { AppendContinuationItemsAction } from '../nodes.js';
import type { AppendContinuationItemsAction, MusicThumbnail } from '../nodes.js';
export interface IParsedResponse {
background?: MusicThumbnail;
actions?: SuperParsedResult<YTNode>;
actions_memo?: Memo;
contents?: SuperParsedResult<YTNode>;
@@ -50,10 +51,7 @@ export interface IParsedResponse {
refinements?: string[];
estimated_results?: number;
player_overlays?: SuperParsedResult<YTNode>;
playback_tracking?: {
videostats_watchtime_url: string;
videostats_playback_url: string;
};
playback_tracking?: IPlaybackTracking;
playability_status?: IPlayabilityStatus;
streaming_data?: IStreamingData;
player_config?: IPlayerConfig;
@@ -65,11 +63,29 @@ export interface IParsedResponse {
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
endscreen?: Endscreen;
cards?: CardCollection;
cpn_info?: {
cpn: string;
cpn_source: CpnSource;
},
engagement_panels?: ObservedArray<EngagementPanelSectionList>;
items?: SuperParsedResult<YTNode>;
entries?: SuperParsedResult<YTNode>;
entries?: NavigationEndpoint[];
entries_memo?: Memo;
continuation_endpoint?: YTNode;
player_response?: IPlayerResponse;
watch_next_response?: INextResponse;
}
export interface IPlaybackTracking {
videostats_watchtime_url: string;
videostats_playback_url: string;
}
export interface IPlayabilityStatus {
status: string;
error_screen: YTNode | null;
audio_only_playablility: AudioOnlyPlayability | null;
embeddable: boolean;
reason: string;
}
export interface IPlayerConfig {
@@ -98,94 +114,12 @@ export interface IStreamingData {
hls_manifest_url: string | null;
}
export interface IPlayerResponse {
captions?: PlayerCaptionsTracklist;
cards?: CardCollection;
endscreen?: Endscreen;
microformat?: YTNode;
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
playability_status: IPlayabilityStatus;
streaming_data?: IStreamingData;
player_config: IPlayerConfig;
playback_tracking?: {
videostats_watchtime_url: string;
videostats_playback_url: string;
};
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
video_details?: VideoDetails;
}
export interface IPlayabilityStatus {
status: string;
error_screen: YTNode | null;
audio_only_playablility: AudioOnlyPlayability | null;
embeddable: boolean;
reason: string;
}
export interface INextResponse {
contents?: SuperParsedResult<YTNode>;
contents_memo?: Memo;
current_video_endpoint?: NavigationEndpoint;
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
on_response_received_endpoints_memo?: Memo;
player_overlays?: SuperParsedResult<YTNode>;
engagement_panels?: ObservedArray<EngagementPanelSectionList>;
}
export interface IBrowseResponse {
continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation |
MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation;
continuation_contents_memo?: Memo;
on_response_received_actions: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
on_response_received_actions_memo: Memo;
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
on_response_received_endpoints_memo?: Memo;
contents?: SuperParsedResult<YTNode>;
contents_memo?: Memo;
header?: SuperParsedResult<YTNode>;
header_memo?: Memo;
metadata?: SuperParsedResult<YTNode>;
microformat?: YTNode;
alerts?: ObservedArray<Alert | AlertWithButton>;
sidebar?: YTNode;
sidebar_memo?: Memo;
}
export interface ISearchResponse {
header?: SuperParsedResult<YTNode>;
header_memo?: Memo;
contents?: SuperParsedResult<YTNode>;
contents_memo?: Memo;
on_response_received_commands?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation |
MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation;
continuation_contents_memo?: Memo;
refinements?: string[];
estimated_results: number;
}
export interface IResolveURLResponse {
endpoint: NavigationEndpoint;
}
export interface IGetTranscriptResponse {
actions: SuperParsedResult<YTNode>;
actions_memo: Memo;
}
export interface IGetNotificationsMenuResponse {
actions: SuperParsedResult<YTNode>;
actions_memo: Memo;
}
export interface IUpdatedMetadataResponse {
actions: SuperParsedResult<YTNode>;
actions_memo: Memo;
continuation?: Continuation;
}
export interface IGuideResponse {
items: SuperParsedResult<YTNode>;
items_memo: Memo;
}
export type IPlayerResponse = Pick<IParsedResponse, 'captions' | 'cards' | 'endscreen' | 'microformat' | 'annotations' | 'playability_status' | 'streaming_data' | 'player_config' | 'playback_tracking' | 'storyboards' | 'video_details'>;
export type INextResponse = Pick<IParsedResponse, 'contents' | 'contents_memo' | 'current_video_endpoint' | 'on_response_received_endpoints' | 'on_response_received_endpoints_memo' | 'player_overlays' | 'engagement_panels'>
export type IBrowseResponse = Pick<IParsedResponse, 'background' | 'continuation_contents' | 'continuation_contents_memo' | 'on_response_received_actions' | 'on_response_received_actions_memo' | 'on_response_received_endpoints' | 'on_response_received_endpoints_memo' | 'contents' | 'contents_memo' | 'header' | 'header_memo' | 'metadata' | 'microformat' | 'alerts' | 'sidebar' | 'sidebar_memo'>
export type ISearchResponse = Pick<IParsedResponse, 'header' | 'header_memo' | 'contents' | 'contents_memo' | 'on_response_received_commands' | 'continuation_contents' | 'continuation_contents_memo' | 'refinements' | 'estimated_results'>;
export type IResolveURLResponse = Pick<IParsedResponse, 'endpoint'>;
export type IGetTranscriptResponse = Pick<IParsedResponse, 'actions' | 'actions_memo'>
export type IGetNotificationsMenuResponse = Pick<IParsedResponse, 'actions' | 'actions_memo'>
export type IUpdatedMetadataResponse = Pick<IParsedResponse, 'actions' | 'actions_memo' | 'continuation'>
export type IGuideResponse = Pick<IParsedResponse, 'items' | 'items_memo'>

View File

@@ -1,6 +1,21 @@
export type RawNode = Record<string, any>;
export type RawData = RawNode | RawNode[];
export type CpnSource = 'CPN_SOURCE_TYPE_CLIENT' | 'CPN_SOURCE_TYPE_WATCH_SERVER';
export interface IServiceTrackingParams {
service: string;
params?: {
key: string;
value: string;
}[];
}
export interface IResponseContext {
serviceTrackingParams: IServiceTrackingParams[];
maxAgeSeconds: number;
}
export interface IRawPlayerConfig {
audioConfig: {
loudnessDb?: number;
@@ -20,6 +35,8 @@ export interface IRawPlayerConfig {
}
export interface IRawResponse {
responseContext?: IResponseContext;
background?: RawNode;
contents?: RawData;
onResponseReceivedActions?: RawNode[];
onResponseReceivedEndpoints?: RawNode[];
@@ -60,6 +77,8 @@ export interface IRawResponse {
hlsManifestUrl?: string;
};
playerConfig?: IRawPlayerConfig;
playerResponse?: IRawResponse;
watchNextResponse?: IRawResponse;
currentVideoEndpoint?: RawNode;
unseenCount?: number;
playlistId?: string;
@@ -70,8 +89,13 @@ export interface IRawResponse {
storyboards?: RawNode;
endscreen?: RawNode;
cards?: RawNode;
cpnInfo?: {
cpn: string;
cpnSource: CpnSource;
},
items?: RawNode[];
frameworkUpdates?: any;
engagementPanels: RawNode[];
engagementPanels?: RawNode[];
entries?: RawNode[];
[key: string]: any;
}

View File

@@ -7,7 +7,7 @@ import type { IParsedResponse } from '../types/index.js';
import type AccountItemSection from '../classes/AccountItemSection.js';
import type AccountChannel from '../classes/AccountChannel.js';
class AccountInfo {
export default class AccountInfo {
#page: IParsedResponse;
contents: AccountItemSection | null;
@@ -31,6 +31,4 @@ class AccountInfo {
get page(): IParsedResponse {
return this.#page;
}
}
export default AccountInfo;
}

View File

@@ -3,7 +3,7 @@ import Element from '../classes/Element.js';
import type { ApiResponse } from '../../core/index.js';
import type { IBrowseResponse } from '../types/index.js';
class Analytics {
export default class Analytics {
#page: IBrowseResponse;
sections;
@@ -15,6 +15,4 @@ class Analytics {
get page(): IBrowseResponse {
return this.#page;
}
}
export default Analytics;
}

View File

@@ -315,7 +315,7 @@ export class ChannelListContinuation extends Feed<IBrowseResponse> {
export class FilteredChannelList extends FilterableFeed<IBrowseResponse> {
applied_filter?: ChipCloudChip;
contents: ReloadContinuationItemsCommand | AppendContinuationItemsAction;
contents?: ReloadContinuationItemsCommand | AppendContinuationItemsAction;
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
super(actions, data, already_parsed);
@@ -330,7 +330,7 @@ export class FilteredChannelList extends FilterableFeed<IBrowseResponse> {
this.page.on_response_received_actions.shift();
}
this.contents = this.page.on_response_received_actions.first();
this.contents = this.page.on_response_received_actions?.first();
}
/**

View File

@@ -11,7 +11,7 @@ import type { Actions, ApiResponse } from '../../core/index.js';
import type { ObservedArray } from '../helpers.js';
import type { INextResponse } from '../types/index.js';
class Comments {
export default class Comments {
#page: INextResponse;
#actions: Actions;
#continuation?: ContinuationItem;
@@ -121,6 +121,4 @@ class Comments {
get page(): INextResponse {
return this.#page;
}
}
export default Comments;
}

View File

@@ -7,13 +7,13 @@ import type { IGuideResponse } from '../types/index.js';
import type { IRawResponse } from '../index.js';
export default class Guide {
#page: IGuideResponse;
contents: ObservedArray<GuideSection | GuideSubscriptionsSection>;
contents?: ObservedArray<GuideSection | GuideSubscriptionsSection>;
constructor(data: IRawResponse) {
this.#page = Parser.parseResponse<IGuideResponse>(data);
this.contents = this.#page.items.array().as(GuideSection, GuideSubscriptionsSection);
if (this.#page.items)
this.contents = this.#page.items.array().as(GuideSection, GuideSubscriptionsSection);
}
get page(): IGuideResponse {

View File

@@ -6,7 +6,7 @@ import type { Actions, ApiResponse } from '../../core/index.js';
import type { IBrowseResponse } from '../types/index.js';
// TODO: make feed actions usable
class History extends Feed<IBrowseResponse> {
export default class History extends Feed<IBrowseResponse> {
sections: ItemSection[];
feed_actions: BrowseFeedActions;
@@ -25,6 +25,4 @@ class History extends Feed<IBrowseResponse> {
throw new Error('No continuation data found');
return new History(this.actions, response, true);
}
}
export default History;
}

View File

@@ -8,13 +8,13 @@ import type { ApiResponse, Actions } from '../../core/index.js';
import type ChipCloudChip from '../classes/ChipCloudChip.js';
export default class HomeFeed extends FilterableFeed<IBrowseResponse> {
contents: RichGrid | AppendContinuationItemsAction | ReloadContinuationItemsCommand;
header: FeedTabbedHeader;
contents?: RichGrid | AppendContinuationItemsAction | ReloadContinuationItemsCommand;
header?: FeedTabbedHeader;
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
super(actions, data, already_parsed);
this.header = this.memo.getType(FeedTabbedHeader).first();
this.contents = this.memo.getType(RichGrid).first() || this.page.on_response_received_actions.first();
this.contents = this.memo.getType(RichGrid).first() || this.page.on_response_received_actions?.first();
}
/**
@@ -34,7 +34,9 @@ export default class HomeFeed extends FilterableFeed<IBrowseResponse> {
// Keep the page header
feed.page.header = this.page.header;
feed.page.header_memo?.set(this.header.type, [ this.header ]);
if (this.header)
feed.page.header_memo?.set(this.header.type, [ this.header ]);
return new HomeFeed(this.actions, feed.page, true);
}

View File

@@ -8,7 +8,7 @@ import type { ObservedArray, YTNode } from '../helpers.js';
import type { IParsedResponse } from '../types/index.js';
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
class ItemMenu {
export default class ItemMenu {
#page: IParsedResponse;
#actions: Actions;
#items: ObservedArray<YTNode>;
@@ -65,6 +65,4 @@ class ItemMenu {
page(): IParsedResponse {
return this.#page;
}
}
export default ItemMenu;
}

View File

@@ -10,7 +10,7 @@ import PageHeader from '../classes/PageHeader.js';
import type { Actions, ApiResponse } from '../../core/index.js';
import type { IBrowseResponse } from '../types/index.js';
class Library extends Feed<IBrowseResponse> {
export default class Library extends Feed<IBrowseResponse> {
header: PageHeader | null;
sections;
@@ -32,7 +32,7 @@ class Library extends Feed<IBrowseResponse> {
}));
}
async #getAll(shelf: Shelf): Promise<Playlist | History | Feed> {
async #getAll(shelf: Shelf): Promise<Playlist | History | Feed<IBrowseResponse>> {
if (!shelf.menu?.as(Menu).hasKey('top_level_buttons'))
throw new InnertubeError(`The ${shelf.title.text} shelf doesn't have more items`);
@@ -75,6 +75,4 @@ class Library extends Feed<IBrowseResponse> {
get clips() {
return this.sections.find((section) => section.type === 'CONTENT_CUT');
}
}
export default Library;
}

View File

@@ -42,7 +42,7 @@ export type ChatAction =
ReplaceChatItemAction | ReplayChatItemAction | ShowLiveChatActionPanelAction | ShowLiveChatTooltipCommand;
export type ChatItemWithMenu = LiveChatAutoModMessage | LiveChatMembershipItem | LiveChatPaidMessage | LiveChatPaidSticker | LiveChatTextMessage | LiveChatViewerEngagementMessage;
export interface LiveMetadata {
title?: UpdateTitleAction;
description?: UpdateDescriptionAction;
@@ -51,7 +51,7 @@ export interface LiveMetadata {
date?: UpdateDateTextAction;
}
class LiveChat extends EventEmitter {
export default class LiveChat extends EventEmitter {
smoothed_queue: SmoothedQueue;
#actions: Actions;
@@ -237,7 +237,7 @@ class LiveChat extends EventEmitter {
if (this.running)
this.#pollMetadata();
} catch (err) {
} catch {
await this.#wait(2000);
if (this.running)
this.#pollMetadata();
@@ -309,6 +309,4 @@ class LiveChat extends EventEmitter {
async #wait(ms: number) {
return new Promise<void>((resolve) => setTimeout(() => resolve(), ms));
}
}
export default LiveChat;
}

View File

@@ -8,7 +8,7 @@ import Notification from '../classes/Notification.js';
import type { ApiResponse, Actions } from '../../core/index.js';
import type { IGetNotificationsMenuResponse } from '../types/index.js';
class NotificationsMenu {
export default class NotificationsMenu {
#page: IGetNotificationsMenuResponse;
#actions: Actions;
@@ -19,12 +19,15 @@ class NotificationsMenu {
this.#actions = actions;
this.#page = Parser.parseResponse<IGetNotificationsMenuResponse>(response.data);
if (!this.#page.actions_memo)
throw new InnertubeError('Page actions not found');
this.header = this.#page.actions_memo.getType(SimpleMenuHeader).first();
this.contents = this.#page.actions_memo.getType(Notification);
}
async getContinuation(): Promise<NotificationsMenu> {
const continuation = this.#page.actions_memo.getType(ContinuationItem).first();
const continuation = this.#page.actions_memo?.getType(ContinuationItem).first();
if (!continuation)
throw new InnertubeError('Continuation not found');
@@ -37,6 +40,4 @@ class NotificationsMenu {
get page(): IGetNotificationsMenuResponse {
return this.#page;
}
}
export default NotificationsMenu;
}

View File

@@ -8,13 +8,15 @@ import SearchSubMenu from '../classes/SearchSubMenu.js';
import SectionList from '../classes/SectionList.js';
import UniversalWatchCard from '../classes/UniversalWatchCard.js';
import { observe } from '../helpers.js';
import type { ApiResponse, Actions } from '../../core/index.js';
import type { ObservedArray, YTNode } from '../helpers.js';
import type { ISearchResponse } from '../types/index.js';
class Search extends Feed<ISearchResponse> {
export default class Search extends Feed<ISearchResponse> {
header?: SearchHeader;
results?: ObservedArray<YTNode> | null;
results: ObservedArray<YTNode>;
refinements: string[];
estimated_results: number;
sub_menu?: SearchSubMenu;
@@ -34,13 +36,16 @@ class Search extends Feed<ISearchResponse> {
if (this.page.header)
this.header = this.page.header.item().as(SearchHeader);
this.results = contents.find((content) => content.is(ItemSection) && content.contents && content.contents.length > 0)?.as(ItemSection).contents;
this.results = observe(contents.filterType(ItemSection).flatMap((section) => section.contents));
this.refinements = this.page.refinements || [];
this.estimated_results = this.page.estimated_results;
this.estimated_results = this.page.estimated_results || 0;
if (this.page.contents_memo) {
this.sub_menu = this.page.contents_memo.getType(SearchSubMenu).first();
this.watch_card = this.page.contents_memo.getType(UniversalWatchCard).first();
}
this.sub_menu = this.page.contents_memo?.getType(SearchSubMenu).first();
this.watch_card = this.page.contents_memo?.getType(UniversalWatchCard).first();
this.refinement_cards = this.results?.firstOfType(HorizontalCardList);
}
@@ -82,6 +87,4 @@ class Search extends Feed<ISearchResponse> {
throw new InnertubeError('Could not get continuation data');
return new Search(this.actions, response, true);
}
}
export default Search;
}

View File

@@ -17,7 +17,7 @@ import TwoColumnBrowseResults from '../classes/TwoColumnBrowseResults.js';
import type { ApiResponse, Actions } from '../../core/index.js';
import type { IBrowseResponse } from '../types/index.js';
class Settings {
export default class Settings {
#page: IBrowseResponse;
#actions: Actions;
@@ -132,6 +132,4 @@ class Settings {
get page(): IBrowseResponse {
return this.#page;
}
}
export default Settings;
}

View File

@@ -43,10 +43,10 @@ class DelayQueue {
}
}
class SmoothedQueue {
export default class SmoothedQueue {
#last_update_time: number | null;
#estimated_update_interval: number | null;
#callback: Function | null;
#callback: ((actions: YTNode[]) => void) | null;
#action_queue: YTNode[][];
#next_update_id: any;
#poll_response_delay_queue: DelayQueue;
@@ -107,12 +107,14 @@ class SmoothedQueue {
}
if (this.#action_queue !== null) {
delay == 1 ? (
delay = this.#estimated_update_interval as number / this.#action_queue.length,
delay *= Math.random() + 0.5,
delay = Math.min(1E3, delay),
delay = Math.max(80, delay)
) : delay = 80;
if (delay == 1) {
delay = this.#estimated_update_interval as number / this.#action_queue.length;
delay *= Math.random() + 0.5;
delay = Math.min(1E3, delay);
delay = Math.max(80, delay);
} else {
delay = 80;
}
this.#next_update_id = setTimeout(this.emitSmoothedActions.bind(this), delay);
}
@@ -127,11 +129,11 @@ class SmoothedQueue {
this.#action_queue = [];
}
set callback(cb: Function | null) {
set callback(cb: ((actions: YTNode[]) => void) | null) {
this.#callback = cb;
}
get callback(): Function | null {
get callback(): ((actions: YTNode[]) => void) | null {
return this.#callback;
}
@@ -154,6 +156,4 @@ class SmoothedQueue {
get poll_response_delay_queue(): DelayQueue {
return this.#poll_response_delay_queue;
}
}
export default SmoothedQueue;
}

View File

@@ -8,7 +8,7 @@ import type { ApiResponse } from '../../core/index.js';
import type { ObservedArray } from '../helpers.js';
import type { IBrowseResponse } from '../types/index.js';
class TimeWatched {
export default class TimeWatched {
#page: IBrowseResponse;
contents?: ObservedArray<ItemSection>;
@@ -29,6 +29,4 @@ class TimeWatched {
get page(): IBrowseResponse {
return this.#page;
}
}
export default TimeWatched;
}

View File

@@ -12,6 +12,10 @@ export default class TranscriptInfo {
constructor(actions: Actions, response: ApiResponse) {
this.#page = Parser.parseResponse(response.data);
this.#actions = actions;
if (!this.#page.actions_memo)
throw new Error('Page actions not found');
this.transcript = this.#page.actions_memo.getType(Transcript).first();
}

View File

@@ -8,7 +8,6 @@ import ContinuationItem from '../classes/ContinuationItem.js';
import ItemSection from '../classes/ItemSection.js';
import LiveChat from '../classes/LiveChat.js';
import MerchandiseShelf from '../classes/MerchandiseShelf.js';
import MicroformatData from '../classes/MicroformatData.js';
import PlayerMicroformat from '../classes/PlayerMicroformat.js';
import PlayerOverlay from '../classes/PlayerOverlay.js';
import RelatedChipCloud from '../classes/RelatedChipCloud.js';
@@ -27,26 +26,11 @@ import VideoDescriptionMusicSection from '../classes/VideoDescriptionMusicSectio
import LiveChatWrap from './LiveChat.js';
import type { RawNode } from '../index.js';
import type CardCollection from '../classes/CardCollection.js';
import type Endscreen from '../classes/Endscreen.js';
import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.js';
import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js';
import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.js';
import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.js';
import type { ApiResponse, Actions } from '../../core/index.js';
import type { ObservedArray, YTNode } from '../helpers.js';
class VideoInfo extends MediaInfo {
export default class VideoInfo extends MediaInfo {
#watch_next_continuation?: ContinuationItem;
basic_info;
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
endscreen?: Endscreen;
captions?: PlayerCaptionsTracklist;
cards?: CardCollection;
primary_info?: VideoPrimaryInfo | null;
secondary_info?: VideoSecondaryInfo | null;
playlist?;
@@ -70,37 +54,6 @@ class VideoInfo extends MediaInfo {
const [ info, next ] = this.page;
if (info.microformat && !info.microformat?.is(PlayerMicroformat, MicroformatData))
throw new InnertubeError('Invalid microformat', info.microformat);
this.basic_info = { // This type is inferred so no need for an explicit type
...info.video_details,
/**
* Microformat is a bit redundant, so only
* a few things there are interesting to us.
*/
...{
embed: info.microformat?.is(PlayerMicroformat) ? info.microformat?.embed : null,
channel: info.microformat?.is(PlayerMicroformat) ? info.microformat?.channel : null,
is_unlisted: info.microformat?.is_unlisted,
is_family_safe: info.microformat?.is_family_safe,
category: info.microformat?.is(PlayerMicroformat) ? info.microformat?.category : null,
has_ypc_metadata: info.microformat?.is(PlayerMicroformat) ? info.microformat?.has_ypc_metadata : null,
start_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.start_timestamp : null,
end_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.end_timestamp : null,
view_count: info.microformat?.is(PlayerMicroformat) && isNaN(info.video_details?.view_count as number) ? info.microformat.view_count : info.video_details?.view_count
},
like_count: undefined as number | undefined,
is_liked: undefined as boolean | undefined,
is_disliked: undefined as boolean | undefined
};
this.annotations = info.annotations;
this.storyboards = info.storyboards;
this.endscreen = info.endscreen;
this.captions = info.captions;
this.cards = info.cards;
if (this.streaming_data) {
const default_audio_track = this.streaming_data.adaptive_formats.find((format) => format.audio_track?.audio_is_default);
if (default_audio_track) {
@@ -398,7 +351,7 @@ class VideoInfo extends MediaInfo {
* @returns `VideoInfo` for the trailer, or `null` if none.
*/
getTrailerInfo(): VideoInfo | null {
if (this.has_trailer) {
if (this.has_trailer && this.playability_status) {
const player_response = this.playability_status.error_screen?.as(PlayerLegacyDesktopYpcTrailer).trailer?.player_response;
if (player_response) {
return new VideoInfo([ { data: player_response } as ApiResponse ], this.actions, this.cpn);
@@ -432,7 +385,7 @@ class VideoInfo extends MediaInfo {
* Checks if trailer is available.
*/
get has_trailer(): boolean {
return !!this.playability_status.error_screen?.is(PlayerLegacyDesktopYpcTrailer);
return !!this.playability_status?.error_screen?.is(PlayerLegacyDesktopYpcTrailer);
}
/**
@@ -487,6 +440,4 @@ class VideoInfo extends MediaInfo {
}
return [];
}
}
export default VideoInfo;
}

View File

@@ -6,7 +6,7 @@ import { ItemSectionContinuation } from '../index.js';
import type { IBrowseResponse } from '../types/index.js';
import type { ApiResponse, Actions } from '../../core/index.js';
class Channel extends Feed<IBrowseResponse> {
export default class Channel extends Feed<IBrowseResponse> {
header?: C4TabbedHeader;
contents?: ItemSection | ItemSectionContinuation;
@@ -31,6 +31,4 @@ class Channel extends Feed<IBrowseResponse> {
get has_continuation(): boolean {
return !!this.contents?.continuation;
}
}
export default Channel;
}

View File

@@ -8,7 +8,7 @@ import KidsHomeScreen from '../classes/ytkids/KidsHomeScreen.js';
import type { ApiResponse, Actions } from '../../core/index.js';
import type { IBrowseResponse } from '../types/ParsedResponse.js';
class HomeFeed extends Feed<IBrowseResponse> {
export default class HomeFeed extends Feed<IBrowseResponse> {
header?: KidsCategoriesHeader;
contents?: KidsHomeScreen;
@@ -46,6 +46,4 @@ class HomeFeed extends Feed<IBrowseResponse> {
get categories(): string[] {
return this.header?.category_tabs.map((tab) => tab.title.toString()) || [];
}
}
export default HomeFeed;
}

View File

@@ -6,8 +6,8 @@ import type { ApiResponse, Actions } from '../../core/index.js';
import type { ObservedArray, YTNode } from '../helpers.js';
import type { ISearchResponse } from '../types/index.js';
class Search extends Feed<ISearchResponse> {
estimated_results: number;
export default class Search extends Feed<ISearchResponse> {
estimated_results?: number;
contents: ObservedArray<YTNode> | null;
constructor(actions: Actions, data: ApiResponse | ISearchResponse) {
@@ -21,6 +21,4 @@ class Search extends Feed<ISearchResponse> {
this.contents = item_section.contents;
}
}
export default Search;
}

View File

@@ -8,10 +8,7 @@ import type { ApiResponse, Actions } from '../../core/index.js';
import type { ObservedArray, YTNode } from '../helpers.js';
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
class VideoInfo extends MediaInfo {
basic_info;
captions;
export default class VideoInfo extends MediaInfo {
slim_video_metadata?: SlimVideoMetadata;
watch_next_feed?: ObservedArray<YTNode>;
current_video_endpoint?: NavigationEndpoint;
@@ -20,11 +17,7 @@ class VideoInfo extends MediaInfo {
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
super(data, actions, cpn);
const [ info, next ] = this.page;
this.basic_info = info.video_details;
this.captions = info.captions;
const next = this.page[1];
const two_col = next?.contents?.item().as(TwoColumnWatchNextResults);
@@ -38,13 +31,4 @@ class VideoInfo extends MediaInfo {
this.player_overlays = next?.player_overlays?.item().as(PlayerOverlay);
}
}
/**
* Adds video to the watch history.
*/
async addToWatchHistory(): Promise<Response> {
return super.addToWatchHistory();
}
}
export default VideoInfo;
}

View File

@@ -3,38 +3,38 @@ import { Parser } from '../index.js';
import MicroformatData from '../classes/MicroformatData.js';
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
import MusicDetailHeader from '../classes/MusicDetailHeader.js';
import MusicResponsiveHeader from '../classes/MusicResponsiveHeader.js';
import MusicShelf from '../classes/MusicShelf.js';
import type MusicThumbnail from '../classes/MusicThumbnail.js';
import type { ApiResponse } from '../../core/index.js';
import type { ObservedArray } from '../helpers.js';
import { observe, type ObservedArray } from '../helpers.js';
import type { IBrowseResponse } from '../types/index.js';
import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
class Album {
export default class Album {
#page: IBrowseResponse;
header?: MusicDetailHeader;
header?: MusicDetailHeader | MusicResponsiveHeader;
contents: ObservedArray<MusicResponsiveListItem>;
sections: ObservedArray<MusicCarouselShelf>;
url: string | null;
background?: MusicThumbnail;
url?: string;
constructor(response: ApiResponse) {
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
this.header = this.#page.header?.item().as(MusicDetailHeader);
this.url = this.#page.microformat?.as(MicroformatData).url_canonical || null;
if (!this.#page.contents_memo)
throw new Error('No contents found in the response');
this.contents = this.#page.contents_memo.getType(MusicShelf)?.first().contents;
this.sections = this.#page.contents_memo.getType(MusicCarouselShelf) || [];
this.header = this.#page.contents_memo.getType(MusicDetailHeader, MusicResponsiveHeader)?.first();
this.contents = this.#page.contents_memo.getType(MusicShelf)?.first().contents || observe([]);
this.sections = this.#page.contents_memo.getType(MusicCarouselShelf) || observe([]);
this.background = this.#page.background;
this.url = this.#page.microformat?.as(MicroformatData).url_canonical;
}
get page(): IBrowseResponse {
return this.#page;
}
}
export default Album;
}

View File

@@ -13,7 +13,7 @@ import type { ApiResponse, Actions } from '../../core/index.js';
import type { IBrowseResponse } from '../types/ParsedResponse.js';
import type { ObservedArray } from '../helpers.js';
class Artist {
export default class Artist {
#page: IBrowseResponse;
#actions: Actions;
@@ -55,6 +55,4 @@ class Artist {
get page(): IBrowseResponse {
return this.#page;
}
}
export default Artist;
}

View File

@@ -11,7 +11,7 @@ import type { ApiResponse } from '../../core/index.js';
import type { ObservedArray } from '../helpers.js';
import type { IBrowseResponse } from '../types/index.js';
class Explore {
export default class Explore {
#page: IBrowseResponse;
top_buttons: MusicNavigationButton[];
@@ -37,6 +37,4 @@ class Explore {
get page(): IBrowseResponse {
return this.#page;
}
}
export default Explore;
}

View File

@@ -11,7 +11,7 @@ import type { ApiResponse, Actions } from '../../core/index.js';
import type { ObservedArray } from '../helpers.js';
import type { IBrowseResponse } from '../types/index.js';
class HomeFeed {
export default class HomeFeed {
#page: IBrowseResponse;
#actions: Actions;
#continuation?: string;
@@ -92,6 +92,4 @@ class HomeFeed {
get page(): IBrowseResponse {
return this.#page;
}
}
export default HomeFeed;
}

View File

@@ -17,7 +17,7 @@ import type { IBrowseResponse } from '../types/index.js';
import type MusicMenuItemDivider from '../classes/menus/MusicMenuItemDivider.js';
import type { ApiResponse, Actions } from '../../core/index.js';
class Library {
export default class Library {
#page: IBrowseResponse;
#actions: Actions;
#continuation?: string | null;
@@ -147,7 +147,7 @@ class Library {
}
}
class LibraryContinuation {
export class LibraryContinuation {
#page;
#actions;
#continuation;
@@ -185,7 +185,4 @@ class LibraryContinuation {
get page(): IBrowseResponse {
return this.#page;
}
}
export { LibraryContinuation };
export default Library;
}

View File

@@ -6,22 +6,25 @@ import MusicEditablePlaylistDetailHeader from '../classes/MusicEditablePlaylistD
import MusicPlaylistShelf from '../classes/MusicPlaylistShelf.js';
import MusicShelf from '../classes/MusicShelf.js';
import SectionList from '../classes/SectionList.js';
import MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
import MusicResponsiveHeader from '../classes/MusicResponsiveHeader.js';
import { InnertubeError } from '../../utils/Utils.js';
import type { ObservedArray, YTNode } from '../helpers.js';
import { observe, type ObservedArray } from '../helpers.js';
import type { ApiResponse, Actions } from '../../core/index.js';
import type { IBrowseResponse } from '../types/index.js';
import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
import type MusicThumbnail from '../classes/MusicThumbnail.js';
class Playlist {
export default class Playlist {
#page: IBrowseResponse;
#actions: Actions;
#continuation: string | null;
#last_fetched_suggestions: any;
#suggestions_continuation: any;
#last_fetched_suggestions: ObservedArray<MusicResponsiveListItem> | null;
#suggestions_continuation: string | null;
header?: MusicDetailHeader;
items?: ObservedArray<YTNode> | null;
header?: MusicResponsiveHeader | MusicDetailHeader | MusicEditablePlaylistDetailHeader;
contents?: ObservedArray<MusicResponsiveListItem>;
background?: MusicThumbnail;
constructor(response: ApiResponse, actions: Actions) {
this.#actions = actions;
@@ -32,16 +35,17 @@ class Playlist {
if (this.#page.continuation_contents) {
const data = this.#page.continuation_contents?.as(MusicPlaylistShelfContinuation);
this.items = data.contents;
if (!data.contents)
throw new InnertubeError('No contents found in the response');
this.contents = data.contents.as(MusicResponsiveListItem);
this.#continuation = data.continuation;
} else {
if (this.#page.header?.item().type === 'MusicEditablePlaylistDetailHeader') {
this.header = this.#page.header?.item().as(MusicEditablePlaylistDetailHeader).header?.as(MusicDetailHeader);
} else {
this.header = this.#page.header?.item().as(MusicDetailHeader);
}
this.items = this.#page.contents_memo?.getType(MusicPlaylistShelf).first().contents || null;
this.#continuation = this.#page.contents_memo?.getType(MusicPlaylistShelf).first().continuation || null;
if (!this.#page.contents_memo)
throw new InnertubeError('No contents found in the response');
this.header = this.#page.contents_memo.getType(MusicResponsiveHeader, MusicEditablePlaylistDetailHeader, MusicDetailHeader)?.first();
this.contents = this.#page.contents_memo.getType(MusicPlaylistShelf)?.first()?.contents || observe([]);
this.background = this.#page.background;
this.#continuation = this.#page.contents_memo.getType(MusicPlaylistShelf)?.first()?.continuation || null;
}
}
@@ -64,7 +68,12 @@ class Playlist {
* Retrieves related playlists
*/
async getRelated(): Promise<MusicCarouselShelf> {
let section_continuation = this.#page.contents_memo?.getType(SectionList)?.[0].continuation;
const target_section_list = this.#page.contents_memo?.getType(SectionList).find((section_list) => section_list.continuation);
if (!target_section_list)
throw new InnertubeError('Could not find "Related" section.');
let section_continuation = target_section_list.continuation;
while (section_continuation) {
const data = await this.#actions.execute('/browse', {
@@ -76,7 +85,7 @@ class Playlist {
const section_list = data.continuation_contents?.as(SectionListContinuation);
const sections = section_list?.contents?.as(MusicCarouselShelf, MusicShelf);
const related = sections?.matchCondition((section) => section.is(MusicCarouselShelf))?.as(MusicCarouselShelf);
const related = sections?.find((section) => section.is(MusicCarouselShelf))?.as(MusicCarouselShelf);
if (related)
return related;
@@ -84,10 +93,10 @@ class Playlist {
section_continuation = section_list?.continuation;
}
throw new InnertubeError('Target section not found.');
throw new InnertubeError('Could not fetch related playlists.');
}
async getSuggestions(refresh = true) {
async getSuggestions(refresh = true): Promise<ObservedArray<MusicResponsiveListItem>> {
const require_fetch = refresh || !this.#last_fetched_suggestions;
const fetch_promise = require_fetch ? this.#fetchSuggestions() : Promise.resolve(null);
const fetch_result = await fetch_promise;
@@ -97,11 +106,12 @@ class Playlist {
this.#suggestions_continuation = fetch_result.continuation;
}
return fetch_result?.items || this.#last_fetched_suggestions;
return fetch_result?.items || this.#last_fetched_suggestions || observe([]);
}
async #fetchSuggestions(): Promise<{ items: never[] | ObservedArray<MusicResponsiveListItem>, continuation: string | null }> {
const continuation = this.#suggestions_continuation || this.#page.contents_memo?.get('SectionList')?.[0].as(SectionList).continuation;
async #fetchSuggestions(): Promise<{ items: ObservedArray<MusicResponsiveListItem>, continuation: string | null }> {
const target_section_list = this.#page.contents_memo?.getType(SectionList).find((section_list) => section_list.continuation);
const continuation = this.#suggestions_continuation || target_section_list?.continuation;
if (continuation) {
const page = await this.#actions.execute('/browse', {
@@ -113,16 +123,16 @@ class Playlist {
const section_list = page.continuation_contents?.as(SectionListContinuation);
const sections = section_list?.contents?.as(MusicCarouselShelf, MusicShelf);
const suggestions = sections?.matchCondition((section) => section.is(MusicShelf))?.as(MusicShelf);
const suggestions = sections?.find((section) => section.is(MusicShelf))?.as(MusicShelf);
return {
items: suggestions?.contents || [],
items: suggestions?.contents || observe([]),
continuation: suggestions?.continuation || null
};
}
return {
items: [],
items: observe([]),
continuation: null
};
}
@@ -131,9 +141,11 @@ class Playlist {
return this.#page;
}
get items(): ObservedArray<MusicResponsiveListItem> {
return this.contents || observe([]);
}
get has_continuation(): boolean {
return !!this.#continuation;
}
}
export default Playlist;
}

View File

@@ -16,7 +16,7 @@ import type { ObservedArray } from '../helpers.js';
import type { IBrowseResponse } from '../types/index.js';
import type { ApiResponse, Actions } from '../../core/index.js';
class Recap {
export default class Recap {
#page: IBrowseResponse;
#actions: Actions;
@@ -60,6 +60,4 @@ class Recap {
get page(): IBrowseResponse {
return this.#page;
}
}
export default Recap;
}

View File

@@ -5,7 +5,6 @@ import { MediaInfo } from '../../core/mixins/index.js';
import Tab from '../classes/Tab.js';
import AutomixPreviewVideo from '../classes/AutomixPreviewVideo.js';
import Message from '../classes/Message.js';
import MicroformatData from '../classes/MicroformatData.js';
import MusicDescriptionShelf from '../classes/MusicDescriptionShelf.js';
import PlayerOverlay from '../classes/PlayerOverlay.js';
import PlaylistPanel from '../classes/PlaylistPanel.js';
@@ -14,19 +13,12 @@ import WatchNextTabbedResults from '../classes/WatchNextTabbedResults.js';
import type RichGrid from '../classes/RichGrid.js';
import type MusicQueue from '../classes/MusicQueue.js';
import type Endscreen from '../classes/Endscreen.js';
import type MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.js';
import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.js';
import type { ObservedArray, YTNode } from '../helpers.js';
import type { ApiResponse, Actions } from '../../core/index.js';
class TrackInfo extends MediaInfo {
basic_info;
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
endscreen?: Endscreen;
tabs?: ObservedArray<Tab>;
current_video_endpoint?: NavigationEndpoint;
player_overlays?: PlayerOverlay;
@@ -34,24 +26,7 @@ class TrackInfo extends MediaInfo {
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
super(data, actions, cpn);
const [ info, next ] = this.page;
if (!info.microformat?.is(MicroformatData))
throw new InnertubeError('Invalid microformat', info.microformat);
this.basic_info = {
...info.video_details,
...{
description: info.microformat?.description,
is_unlisted: info.microformat?.is_unlisted,
is_family_safe: info.microformat?.is_family_safe,
url_canonical: info.microformat?.url_canonical,
tags: info.microformat?.tags
}
};
this.storyboards = info.storyboards;
this.endscreen = info.endscreen;
const next = this.page[1];
if (next) {
const tabbed_results = next.contents_memo?.getType(WatchNextTabbedResults)?.[0];

View File

@@ -0,0 +1,55 @@
import { Parser, ContinuationCommand } from '../index.js';
import { InnertubeError } from '../../utils/Utils.js';
import { Reel } from '../../core/endpoints/index.js';
import MediaInfo from '../../core/mixins/MediaInfo.js';
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
import type PlayerOverlay from '../classes/PlayerOverlay.js';
import type { ApiResponse, Actions } from '../../core/index.js';
export default class ShortFormVideoInfo extends MediaInfo {
#watch_next_continuation?: ContinuationCommand;
watch_next_feed?: NavigationEndpoint[];
current_video_endpoint?: NavigationEndpoint;
player_overlays?: PlayerOverlay;
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string, reel_watch_sequence_response: ApiResponse) {
super(data, actions, cpn);
if (reel_watch_sequence_response) {
const reel_watch_sequence = Parser.parseResponse(reel_watch_sequence_response.data);
if (reel_watch_sequence.entries)
this.watch_next_feed = reel_watch_sequence.entries;
if (reel_watch_sequence.continuation_endpoint)
this.#watch_next_continuation = reel_watch_sequence.continuation_endpoint?.as(ContinuationCommand);
}
}
async getWatchNextContinuation(): Promise<ShortFormVideoInfo> {
if (!this.#watch_next_continuation)
throw new InnertubeError('Continuation not found');
const response = await this.actions.execute(
Reel.ReelWatchSequenceEndpoint.PATH, {
...Reel.ReelWatchSequenceEndpoint.build({
sequence_params: this.#watch_next_continuation.token
}),
parse: true
}
);
if (response.entries)
this.watch_next_feed = response.entries;
this.#watch_next_continuation = response.continuation_endpoint?.as(ContinuationCommand);
return this;
}
/**
* Checks if continuation is available for the watch next feed.
*/
get wn_has_continuation(): boolean {
return !!this.#watch_next_continuation;
}
}

View File

@@ -1,56 +0,0 @@
import { Parser, ContinuationCommand } from '../index.js';
import { InnertubeError } from '../../utils/Utils.js';
import { Reel } from '../../core/endpoints/index.js';
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
import type PlayerOverlay from '../classes/PlayerOverlay.js';
import type { ApiResponse, Actions } from '../../core/index.js';
import type { ObservedArray, YTNode } from '../helpers.js';
class VideoInfo {
#watch_next_continuation?: ContinuationCommand;
#actions: Actions;
basic_info;
watch_next_feed?: ObservedArray<YTNode>;
current_video_endpoint?: NavigationEndpoint;
player_overlays?: PlayerOverlay;
constructor(data: [ApiResponse, ApiResponse], actions: Actions) {
this.#actions = actions;
const info = Parser.parseResponse(data[0].data);
const watch_next = Parser.parseResponse(data[1].data);
this.basic_info = info.video_details;
this.watch_next_feed = watch_next.entries?.array();
this.#watch_next_continuation = watch_next.continuation_endpoint?.as(ContinuationCommand);
}
/**
* Retrieves watch next feed continuation.
*/
async getWatchNextContinuation(): Promise<VideoInfo> {
if (!this.#watch_next_continuation)
throw new InnertubeError('Watch next feed continuation not found');
const response = await this.#actions.execute(Reel.WatchSequenceEndpoint.PATH, Reel.WatchSequenceEndpoint.build({
sequenceParams: this.#watch_next_continuation.token
}));
if (!response.success) {
throw new InnertubeError('Continue failed ', response.status_code);
}
const parsed = Parser.parseResponse(response.data);
this.watch_next_feed = parsed.entries?.array();
this.#watch_next_continuation = parsed.continuation_endpoint?.as(ContinuationCommand);
return this;
}
}
export default VideoInfo;

View File

@@ -1 +1 @@
export { default as VideoInfo } from './VideoInfo.js';
export { default as ShortFormVideoInfo } from './ShortFormVideoInfo.js';

View File

@@ -7,13 +7,13 @@ const TAG = 'JsRuntime';
export default function evaluate(code: string, env: Record<string, VMPrimative>) {
Log.debug(TAG, 'Evaluating JavaScript:\n', code);
const runtime = new Jinter(code);
const runtime = new Jinter();
for (const [ key, value ] of Object.entries(env)) {
runtime.scope.set(key, value);
}
const result = runtime.interpret();
const result = runtime.evaluate(code);
Log.debug(TAG, 'Done. Result:', result);

View File

@@ -21,6 +21,7 @@ export declare namespace $.youtube.GetCommentsSectionParams.Params {
videoId: string;
sortBy: number;
type: number;
commentId?: string;
}
}
@@ -31,6 +32,7 @@ export function getDefaultValue(): $.youtube.GetCommentsSectionParams.Params.Opt
videoId: "",
sortBy: 0,
type: 0,
commentId: undefined,
};
}
@@ -46,6 +48,7 @@ export function encodeJson(value: $.youtube.GetCommentsSectionParams.Params.Opti
if (value.videoId !== undefined) result.videoId = tsValueToJsonValueFns.string(value.videoId);
if (value.sortBy !== undefined) result.sortBy = tsValueToJsonValueFns.int32(value.sortBy);
if (value.type !== undefined) result.type = tsValueToJsonValueFns.int32(value.type);
if (value.commentId !== undefined) result.commentId = tsValueToJsonValueFns.string(value.commentId);
return result;
}
@@ -54,6 +57,7 @@ export function decodeJson(value: any): $.youtube.GetCommentsSectionParams.Param
if (value.videoId !== undefined) result.videoId = jsonValueToTsValueFns.string(value.videoId);
if (value.sortBy !== undefined) result.sortBy = jsonValueToTsValueFns.int32(value.sortBy);
if (value.type !== undefined) result.type = jsonValueToTsValueFns.int32(value.type);
if (value.commentId !== undefined) result.commentId = jsonValueToTsValueFns.string(value.commentId);
return result;
}
@@ -77,6 +81,12 @@ export function encodeBinary(value: $.youtube.GetCommentsSectionParams.Params.Op
[15, tsValueToWireValueFns.int32(tsValue)],
);
}
if (value.commentId !== undefined) {
const tsValue = value.commentId;
result.push(
[16, tsValueToWireValueFns.string(tsValue)],
);
}
return serialize(result);
}
@@ -105,5 +115,12 @@ export function decodeBinary(binary: Uint8Array): $.youtube.GetCommentsSectionPa
if (value === undefined) break field;
result.type = value;
}
field: {
const wireValue = wireFields.get(16);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.commentId = value;
}
return result;
}

View File

@@ -18,9 +18,17 @@ import {
export declare namespace $.youtube.InnertubePayload.Context {
export type Client = {
unkparam: number;
deviceMake: string;
deviceModel: string;
nameId: number;
clientVersion: string;
clientName: string;
osName: string;
osVersion: string;
acceptLanguage: string;
acceptRegion: string;
androidSdkVersion: number;
windowWidthPoints: number;
windowHeightPoints: number;
}
}
@@ -28,9 +36,17 @@ export type Type = $.youtube.InnertubePayload.Context.Client;
export function getDefaultValue(): $.youtube.InnertubePayload.Context.Client {
return {
unkparam: 0,
deviceMake: "",
deviceModel: "",
nameId: 0,
clientVersion: "",
clientName: "",
osName: "",
osVersion: "",
acceptLanguage: "",
acceptRegion: "",
androidSdkVersion: 0,
windowWidthPoints: 0,
windowHeightPoints: 0,
};
}
@@ -43,24 +59,52 @@ export function createValue(partialValue: Partial<$.youtube.InnertubePayload.Con
export function encodeJson(value: $.youtube.InnertubePayload.Context.Client): unknown {
const result: any = {};
if (value.unkparam !== undefined) result.unkparam = tsValueToJsonValueFns.int32(value.unkparam);
if (value.deviceMake !== undefined) result.deviceMake = tsValueToJsonValueFns.string(value.deviceMake);
if (value.deviceModel !== undefined) result.deviceModel = tsValueToJsonValueFns.string(value.deviceModel);
if (value.nameId !== undefined) result.nameId = tsValueToJsonValueFns.int32(value.nameId);
if (value.clientVersion !== undefined) result.clientVersion = tsValueToJsonValueFns.string(value.clientVersion);
if (value.clientName !== undefined) result.clientName = tsValueToJsonValueFns.string(value.clientName);
if (value.osName !== undefined) result.osName = tsValueToJsonValueFns.string(value.osName);
if (value.osVersion !== undefined) result.osVersion = tsValueToJsonValueFns.string(value.osVersion);
if (value.acceptLanguage !== undefined) result.acceptLanguage = tsValueToJsonValueFns.string(value.acceptLanguage);
if (value.acceptRegion !== undefined) result.acceptRegion = tsValueToJsonValueFns.string(value.acceptRegion);
if (value.androidSdkVersion !== undefined) result.androidSdkVersion = tsValueToJsonValueFns.int32(value.androidSdkVersion);
if (value.windowWidthPoints !== undefined) result.windowWidthPoints = tsValueToJsonValueFns.int32(value.windowWidthPoints);
if (value.windowHeightPoints !== undefined) result.windowHeightPoints = tsValueToJsonValueFns.int32(value.windowHeightPoints);
return result;
}
export function decodeJson(value: any): $.youtube.InnertubePayload.Context.Client {
const result = getDefaultValue();
if (value.unkparam !== undefined) result.unkparam = jsonValueToTsValueFns.int32(value.unkparam);
if (value.deviceMake !== undefined) result.deviceMake = jsonValueToTsValueFns.string(value.deviceMake);
if (value.deviceModel !== undefined) result.deviceModel = jsonValueToTsValueFns.string(value.deviceModel);
if (value.nameId !== undefined) result.nameId = jsonValueToTsValueFns.int32(value.nameId);
if (value.clientVersion !== undefined) result.clientVersion = jsonValueToTsValueFns.string(value.clientVersion);
if (value.clientName !== undefined) result.clientName = jsonValueToTsValueFns.string(value.clientName);
if (value.osName !== undefined) result.osName = jsonValueToTsValueFns.string(value.osName);
if (value.osVersion !== undefined) result.osVersion = jsonValueToTsValueFns.string(value.osVersion);
if (value.acceptLanguage !== undefined) result.acceptLanguage = jsonValueToTsValueFns.string(value.acceptLanguage);
if (value.acceptRegion !== undefined) result.acceptRegion = jsonValueToTsValueFns.string(value.acceptRegion);
if (value.androidSdkVersion !== undefined) result.androidSdkVersion = jsonValueToTsValueFns.int32(value.androidSdkVersion);
if (value.windowWidthPoints !== undefined) result.windowWidthPoints = jsonValueToTsValueFns.int32(value.windowWidthPoints);
if (value.windowHeightPoints !== undefined) result.windowHeightPoints = jsonValueToTsValueFns.int32(value.windowHeightPoints);
return result;
}
export function encodeBinary(value: $.youtube.InnertubePayload.Context.Client): Uint8Array {
const result: WireMessage = [];
if (value.unkparam !== undefined) {
const tsValue = value.unkparam;
if (value.deviceMake !== undefined) {
const tsValue = value.deviceMake;
result.push(
[12, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.deviceModel !== undefined) {
const tsValue = value.deviceModel;
result.push(
[13, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.nameId !== undefined) {
const tsValue = value.nameId;
result.push(
[16, tsValueToWireValueFns.int32(tsValue)],
);
@@ -71,12 +115,48 @@ export function encodeBinary(value: $.youtube.InnertubePayload.Context.Client):
[17, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.clientName !== undefined) {
const tsValue = value.clientName;
if (value.osName !== undefined) {
const tsValue = value.osName;
result.push(
[18, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.osVersion !== undefined) {
const tsValue = value.osVersion;
result.push(
[19, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.acceptLanguage !== undefined) {
const tsValue = value.acceptLanguage;
result.push(
[21, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.acceptRegion !== undefined) {
const tsValue = value.acceptRegion;
result.push(
[22, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.androidSdkVersion !== undefined) {
const tsValue = value.androidSdkVersion;
result.push(
[34, tsValueToWireValueFns.int32(tsValue)],
);
}
if (value.windowWidthPoints !== undefined) {
const tsValue = value.windowWidthPoints;
result.push(
[37, tsValueToWireValueFns.int32(tsValue)],
);
}
if (value.windowHeightPoints !== undefined) {
const tsValue = value.windowHeightPoints;
result.push(
[38, tsValueToWireValueFns.int32(tsValue)],
);
}
return serialize(result);
}
@@ -84,12 +164,26 @@ export function decodeBinary(binary: Uint8Array): $.youtube.InnertubePayload.Con
const result = getDefaultValue();
const wireMessage = deserialize(binary);
const wireFields = new Map(wireMessage);
field: {
const wireValue = wireFields.get(12);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.deviceMake = value;
}
field: {
const wireValue = wireFields.get(13);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.deviceModel = value;
}
field: {
const wireValue = wireFields.get(16);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.int32(wireValue);
if (value === undefined) break field;
result.unkparam = value;
result.nameId = value;
}
field: {
const wireValue = wireFields.get(17);
@@ -103,7 +197,49 @@ export function decodeBinary(binary: Uint8Array): $.youtube.InnertubePayload.Con
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.clientName = value;
result.osName = value;
}
field: {
const wireValue = wireFields.get(19);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.osVersion = value;
}
field: {
const wireValue = wireFields.get(21);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.acceptLanguage = value;
}
field: {
const wireValue = wireFields.get(22);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.acceptRegion = value;
}
field: {
const wireValue = wireFields.get(34);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.int32(wireValue);
if (value === undefined) break field;
result.androidSdkVersion = value;
}
field: {
const wireValue = wireFields.get(37);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.int32(wireValue);
if (value === undefined) break field;
result.windowWidthPoints = value;
}
field: {
const wireValue = wireFields.get(38);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.int32(wireValue);
if (value === undefined) break field;
result.windowHeightPoints = value;
}
return result;
}

View File

@@ -90,7 +90,7 @@ import {
export declare namespace $.youtube {
export type InnertubePayload = {
context?: Context;
target?: string;
videoId?: string;
title?: Title;
description?: Description;
tags?: Tags;
@@ -108,7 +108,7 @@ export type Type = $.youtube.InnertubePayload;
export function getDefaultValue(): $.youtube.InnertubePayload {
return {
context: undefined,
target: undefined,
videoId: undefined,
title: undefined,
description: undefined,
tags: undefined,
@@ -131,7 +131,7 @@ export function createValue(partialValue: Partial<$.youtube.InnertubePayload>):
export function encodeJson(value: $.youtube.InnertubePayload): unknown {
const result: any = {};
if (value.context !== undefined) result.context = encodeJson_1(value.context);
if (value.target !== undefined) result.target = tsValueToJsonValueFns.string(value.target);
if (value.videoId !== undefined) result.videoId = tsValueToJsonValueFns.string(value.videoId);
if (value.title !== undefined) result.title = encodeJson_2(value.title);
if (value.description !== undefined) result.description = encodeJson_3(value.description);
if (value.tags !== undefined) result.tags = encodeJson_4(value.tags);
@@ -147,7 +147,7 @@ export function encodeJson(value: $.youtube.InnertubePayload): unknown {
export function decodeJson(value: any): $.youtube.InnertubePayload {
const result = getDefaultValue();
if (value.context !== undefined) result.context = decodeJson_1(value.context);
if (value.target !== undefined) result.target = jsonValueToTsValueFns.string(value.target);
if (value.videoId !== undefined) result.videoId = jsonValueToTsValueFns.string(value.videoId);
if (value.title !== undefined) result.title = decodeJson_2(value.title);
if (value.description !== undefined) result.description = decodeJson_3(value.description);
if (value.tags !== undefined) result.tags = decodeJson_4(value.tags);
@@ -168,8 +168,8 @@ export function encodeBinary(value: $.youtube.InnertubePayload): Uint8Array {
[1, { type: WireType.LengthDelimited as const, value: encodeBinary_1(tsValue) }],
);
}
if (value.target !== undefined) {
const tsValue = value.target;
if (value.videoId !== undefined) {
const tsValue = value.videoId;
result.push(
[2, tsValueToWireValueFns.string(tsValue)],
);
@@ -247,7 +247,7 @@ export function decodeBinary(binary: Uint8Array): $.youtube.InnertubePayload {
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.target = value;
result.videoId = value;
}
field: {
const wireValue = wireFields.get(3);

View File

@@ -155,7 +155,8 @@ export function encodeMessageParams(channel_id: string, video_id: string): strin
export function encodeCommentsSectionParams(video_id: string, options: {
type?: number,
sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'
sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST',
comment_id?: string
} = {}): string {
const sort_options = {
TOP_COMMENTS: 0,
@@ -171,7 +172,8 @@ export function encodeCommentsSectionParams(video_id: string, options: {
opts: {
videoId: video_id,
sortBy: sort_options[options.sort_by || 'TOP_COMMENTS'],
type: options.type || 2
type: options.type || 2,
commentId: options.comment_id || ''
},
target: 'comments-section'
}
@@ -208,7 +210,10 @@ export function encodeCommentActionParams(type: number, args: {
if (args.hasOwnProperty('text')) {
if (typeof args.target_language !== 'string')
throw new Error('target_language must be a string');
args.comment_id && (delete data.unkNum);
if (args.comment_id)
delete data.unkNum;
data.translateCommentParams = {
params: {
comment: {
@@ -240,12 +245,20 @@ export function encodeVideoMetadataPayload(video_id: string, metadata: UpdateVid
const data: InnertubePayload.Type = {
context: {
client: {
unkparam: 14,
clientName: CLIENTS.ANDROID.NAME,
clientVersion: CLIENTS.YTSTUDIO_ANDROID.VERSION
nameId: 3,
osName: 'Android',
androidSdkVersion: CLIENTS.ANDROID.SDK_VERSION,
osVersion: '13',
acceptLanguage: 'en-US',
acceptRegion: 'US',
deviceMake: 'Google',
deviceModel: 'sdk_gphone64_x86_64',
windowHeightPoints: 840,
windowWidthPoints: 432,
clientVersion: CLIENTS.ANDROID.VERSION
}
},
target: video_id
videoId: video_id
};
if (Reflect.has(metadata, 'title'))
@@ -302,12 +315,20 @@ export function encodeCustomThumbnailPayload(video_id: string, bytes: Uint8Array
const data: InnertubePayload.Type = {
context: {
client: {
unkparam: 14,
clientName: CLIENTS.ANDROID.NAME,
clientVersion: CLIENTS.YTSTUDIO_ANDROID.VERSION
nameId: 3,
osName: 'Android',
androidSdkVersion: CLIENTS.ANDROID.SDK_VERSION,
osVersion: '13',
acceptLanguage: 'en-US',
acceptRegion: 'US',
deviceMake: 'Google',
deviceModel: 'sdk_gphone64_x86_64',
windowHeightPoints: 840,
windowWidthPoints: 432,
clientVersion: CLIENTS.ANDROID.VERSION
}
},
target: video_id,
videoId: video_id,
videoThumbnail: {
type: 3,
thumbnail: {

View File

@@ -11,17 +11,24 @@ message VisitorData {
message InnertubePayload {
message Context {
message Client {
required int32 unkparam = 16;
required string client_version = 17;
required string client_name = 18;
string deviceMake = 12;
string deviceModel = 13;
int32 nameId = 16;
string clientVersion = 17;
string osName = 18;
string osVersion = 19;
string acceptLanguage = 21;
string acceptRegion = 22;
int32 windowWidthPoints = 37;
int32 windowHeightPoints = 38;
int32 androidSdkVersion = 34;
}
required Client client = 1;
}
required Context context = 1;
// This can be either a target id or a video id.
optional string target = 2;
optional string videoId = 2;
/**** YT Sudio stuff ****/
@@ -155,6 +162,7 @@ message GetCommentsSectionParams {
required string video_id = 4;
required int32 sort_by = 6;
required int32 type = 15;
optional string comment_id = 16;
}
message RepliesOptions {

View File

@@ -1,23 +0,0 @@
// Studio.ts
export type UpdateVideoMetadataOptions = Partial<{
title: string;
description: string;
tags: string[];
category: number;
license: string;
age_restricted: boolean;
made_for_kids: boolean;
privacy: 'PUBLIC' | 'PRIVATE' | 'UNLISTED';
}>;
export type UploadedVideoMetadataOptions = Partial<{
title: string;
description: string;
privacy: 'PUBLIC' | 'PRIVATE' | 'UNLISTED';
is_draft: boolean;
}>;
// Music.ts
export type MusicSearchFilters = Partial<{
type: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist';
}>;

View File

@@ -1,4 +1,4 @@
import type { InnerTubeClient } from '../Innertube.js';
import type { InnerTubeClient } from '../types/index.js';
export type SnakeToCamel<S extends string> = S extends `${infer T}_${infer U}` ? `${Lowercase<T>}${Capitalize<SnakeToCamel<U>>}` : S;
@@ -29,6 +29,9 @@ export interface IPlayerRequest {
playlistId?: string;
params?: string;
client?: InnerTubeClient;
serviceIntegrityDimensions?: {
poToken: string
}
}
export type PlayerEndpointOptions = {
@@ -52,6 +55,10 @@ export type PlayerEndpointOptions = {
* Additional protobuf parameters.
*/
params?: string;
/**
* Token for serviceIntegrityDimensions
*/
po_token?: string;
}
export type NextEndpointOptions = {
@@ -301,7 +308,7 @@ export type CreateVideoEndpointOptions = {
client?: InnerTubeClient;
}
export type ICreateVideoRequest = ObjectSnakeToCamel<CreateVideoEndpointOptions>;
export type ICreateVideoRequest = Omit<ObjectSnakeToCamel<CreateVideoEndpointOptions>, 'client'>;
export type CreatePlaylistEndpointOptions = {
/**
@@ -343,16 +350,7 @@ export type EditPlaylistEndpointOptions = {
}[];
}
export interface IEditPlaylistRequest extends ObjectSnakeToCamel<EditPlaylistEndpointOptions> {
actions: {
action: 'ACTION_ADD_VIDEO' | 'ACTION_REMOVE_VIDEO' | 'ACTION_MOVE_VIDEO_AFTER' | 'ACTION_SET_PLAYLIST_DESCRIPTION' | 'ACTION_SET_PLAYLIST_NAME';
addedVideoId?: string;
setVideoId?: string;
movedSetVideoIdPredecessor?: string;
playlistDescription?: string;
playlistName?: string;
}[];
}
export type IEditPlaylistRequest = ObjectSnakeToCamel<EditPlaylistEndpointOptions>;
export type BlocklistPickerRequestEndpointOptions = {
channel_id: string;
@@ -364,19 +362,21 @@ export type IBlocklistPickerRequest = {
}
}
export interface IReelWatchRequest {
export interface IReelItemWatchRequest {
disablePlayerResponse: boolean;
playerRequest: {
videoId: string,
params: string,
},
params?: string;
client?: InnerTubeClient;
}
export type ReelWatchEndpointOptions = {
export type ReelItemWatchEndpointOptions = {
/**
* The shorts ID.
*/
short_id: string;
video_id: string;
/**
* The client to use.
*/
@@ -387,7 +387,7 @@ export type ReelWatchEndpointOptions = {
params?: string;
}
export interface IReelSequenceRequest {
export interface IReelWatchSequenceRequest {
sequenceParams: string;
}
@@ -395,7 +395,7 @@ export type ReelWatchSequenceEndpointOptions = {
/**
* The protobuf parameters.
*/
sequenceParams: string;
sequence_params: string;
/**
* The client to use.
*/

View File

@@ -1,3 +1,4 @@
import type { InnerTubeClient } from '../types/index.js';
import type { Format } from '../parser/misc.js';
export type URLTransformer = (url: URL) => URL;
@@ -21,9 +22,9 @@ export interface FormatOptions {
*/
format?: string;
/**
* InnerTube client, can be ANDROID, WEB, YTMUSIC, YTMUSIC_ANDROID, YTSTUDIO_ANDROID or TV_EMBEDDED
* InnerTube client.
*/
client?: 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED';
client?: InnerTubeClient;
}
export interface DownloadOptions extends FormatOptions {

42
src/types/Misc.ts Normal file
View File

@@ -0,0 +1,42 @@
import type { SessionOptions } from '../core/index.js';
export type InnerTubeConfig = SessionOptions;
export type InnerTubeClient = 'IOS' | 'WEB' | 'ANDROID' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS';
export type UploadDate = 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';
export type SearchType = 'all' | 'video' | 'channel' | 'playlist' | 'movie';
export type Duration = 'all' | 'short' | 'medium' | 'long';
export type SortBy = 'relevance' | 'rating' | 'upload_date' | 'view_count';
export type Feature = 'hd' | 'subtitles' | 'creative_commons' | '3d' | 'live' | 'purchased' | '4k' | '360' | 'location' | 'hdr' | 'vr180';
export type SearchFilters = {
upload_date?: UploadDate;
type?: SearchType;
duration?: Duration;
sort_by?: SortBy;
features?: Feature[];
};
export type UpdateVideoMetadataOptions = Partial<{
title: string;
description: string;
tags: string[];
category: number;
license: string;
age_restricted: boolean;
made_for_kids: boolean;
privacy: 'PUBLIC' | 'PRIVATE' | 'UNLISTED';
}>;
export type UploadedVideoMetadataOptions = Partial<{
title: string;
description: string;
privacy: 'PUBLIC' | 'PRIVATE' | 'UNLISTED';
is_draft: boolean;
}>;
export type MusicSearchType = 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist';
export type MusicSearchFilters = {
type?: MusicSearchType;
};

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