mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 01:52:11 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fca6c354e | ||
|
|
93906e0539 | ||
|
|
aaebcca90b | ||
|
|
36121878b1 | ||
|
|
d89909a19a | ||
|
|
367a6f7ec5 | ||
|
|
05a663710d | ||
|
|
cc0fc7145a | ||
|
|
bdff3eae1c | ||
|
|
5a66d0ba93 | ||
|
|
370cb0b29e | ||
|
|
fcd00b0fb0 | ||
|
|
2dae5634f3 | ||
|
|
2c43a5705f | ||
|
|
83801ebc37 | ||
|
|
c14a687e65 | ||
|
|
f9ccba4af5 | ||
|
|
4b60b97132 | ||
|
|
7afc3da80e | ||
|
|
bb6e647b8c | ||
|
|
f1973c11d9 | ||
|
|
4942992630 | ||
|
|
0e91a08ae2 | ||
|
|
261f2ac12b | ||
|
|
041aebc358 | ||
|
|
c9f0ddd573 | ||
|
|
25d268beba | ||
|
|
2c0bb237e1 | ||
|
|
4f5635ad07 | ||
|
|
3b3cf1b2aa | ||
|
|
d85fbc56cf | ||
|
|
e55d4af100 | ||
|
|
04369be620 | ||
|
|
a89a5ac2dd | ||
|
|
5529a6aca0 | ||
|
|
94a6765c97 | ||
|
|
9b9fb82131 | ||
|
|
3153375bca | ||
|
|
a9bf225a62 | ||
|
|
1e29019a07 | ||
|
|
6765f4e0d7 | ||
|
|
3048f70f60 | ||
|
|
090539b28f | ||
|
|
6d0bc89be1 | ||
|
|
a5f62093a1 | ||
|
|
a352ddeb9d | ||
|
|
0f8f92a28a | ||
|
|
7d03469e64 | ||
|
|
62ac2f6f32 | ||
|
|
142a7d0428 | ||
|
|
efa7205723 | ||
|
|
84f90aaf29 | ||
|
|
858cdd197c | ||
|
|
5a8fd3ad37 | ||
|
|
a19511de24 | ||
|
|
bd9f6ac64c | ||
|
|
e5aab9a9b3 | ||
|
|
d6fa134c3d | ||
|
|
fe953072a2 | ||
|
|
055fa33403 | ||
|
|
14c3a06d40 | ||
|
|
67376afae6 | ||
|
|
4cbaa7983f |
@@ -1,9 +0,0 @@
|
||||
.git
|
||||
.github
|
||||
test/
|
||||
cache/
|
||||
src/proto/youtube.ts
|
||||
coverage/
|
||||
node_modules/
|
||||
dist/
|
||||
src/proto/generated/
|
||||
@@ -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
3
.gitignore
vendored
@@ -56,6 +56,7 @@ pnpm-lock.yaml
|
||||
|
||||
# Downloaded assets
|
||||
*.mp4
|
||||
*.m4a
|
||||
*.webm
|
||||
*.mkv
|
||||
|
||||
@@ -75,3 +76,5 @@ deno/
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
*.bin
|
||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -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
111
eslint.config.js
Normal 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"],
|
||||
},
|
||||
}
|
||||
];
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
298
examples/browser/web/package-lock.json
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}) {
|
||||
|
||||
@@ -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!`);
|
||||
})();
|
||||
@@ -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
678
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -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"
|
||||
|
||||
137
src/Innertube.ts
137
src/Innertube.ts
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { IMusicGetSearchSuggestionsRequest, MusicGetSearchSuggestionsEndpointOptions } from '../../../types/index.js';
|
||||
|
||||
|
||||
export const PATH = '/music/get_search_suggestions';
|
||||
|
||||
/**
|
||||
|
||||
20
src/core/endpoints/reel/ReelItemWatchEndpoint.ts
Normal file
20
src/core/endpoints/reel/ReelItemWatchEndpoint.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
22
src/parser/classes/EomSettingsDisclaimer.ts
Normal file
22
src/parser/classes/EomSettingsDisclaimer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export default class Factoid extends YTNode {
|
||||
|
||||
label: Text;
|
||||
value: Text;
|
||||
accessibility_text: String;
|
||||
accessibility_text: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
28
src/parser/classes/MusicPlaylistEditHeader.ts
Normal file
28
src/parser/classes/MusicPlaylistEditHeader.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/parser/classes/VideoAttributesSectionView.ts
Normal file
24
src/parser/classes/VideoAttributesSectionView.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
55
src/parser/ytshorts/ShortFormVideoInfo.ts
Normal file
55
src/parser/ytshorts/ShortFormVideoInfo.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1 +1 @@
|
||||
export { default as VideoInfo } from './VideoInfo.js';
|
||||
export { default as ShortFormVideoInfo } from './ShortFormVideoInfo.js';
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
}>;
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
42
src/types/Misc.ts
Normal 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
Reference in New Issue
Block a user