Compare commits

..

29 Commits

Author SHA1 Message Date
github-actions[bot]
312c636ec4 chore(main): release 7.0.0 (#528)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-10-28 15:21:53 -03:00
Azarattum
4c0de199e8 fix(build): Inline package.json import to avoid runtime erros (#509)
* chore(main): Inline package.json import

* chore: add `--legacy-peer-deps` flag to ci

* chore: update lock file

---------

Co-authored-by: LuanRT <luan.lrt4@gmail.com>
2023-10-28 15:19:37 -03:00
Ryan Sandbach
9ab528ec82 feat(Kids): Add blockChannel command to easily block channels (#503)
* Add blockChannel command to support easily blocking content for supervised accounts.

* Moved blockChannel functionality to the Kids client and updated API docs.

* Fix whitepsace issues.

* Resolve remaining linting errors.

* Avoid changing interaction manager. Remove comment for ToggleButton change.

* chore: clean up

---------

Co-authored-by: LuanRT <luan.lrt4@gmail.com>
2023-10-28 14:28:17 -03:00
LuanRT
24ffb01aef Merge branch 'main' of https://github.com/LuanRT/YouTube.js 2023-10-28 13:31:10 -03:00
LuanRT
eaac38c919 chore: lint 2023-10-28 13:30:58 -03:00
absidue
e627887fe0 chore(MediaInfo): Throw helpful errors when calling toDash or download for live and Post-Live DVR videos (#526)
* Address pull request feedback

---------

Co-authored-by: LuanRT <luan.lrt4@gmail.com>
2023-10-28 13:29:37 -03:00
LuanRT
beaa28f4c6 feat(music#getSearchSuggestions)!: Return array of SearchSuggestionsSection instead of a single node 2023-10-28 13:27:58 -03:00
LuanRT
a45273fec4 feat(parser): Add PlayerOverflow and PlayerControlsOverlay 2023-10-28 13:17:26 -03:00
absidue
bc97e07ac6 feat(UpdateViewerShipAction): Add original_view_count and unlabeled_view_count_value (#527) 2023-10-21 12:39:03 -03:00
LuanRT
f35b4c2c8c Merge branch 'main' of https://github.com/LuanRT/YouTube.js 2023-10-18 23:32:01 -03:00
LuanRT
c934325648 chore: update readme [skip ci] 2023-10-18 23:31:03 -03:00
dependabot[bot]
cd27acd25b chore(deps-dev): bump @babel/traverse from 7.22.10 to 7.23.2 (#524)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.10 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 22:55:44 -03:00
dependabot[bot]
83b42d2585 chore(deps): bump undici from 5.23.0 to 5.26.2 (#523)
Bumps [undici](https://github.com/nodejs/undici) from 5.23.0 to 5.26.2.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.23.0...v5.26.2)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: direct:production
...

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

+ Add support for retrieving different languages.

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

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

* chore: lint

* chore: update docs

* chore: Do not include nodes in errors thrown

* chore: Improve error messages

* fix(ExpandableMetadata): `expanded_content` type mismatch

* chore: lint
2023-09-10 01:50:30 -03:00
Jeremy Banks
86fb33ed03 feat(PlaylistManager): add .setName() and .setDescription() functions for editing playlists (#498) 2023-09-01 17:40:25 -03:00
github-actions[bot]
bff4210349 chore(main): release 6.3.0 (#495)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-08-31 20:41:06 -03:00
absidue
91de6e5c0e feat(ChannelMetadata): Add music_artist_name (#497) 2023-08-31 20:29:58 -03:00
absidue
c26972c42a fix(CompactMovie): Add missing import and remove unnecessary console.log (#496) 2023-08-31 20:28:25 -03:00
Jeremy Banks
8bc2aaa358 feat(Session): Add on_behalf_of_user session option. (#494)
This specifies which channel to use if multiple are associated with the logged-in account.
2023-08-31 08:20:37 -03:00
68 changed files with 5302 additions and 1572 deletions

View File

@@ -14,5 +14,5 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm ci
- run: npm ci --legacy-peer-deps
- run: npm run lint

View File

@@ -24,7 +24,7 @@ jobs:
node-version: "16.x"
- name: Build for Deno
run: |
npm ci
npm ci --legacy-peer-deps
npm run build:deno
if: ${{ steps.release.outputs.release_created }}
- name: Move Deno files
@@ -55,7 +55,7 @@ jobs:
if: ${{ steps.release.outputs.release_created }}
- name: Publish package to npmjs
run: |
npm ci
npm ci --legacy-peer-deps
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -14,5 +14,5 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm ci
- run: npm ci --legacy-peer-deps
- run: npm run test

View File

@@ -1,5 +1,64 @@
# Changelog
## [7.0.0](https://github.com/LuanRT/YouTube.js/compare/v6.4.1...v7.0.0) (2023-10-28)
### ⚠ BREAKING CHANGES
* **music#getSearchSuggestions:** Return array of `SearchSuggestionsSection` instead of a single node
### Features
* **Kids:** Add `blockChannel` command to easily block channels ([#503](https://github.com/LuanRT/YouTube.js/issues/503)) ([9ab528e](https://github.com/LuanRT/YouTube.js/commit/9ab528ec823dcd527a97150009eed632c6d3eb6a))
* **music#getSearchSuggestions:** Return array of `SearchSuggestionsSection` instead of a single node ([beaa28f](https://github.com/LuanRT/YouTube.js/commit/beaa28f4c68de8366caa84ce5a026bf9e12e1b9d))
* **parser:** Add `PlayerOverflow` and `PlayerControlsOverlay` ([a45273f](https://github.com/LuanRT/YouTube.js/commit/a45273fec498df87eecd364ffb708c9f787793d5))
* **UpdateViewerShipAction:** Add `original_view_count` and `unlabeled_view_count_value` ([#527](https://github.com/LuanRT/YouTube.js/issues/527)) ([bc97e07](https://github.com/LuanRT/YouTube.js/commit/bc97e07ac6d1cdc45194e214c6001cf92190e1d5))
### Bug Fixes
* **build:** Inline package.json import to avoid runtime erros ([#509](https://github.com/LuanRT/YouTube.js/issues/509)) ([4c0de19](https://github.com/LuanRT/YouTube.js/commit/4c0de199e85dd5cc8b3719920b24dec9613acaab))
## [6.4.1](https://github.com/LuanRT/YouTube.js/compare/v6.4.0...v6.4.1) (2023-10-02)
### Bug Fixes
* **Feed:** Do not throw when multiple continuations are present ([8e372d5](https://github.com/LuanRT/YouTube.js/commit/8e372d5c67f148be288bb0485f2c70ec43fbecd0))
* **Playlist:** Throw a more helpful error when parsing empty responses ([987f506](https://github.com/LuanRT/YouTube.js/commit/987f50604a0163f9a07091ce787995c6f6fddb75))
### Performance Improvements
* Cache deciphered n-params by info response ([#505](https://github.com/LuanRT/YouTube.js/issues/505)) ([d2959b3](https://github.com/LuanRT/YouTube.js/commit/d2959b3a55a5081295da4754627913933bbaf1e7))
* **generator:** Remove duplicate checks in `isMiscType` ([#506](https://github.com/LuanRT/YouTube.js/issues/506)) ([68df321](https://github.com/LuanRT/YouTube.js/commit/68df3218580db10c9a0932c93ff2ce487526ff1e))
## [6.4.0](https://github.com/LuanRT/YouTube.js/compare/v6.3.0...v6.4.0) (2023-09-10)
### Features
* Add support for retrieving transcripts ([#500](https://github.com/LuanRT/YouTube.js/issues/500)) ([f94ea6c](https://github.com/LuanRT/YouTube.js/commit/f94ea6cf917f63f30dd66514b22a4cf43b948f07))
* **PlaylistManager:** add .setName() and .setDescription() functions for editing playlists ([#498](https://github.com/LuanRT/YouTube.js/issues/498)) ([86fb33e](https://github.com/LuanRT/YouTube.js/commit/86fb33ed03a127d9fd4caa695ca97642bffe61bd))
### Bug Fixes
* **BackstagePost:** `vote_button` type mismatch ([fba3fc9](https://github.com/LuanRT/YouTube.js/commit/fba3fc971454d66d80d4920fbd60889a221de381))
## [6.3.0](https://github.com/LuanRT/YouTube.js/compare/v6.2.0...v6.3.0) (2023-08-31)
### Features
* **ChannelMetadata:** Add `music_artist_name` ([#497](https://github.com/LuanRT/YouTube.js/issues/497)) ([91de6e5](https://github.com/LuanRT/YouTube.js/commit/91de6e5c0e5b27e6d12ce5db2f500c5ff78b9830))
* **Session:** Add on_behalf_of_user session option. ([#494](https://github.com/LuanRT/YouTube.js/issues/494)) ([8bc2aaa](https://github.com/LuanRT/YouTube.js/commit/8bc2aaa3587fcf79f69eedbc2bf422a4c6fa7eb1))
### Bug Fixes
* **CompactMovie:** Add missing import and remove unnecessary console.log ([#496](https://github.com/LuanRT/YouTube.js/issues/496)) ([c26972c](https://github.com/LuanRT/YouTube.js/commit/c26972c42a6368822ac254c00f1bbee5a1542486))
## [6.2.0](https://github.com/LuanRT/YouTube.js/compare/v6.1.0...v6.2.0) (2023-08-29)

View File

@@ -171,7 +171,7 @@ import dashjs from 'dashjs';
const youtube = await Innertube.create({ /* setup - see above */ });
// get the video info
// Get the video info
const videoInfo = await youtube.getInfo('videoId');
// now convert to a dash manifest
@@ -191,7 +191,7 @@ const player = dashjs.MediaPlayer().create();
player.initialize(videoElement, uri, true);
```
A fully working example can be found in [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/blob/main/examples/browser/web). Alternatively, you can view it live at [ytjsexample.pages.dev](https://ytjsexample.pages.dev/).
A fully working example can be found in [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/blob/main/examples/browser/web).
<a name="custom-fetch"></a>
@@ -325,6 +325,9 @@ Retrieves video info.
- `<info>#download(options)`
- Downloads the video. See [download](#download).
- `<info>#getTranscript()`
- Retrieves the video's transcript.
- `<info>#filters`
- Returns filters that can be applied to the watch next feed.

View File

@@ -12,7 +12,7 @@ Handles direct interactions.
* [.unsubscribe(video_id)](#unsubscribe)
* [.comment(video_id, text)](#comment)
* [.translate(text, target_language, args?)](#translate)
* [.setNotificationPreferences(channel_id, type)](#setnotificationpreferences)
* [.setNotificationPreferences(channel_id, type)](#setnotificationpreferences)
<a name="like"></a>
### like(video_id)

View File

@@ -9,6 +9,7 @@ YouTube Kids is a modified version of the YouTube app, with a simplified interfa
* [.getInfo(video_id)](#getinfo)
* [.getChannel(channel_id)](#getchannel)
* [.getHomeFeed()](#gethomefeed)
* [.blockChannel(channel_id)](#blockchannel)
<a name="search"></a>
### search(query)
@@ -110,4 +111,17 @@ Retrieves the home feed.
- Returns available categories.
- `<feed>#page`
- Returns the original InnerTube response(s), parsed and sanitized.
- Returns the original InnerTube response(s), parsed and sanitized.
</details>
<a name="blockChannel"></a>
### blockChannel(channel_id)
Retrieves the list of supervised accounts that the signed-in user has access to and blocks the given channel for each of them.
**Returns:** `Promise.<ApiResponse[]>`
| Param | Type | Description |
| --- | --- | --- |
| channel_id | `string` | Channel id |

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
import { Innertube, UniversalCache } from 'youtubei.js';
(async () => {
const yt = await Innertube.create({ cache: new UniversalCache(true, './credcache') });
yt.session.on('auth-pending', (data) => {
console.log(`Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`);
});
yt.session.on('auth', async () => {
console.log('Sign in successful');
await yt.session.oauth.cacheCredentials();
});
yt.session.on('update-credentials', async () => {
await yt.session.oauth.cacheCredentials();
});
// Attempt to sign in
await yt.session.signIn();
// Block Channel for all kids / profiles on the signed-in account.
const resp = await yt.kids.blockChannel('UCpbpfcZfo-hoDAx2m1blFhg');
console.info('Blocked channel for ', resp.length, ' profiles.');
})();

View File

@@ -58,6 +58,4 @@ After that, you can use the library as normal.
## Example
We've got a full example in `examples/browser/web` using vite.
If you don't want to run the example yourself, you can see it in action here: [ytjsexample.pages.dev](https://ytjsexample.pages.dev/).
We've got a full example in `examples/browser/web` using vite.

View File

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

5692
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "6.2.0",
"version": "7.0.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",
@@ -71,7 +71,7 @@
"build": "npm run build:parser-map && npm run build:proto && npm run build:esm && npm run bundle:node && npm run bundle:browser && npm run bundle:browser:prod",
"build:parser-map": "node ./scripts/gen-parser-map.mjs",
"build:proto": "npx pb-gen-ts --entry-path=\"src/proto\" --out-dir=\"src/proto/generated\" --ext-in-import=\".js\"",
"build:esm": "npx tsc",
"build:esm": "npx tspc",
"build:deno": "npx cpy ./src ./deno && npx esbuild ./src/utils/DashManifest.tsx --keep-names --format=esm --platform=neutral --target=es2020 --outfile=./deno/src/utils/DashManifest.js && npx cpy ./package.json ./deno && npx replace \".js';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'./DashManifest.ts';\" \"'./DashManifest.js';\" ./deno -r && npx replace \"'jintr';\" \"'https://esm.sh/jintr';\" ./deno -r",
"bundle:node": "npx esbuild ./dist/src/platform/node.js --bundle --target=node10 --keep-names --format=cjs --platform=node --outfile=./bundle/node.cjs --external:jintr --external:undici --external:linkedom --external:tslib --sourcemap --banner:js=\"/* eslint-disable */\"",
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --conditions=module --outfile=./bundle/browser.js --platform=browser",
@@ -104,6 +104,8 @@
"pbkit": "^0.0.59",
"replace": "^1.2.2",
"ts-jest": "^28.0.8",
"ts-patch": "^3.0.2",
"ts-transformer-inline-file": "^0.2.0",
"typescript": "^5.0.0"
},
"bugs": {

View File

@@ -36,7 +36,7 @@ import {
import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.js';
import type { ApiResponse } from './core/Actions.js';
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js';
import { type IBrowseResponse, type IParsedResponse } from './parser/types/index.js';
import type { INextRequest } from './types/index.js';
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.js';

View File

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

View File

@@ -63,6 +63,7 @@ export interface Context {
user: {
enableSafetyMode: boolean;
lockedSafetyMode: boolean;
onBehalfOfUser?: string;
};
thirdParty?: {
embedUrl: string;
@@ -84,6 +85,10 @@ export interface SessionOptions {
* Only works if you are signed in with cookies.
*/
account_index?: number;
/**
* Specify the Page ID of the YouTube profile/channel to use, if the logged-in account has multiple profiles.
*/
on_behalf_of_user?: string;
/**
* Specifies whether to retrieve the JS player. Disabling this will make session creation faster.
* **NOTE:** Deciphering formats is not possible without the JS player.
@@ -193,7 +198,8 @@ export default class Session extends EventEmitterLike {
options.device_category,
options.client_type,
options.timezone,
options.fetch
options.fetch,
options.on_behalf_of_user
);
return new Session(
@@ -213,11 +219,12 @@ export default class Session extends EventEmitterLike {
device_category: DeviceCategory = 'desktop',
client_name: ClientType = ClientType.WEB,
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
fetch: FetchFunction = Platform.shim.fetch
fetch: FetchFunction = Platform.shim.fetch,
on_behalf_of_user?: string
) {
let session_data: SessionData;
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data };
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user };
if (generate_session_locally) {
session_data = this.#generateSessionData(session_args);
@@ -241,6 +248,7 @@ export default class Session extends EventEmitterLike {
client_name: string;
enable_safety_mode: boolean;
visitor_data: string;
on_behalf_of_user?: string;
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
@@ -300,7 +308,8 @@ export default class Session extends EventEmitterLike {
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
lockedSafetyMode: false,
onBehalfOfUser: options.on_behalf_of_user
}
};
@@ -315,6 +324,7 @@ export default class Session extends EventEmitterLike {
client_name: string;
enable_safety_mode: boolean;
visitor_data: string;
on_behalf_of_user?: string;
}): SessionData {
let visitor_id = generateRandomString(11);
@@ -347,7 +357,8 @@ export default class Session extends EventEmitterLike {
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
lockedSafetyMode: false,
onBehalfOfUser: options.on_behalf_of_user
}
};

View File

@@ -1,16 +1,22 @@
import Parser from '../../parser/index.js';
import Channel from '../../parser/ytkids/Channel.js';
import HomeFeed from '../../parser/ytkids/HomeFeed.js';
import Search from '../../parser/ytkids/Search.js';
import VideoInfo from '../../parser/ytkids/VideoInfo.js';
import type Session from '../Session.js';
import { type ApiResponse } from '../Actions.js';
import { generateRandomString } from '../../utils/Utils.js';
import { InnertubeError, generateRandomString } from '../../utils/Utils.js';
import {
BrowseEndpoint, NextEndpoint,
PlayerEndpoint, SearchEndpoint
} from '../endpoints/index.js';
import { BlocklistPickerEndpoint } from '../endpoints/kids/index.js';
import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.js';
export default class Kids {
#session: Session;
@@ -80,4 +86,38 @@ export default class Kids {
);
return new HomeFeed(this.#session.actions, response);
}
/**
* Retrieves the list of supervised accounts that the signed-in user has
* access to, and blocks the given channel for each of them.
* @param channel_id - The channel id to block.
* @returns A list of API responses.
*/
async blockChannel(channel_id: string): Promise<ApiResponse[]> {
if (!this.#session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const blocklist_payload = BlocklistPickerEndpoint.build({ channel_id: channel_id });
const response = await this.#session.actions.execute(BlocklistPickerEndpoint.PATH, blocklist_payload );
const popup = response.data.command.confirmDialogEndpoint;
const popup_fragment = { contents: popup.content, engagementPanels: [] };
const kid_picker = Parser.parseResponse(popup_fragment);
const kids = kid_picker.contents_memo?.getType(KidsBlocklistPickerItem);
if (!kids)
throw new InnertubeError('Could not find any kids profiles or supervised accounts.');
// Iterate through the kids and block the channel if not already blocked.
const responses: ApiResponse[] = [];
for (const kid of kids) {
if (!kid.block_button?.is_toggled) {
kid.setActions(this.#session.actions);
// Block channel and add to the response list.
responses.push(await kid.blockChannel());
}
}
return responses;
}
}

View File

@@ -20,7 +20,7 @@ import SectionList from '../../parser/classes/SectionList.js';
import Tab from '../../parser/classes/Tab.js';
import * as Proto from '../../proto/index.js';
import type { ObservedArray, YTNode } from '../../parser/helpers.js';
import type { ObservedArray } from '../../parser/helpers.js';
import type { MusicSearchFilters } from '../../types/index.js';
import { InnertubeError, generateRandomString, throwIfMissing } from '../../utils/Utils.js';
import type Actions from '../Actions.js';
@@ -355,17 +355,17 @@ export default class Music {
* Retrieves search suggestions for the given query.
* @param query - The query.
*/
async getSearchSuggestions(query: string): Promise<ObservedArray<YTNode>> {
async getSearchSuggestions(query: string): Promise<ObservedArray<SearchSuggestionsSection>> {
const response = await this.#actions.execute(
GetSearchSuggestionsEndpoint.PATH,
{ ...GetSearchSuggestionsEndpoint.build({ input: query }), parse: true }
);
if (!response.contents_memo)
throw new InnertubeError('Unexpected response', response);
return [] as unknown as ObservedArray<SearchSuggestionsSection>;
const search_suggestions_section = response.contents_memo.getType(SearchSuggestionsSection).first();
const search_suggestions_sections = response.contents_memo.getType(SearchSuggestionsSection);
return search_suggestions_section.contents;
return search_suggestions_sections;
}
}

View File

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

View File

@@ -15,4 +15,5 @@ export * as Music from './music/index.js';
export * as Notification from './notification/index.js';
export * as Playlist from './playlist/index.js';
export * as Subscription from './subscription/index.js';
export * as Upload from './upload/index.js';
export * as Upload from './upload/index.js';
export * as Kids from './kids/index.js';

View File

@@ -0,0 +1,12 @@
import type { IBlocklistPickerRequest, BlocklistPickerRequestEndpointOptions } from '../../../types/index.js';
export const PATH = '/kids/get_kids_blocklist_picker';
/**
* Builds a `/kids/get_kids_blocklist_picker` request payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: BlocklistPickerRequestEndpointOptions): IBlocklistPickerRequest {
return { blockedForKidsContent: { external_channel_id: options.channel_id } };
}

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,8 @@ import Parser from '../../parser/index.js';
import type { DashOptions } from '../../types/DashOptions.js';
import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.js';
import { getStreamingInfo } from '../../utils/StreamingInfo.js';
import ContinuationItem from '../../parser/classes/ContinuationItem.js';
import TranscriptInfo from '../../parser/youtube/TranscriptInfo.js';
export default class MediaInfo {
#page: [IPlayerResponse, INextResponse?];
@@ -44,10 +46,16 @@ export default class MediaInfo {
* @returns DASH manifest
*/
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise<string> {
const player_response = this.#page[0];
if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) {
throw new InnertubeError('Generating DASH manifests for live and Post-Live-DVR videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.');
}
let storyboards;
if (options.include_thumbnails && this.#page[0].storyboards?.is(PlayerStoryboardSpec)) {
storyboards = this.#page[0].storyboards;
if (options.include_thumbnails && player_response.storyboards?.is(PlayerStoryboardSpec)) {
storyboards = player_response.storyboards;
}
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards);
@@ -81,9 +89,45 @@ export default class MediaInfo {
* @param options - Download options.
*/
async download(options: DownloadOptions = {}): Promise<ReadableStream<Uint8Array>> {
const player_response = this.#page[0];
if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) {
throw new InnertubeError('Downloading is not supported for live and Post-Live-DVR videos, as they are split up into 5 second segments that are individual files, which require using a tool such as ffmpeg to stitch them together, so they cannot be returned in a single stream.');
}
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
}
/**
* Retrieves the video's transcript.
* @param video_id - The video id.
*/
async getTranscript(): Promise<TranscriptInfo> {
const next_response = this.page[1];
if (!next_response)
throw new InnertubeError('Cannot get transcript from basic video info.');
if (!next_response.engagement_panels)
throw new InnertubeError('Engagement panels not found. Video likely has no transcript.');
const transcript_panel = next_response.engagement_panels.get({
panel_identifier: 'engagement-panel-searchable-transcript'
});
if (!transcript_panel)
throw new InnertubeError('Transcript panel not found. Video likely has no transcript.');
const transcript_continuation = transcript_panel.content?.as(ContinuationItem);
if (!transcript_continuation)
throw new InnertubeError('Transcript continuation not found.');
const response = await transcript_continuation.endpoint.call(this.actions);
return new TranscriptInfo(this.actions, response);
}
/**
* Adds video to the watch history.
*/

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import type { RawNode } from '../index.js';
import Parser from '../index.js';
import Author from './misc/Author.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import Text from './misc/Text.js';
import Thumbnail from './misc/Thumbnail.js';
import Menu from './menus/Menu.js';
import { timeToSeconds } from '../../utils/Utils.js';
@@ -29,7 +30,6 @@ export default class CompactMovie extends YTNode {
constructor(data: RawNode) {
super();
console.log(data);
const overlay_time_status = data.thumbnailOverlays
.find((overlay: RawNode) => overlay.thumbnailOverlayTimeStatusRenderer)
?.thumbnailOverlayTimeStatusRenderer.text || 'N/A';

View File

@@ -3,6 +3,7 @@ import Parser, { type RawNode } from '../index.js';
import ContinuationItem from './ContinuationItem.js';
import EngagementPanelTitleHeader from './EngagementPanelTitleHeader.js';
import MacroMarkersList from './MacroMarkersList.js';
import ProductList from './ProductList.js';
import SectionList from './SectionList.js';
import StructuredDescriptionContent from './StructuredDescriptionContent.js';
@@ -10,7 +11,7 @@ export default class EngagementPanelSectionList extends YTNode {
static type = 'EngagementPanelSectionList';
header: EngagementPanelTitleHeader | null;
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | null;
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | ProductList | null;
target_id?: string;
panel_identifier?: string;
visibility?: string;
@@ -18,9 +19,9 @@ export default class EngagementPanelSectionList extends YTNode {
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader);
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList ]);
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList, ProductList ]);
this.panel_identifier = data.panelIdentifier;
this.target_id = data.targetId;
this.visibility = data.visibility;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ export default class ToggleButton extends YTNode {
this.toggled_tooltip = data.toggledTooltip;
this.is_toggled = data.isToggled;
this.is_disabled = data.isDisabled;
this.icon_type = data.defaultIcon.iconType;
this.icon_type = data.defaultIcon?.iconType;
const acc_label =
data?.defaultText?.accessibility?.accessibilityData?.label ||

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ import Parser, { type RawNode } from '../index.js';
import { Text, Thumbnail } from '../misc.js';
import Factoid from './Factoid.js';
import NavigationEndpoint from './NavigationEndpoint.js';
import UploadTimeFactoid from './UploadTimeFactoid.js';
import ViewCountFactoid from './ViewCountFactoid.js';
export default class VideoDescriptionHeader extends YTNode {
static type = 'VideoDescriptionHeader';
@@ -10,7 +12,7 @@ export default class VideoDescriptionHeader extends YTNode {
channel: Text;
channel_navigation_endpoint?: NavigationEndpoint;
channel_thumbnail: Thumbnail[];
factoids: ObservedArray<Factoid>;
factoids: ObservedArray<Factoid | ViewCountFactoid | UploadTimeFactoid>;
publish_date: Text;
title: Text;
views: Text;
@@ -23,6 +25,6 @@ export default class VideoDescriptionHeader extends YTNode {
this.channel_thumbnail = Thumbnail.fromResponse(data.channelThumbnail);
this.publish_date = new Text(data.publishDate);
this.views = new Text(data.views);
this.factoids = Parser.parseArray(data.factoid, Factoid);
this.factoids = Parser.parseArray(data.factoid, [ Factoid, ViewCountFactoid, UploadTimeFactoid ]);
}
}

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,8 @@ export default class UpdateViewershipAction extends YTNode {
view_count: Text;
extra_short_view_count: Text;
original_view_count: Number;
unlabeled_view_count_value: Text;
is_live: boolean;
constructor(data: RawNode) {
@@ -14,6 +16,8 @@ export default class UpdateViewershipAction extends YTNode {
const view_count_renderer = data.viewCount.videoViewCountRenderer;
this.view_count = new Text(view_count_renderer.viewCount);
this.extra_short_view_count = new Text(view_count_renderer.extraShortViewCount);
this.original_view_count = parseInt(view_count_renderer.originalViewCount);
this.unlabeled_view_count_value = new Text(view_count_renderer.unlabeledViewCountValue);
this.is_live = view_count_renderer.isLive;
}
}

View File

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

View File

@@ -3,6 +3,8 @@ import { InnertubeError } from '../../../utils/Utils.js';
import type { RawNode } from '../../index.js';
export default class Format {
#this_response_nsig_cache?: Map<string, string>;
itag: number;
mime_type: string;
is_type_otf: boolean;
@@ -52,7 +54,11 @@ export default class Format {
matrix_coefficients?: string;
};
constructor(data: RawNode) {
constructor(data: RawNode, this_response_nsig_cache?: Map<string, string>) {
if (this_response_nsig_cache) {
this.#this_response_nsig_cache = this_response_nsig_cache;
}
this.itag = data.itag;
this.mime_type = data.mimeType;
this.is_type_otf = data.type === 'FORMAT_STREAM_TYPE_OTF';
@@ -122,6 +128,6 @@ export default class Format {
*/
decipher(player: Player | undefined): string {
if (!player) throw new InnertubeError('Cannot decipher format, this session appears to have no valid player.');
return player.decipher(this.url, this.signature_cipher, this.cipher);
return player.decipher(this.url, this.signature_cipher, this.cipher, this.#this_response_nsig_cache);
}
}

View File

@@ -0,0 +1,22 @@
import Text from '../misc/Text.js';
import { YTNode } from '../../helpers.js';
import Button from '../Button.js';
import Parser, { type RawNode } from '../../index.js';
import KidsBlocklistPickerItem from './KidsBlocklistPickerItem.js';
export default class KidsBlocklistPicker extends YTNode {
static type = 'KidsBlocklistPicker';
title: Text;
child_rows: KidsBlocklistPickerItem[] | null;
done_button: Button | null;
successful_toast_action_message: Text;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.child_rows = Parser.parse(data.childRows, true, [ KidsBlocklistPickerItem ]);
this.done_button = Parser.parseItem(data.doneButton, [ Button ]);
this.successful_toast_action_message = new Text(data.successfulToastActionMessage);
}
}

View File

@@ -0,0 +1,49 @@
import Text from '../misc/Text.js';
import { YTNode } from '../../helpers.js';
import Parser, { type RawNode } from '../../index.js';
import ToggleButton from '../ToggleButton.js';
import Thumbnail from '../misc/Thumbnail.js';
import type Actions from '../../../core/Actions.js';
import { InnertubeError } from '../../../utils/Utils.js';
import { type ApiResponse } from '../../../core/Actions.js';
export default class KidsBlocklistPickerItem extends YTNode {
static type = 'KidsBlocklistPickerItem';
#actions?: Actions;
child_display_name: Text;
child_account_description: Text;
avatar: Thumbnail[];
block_button: ToggleButton | null;
blocked_entity_key: string;
constructor(data: RawNode) {
super();
this.child_display_name = new Text(data.childDisplayName);
this.child_account_description = new Text(data.childAccountDescription);
this.avatar = Thumbnail.fromResponse(data.avatar);
this.block_button = Parser.parseItem(data.blockButton, [ ToggleButton ]);
this.blocked_entity_key = data.blockedEntityKey;
}
async blockChannel(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('An active caller must be provide to perform this operation.');
const button = this.block_button;
if (!button)
throw new InnertubeError('Block button was not found.', { child_display_name: this.child_display_name });
if (button.is_toggled)
throw new InnertubeError('This channel is already blocked.', { child_display_name: this.child_display_name });
const response = await button.endpoint.call(this.#actions, { parse: false });
return response;
}
setActions(actions: Actions | undefined) {
this.#actions = actions;
}
}

View File

@@ -134,33 +134,35 @@ export function isRendererList(value: unknown) {
* @returns If it is a misc type, return the InferenceType. Otherwise, return false.
*/
export function isMiscType(key: string, value: unknown): MiscInferenceType | false {
// NavigationEndpoint
if ((key.endsWith('Endpoint') || key.endsWith('Command') || key === 'endpoint') && typeof value === 'object' && value !== null) {
return {
type: 'misc',
endpoint: new NavigationEndpoint(value),
optional: false,
misc_type: 'NavigationEndpoint'
};
}
// Text
if (typeof value === 'object' && value !== null && (Reflect.has(value, 'simpleText') || Reflect.has(value, 'runs'))) {
const textNode = new Text(value);
return {
type: 'misc',
misc_type: 'Text',
optional: false,
endpoint: textNode.endpoint,
text: textNode.toString()
};
}
// Thumbnail
if (typeof value === 'object' && value !== null && Reflect.has(value, 'thumbnails') && Array.isArray(Reflect.get(value, 'thumbnails'))) {
return {
type: 'misc',
misc_type: 'Thumbnail',
optional: false
};
if (typeof value === 'object' && value !== null) {
// NavigationEndpoint
if (key.endsWith('Endpoint') || key.endsWith('Command') || key === 'endpoint') {
return {
type: 'misc',
endpoint: new NavigationEndpoint(value),
optional: false,
misc_type: 'NavigationEndpoint'
};
}
// Text
if (Reflect.has(value, 'simpleText') || Reflect.has(value, 'runs')) {
const textNode = new Text(value);
return {
type: 'misc',
misc_type: 'Text',
optional: false,
endpoint: textNode.endpoint,
text: textNode.toString()
};
}
// Thumbnail
if (Reflect.has(value, 'thumbnails') && Array.isArray(Reflect.get(value, 'thumbnails'))) {
return {
type: 'misc',
misc_type: 'Thumbnail',
optional: false
};
}
}
return false;
}

View File

@@ -7,6 +7,7 @@ export { default as AccountItemSectionHeader } from './classes/AccountItemSectio
export { default as AccountSectionList } from './classes/AccountSectionList.js';
export { default as AppendContinuationItemsAction } from './classes/actions/AppendContinuationItemsAction.js';
export { default as OpenPopupAction } from './classes/actions/OpenPopupAction.js';
export { default as UpdateEngagementPanelAction } from './classes/actions/UpdateEngagementPanelAction.js';
export { default as Alert } from './classes/Alert.js';
export { default as AlertWithButton } from './classes/AlertWithButton.js';
export { default as AnalyticsMainAppKeyMetrics } from './classes/analytics/AnalyticsMainAppKeyMetrics.js';
@@ -102,6 +103,7 @@ export { default as ExpandableTab } from './classes/ExpandableTab.js';
export { default as ExpandableVideoDescriptionBody } from './classes/ExpandableVideoDescriptionBody.js';
export { default as ExpandedShelfContents } from './classes/ExpandedShelfContents.js';
export { default as Factoid } from './classes/Factoid.js';
export { default as FancyDismissibleDialog } from './classes/FancyDismissibleDialog.js';
export { default as FeedFilterChipBar } from './classes/FeedFilterChipBar.js';
export { default as FeedTabbedHeader } from './classes/FeedTabbedHeader.js';
export { default as GameCard } from './classes/GameCard.js';
@@ -191,6 +193,7 @@ export { default as MacroMarkersList } from './classes/MacroMarkersList.js';
export { default as MacroMarkersListItem } from './classes/MacroMarkersListItem.js';
export { default as Menu } from './classes/menus/Menu.js';
export { default as MenuNavigationItem } from './classes/menus/MenuNavigationItem.js';
export { default as MenuPopup } from './classes/menus/MenuPopup.js';
export { default as MenuServiceItem } from './classes/menus/MenuServiceItem.js';
export { default as MenuServiceItemDownload } from './classes/menus/MenuServiceItemDownload.js';
export { default as MultiPageMenu } from './classes/menus/MultiPageMenu.js';
@@ -249,11 +252,13 @@ export { default as PageHeaderView } from './classes/PageHeaderView.js';
export { default as PageIntroduction } from './classes/PageIntroduction.js';
export { default as PlayerAnnotationsExpanded } from './classes/PlayerAnnotationsExpanded.js';
export { default as PlayerCaptionsTracklist } from './classes/PlayerCaptionsTracklist.js';
export { default as PlayerControlsOverlay } from './classes/PlayerControlsOverlay.js';
export { default as PlayerErrorMessage } from './classes/PlayerErrorMessage.js';
export { default as PlayerLegacyDesktopYpcOffer } from './classes/PlayerLegacyDesktopYpcOffer.js';
export { default as PlayerLegacyDesktopYpcTrailer } from './classes/PlayerLegacyDesktopYpcTrailer.js';
export { default as PlayerLiveStoryboardSpec } from './classes/PlayerLiveStoryboardSpec.js';
export { default as PlayerMicroformat } from './classes/PlayerMicroformat.js';
export { default as PlayerOverflow } from './classes/PlayerOverflow.js';
export { default as PlayerOverlay } from './classes/PlayerOverlay.js';
export { default as PlayerOverlayAutoplay } from './classes/PlayerOverlayAutoplay.js';
export { default as PlayerStoryboardSpec } from './classes/PlayerStoryboardSpec.js';
@@ -274,6 +279,9 @@ export { default as PlaylistVideoThumbnail } from './classes/PlaylistVideoThumbn
export { default as Poll } from './classes/Poll.js';
export { default as Post } from './classes/Post.js';
export { default as PostMultiImage } from './classes/PostMultiImage.js';
export { default as ProductList } from './classes/ProductList.js';
export { default as ProductListHeader } from './classes/ProductListHeader.js';
export { default as ProductListItem } from './classes/ProductListItem.js';
export { default as ProfileColumn } from './classes/ProfileColumn.js';
export { default as ProfileColumnStats } from './classes/ProfileColumnStats.js';
export { default as ProfileColumnStatsEntry } from './classes/ProfileColumnStatsEntry.js';
@@ -322,6 +330,7 @@ export { default as SlimOwner } from './classes/SlimOwner.js';
export { default as SlimVideoMetadata } from './classes/SlimVideoMetadata.js';
export { default as SortFilterSubMenu } from './classes/SortFilterSubMenu.js';
export { default as StructuredDescriptionContent } from './classes/StructuredDescriptionContent.js';
export { default as StructuredDescriptionPlaylistLockup } from './classes/StructuredDescriptionPlaylistLockup.js';
export { default as SubFeedOption } from './classes/SubFeedOption.js';
export { default as SubFeedSelector } from './classes/SubFeedSelector.js';
export { default as SubscribeButton } from './classes/SubscribeButton.js';
@@ -349,22 +358,33 @@ export { default as ToggleButton } from './classes/ToggleButton.js';
export { default as ToggleMenuServiceItem } from './classes/ToggleMenuServiceItem.js';
export { default as Tooltip } from './classes/Tooltip.js';
export { default as TopicChannelDetails } from './classes/TopicChannelDetails.js';
export { default as Transcript } from './classes/Transcript.js';
export { default as TranscriptFooter } from './classes/TranscriptFooter.js';
export { default as TranscriptSearchBox } from './classes/TranscriptSearchBox.js';
export { default as TranscriptSearchPanel } from './classes/TranscriptSearchPanel.js';
export { default as TranscriptSectionHeader } from './classes/TranscriptSectionHeader.js';
export { default as TranscriptSegment } from './classes/TranscriptSegment.js';
export { default as TranscriptSegmentList } from './classes/TranscriptSegmentList.js';
export { default as TwoColumnBrowseResults } from './classes/TwoColumnBrowseResults.js';
export { default as TwoColumnSearchResults } from './classes/TwoColumnSearchResults.js';
export { default as TwoColumnWatchNextResults } from './classes/TwoColumnWatchNextResults.js';
export { default as UniversalWatchCard } from './classes/UniversalWatchCard.js';
export { default as UploadTimeFactoid } from './classes/UploadTimeFactoid.js';
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 VideoCard } from './classes/VideoCard.js';
export { default as VideoDescriptionCourseSection } from './classes/VideoDescriptionCourseSection.js';
export { default as VideoDescriptionHeader } from './classes/VideoDescriptionHeader.js';
export { default as VideoDescriptionInfocardsSection } from './classes/VideoDescriptionInfocardsSection.js';
export { default as VideoDescriptionMusicSection } from './classes/VideoDescriptionMusicSection.js';
export { default as VideoDescriptionTranscriptSection } from './classes/VideoDescriptionTranscriptSection.js';
export { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.js';
export { default as VideoOwner } from './classes/VideoOwner.js';
export { default as VideoPrimaryInfo } from './classes/VideoPrimaryInfo.js';
export { default as VideoSecondaryInfo } from './classes/VideoSecondaryInfo.js';
export { default as ViewCountFactoid } from './classes/ViewCountFactoid.js';
export { default as WatchCardCompactVideo } from './classes/WatchCardCompactVideo.js';
export { default as WatchCardHeroVideo } from './classes/WatchCardHeroVideo.js';
export { default as WatchCardRichHeader } from './classes/WatchCardRichHeader.js';
@@ -373,6 +393,8 @@ export { default as WatchNextEndScreen } from './classes/WatchNextEndScreen.js';
export { default as WatchNextTabbedResults } from './classes/WatchNextTabbedResults.js';
export { default as YpcTrailer } from './classes/YpcTrailer.js';
export { default as AnchoredSection } from './classes/ytkids/AnchoredSection.js';
export { default as KidsBlocklistPicker } from './classes/ytkids/KidsBlocklistPicker.js';
export { default as KidsBlocklistPickerItem } from './classes/ytkids/KidsBlocklistPickerItem.js';
export { default as KidsCategoriesHeader } from './classes/ytkids/KidsCategoriesHeader.js';
export { default as KidsCategoryTab } from './classes/ytkids/KidsCategoryTab.js';
export { default as KidsHomeScreen } from './classes/ytkids/KidsHomeScreen.js';

View File

@@ -7,6 +7,7 @@ import PlayerLiveStoryboardSpec from './classes/PlayerLiveStoryboardSpec.js';
import PlayerStoryboardSpec from './classes/PlayerStoryboardSpec.js';
import Alert from './classes/Alert.js';
import AlertWithButton from './classes/AlertWithButton.js';
import EngagementPanelSectionList from './classes/EngagementPanelSectionList.js';
import type { IParsedResponse, IRawResponse, RawData, RawNode } from './types/index.js';
@@ -21,7 +22,13 @@ import { Memo, observe, SuperParsedResult } from './helpers.js';
import * as YTNodes from './nodes.js';
import type { KeyInfo } from './generator.js';
import { camelToSnake, generateRuntimeClass, generateTypescriptClass } from './generator.js';
import { Continuation, ItemSectionContinuation, SectionListContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, GridContinuation, PlaylistPanelContinuation, NavigateAction, ShowMiniplayerCommand, ReloadContinuationItemsCommand } from './continuations.js';
import {
Continuation, ItemSectionContinuation, SectionListContinuation,
LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation,
GridContinuation, PlaylistPanelContinuation, NavigateAction, ShowMiniplayerCommand,
ReloadContinuationItemsCommand
} from './continuations.js';
export type ParserError = {
classname: string,
@@ -289,6 +296,14 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
}
_clearMemo();
_createMemo();
const items = parse(data.items);
if (items) {
parsed_data.items = items;
parsed_data.items_memo = _getMemo();
}
_clearMemo();
applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
const continuation = data.continuation ? parseC(data.continuation) : null;
@@ -352,15 +367,21 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
parsed_data.playability_status = playability_status;
}
const streaming_data = data.streamingData ? {
expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000),
formats: parseFormats(data.streamingData.formats),
adaptive_formats: parseFormats(data.streamingData.adaptiveFormats),
dash_manifest_url: data.streamingData.dashManifestUrl || null,
hls_manifest_url: data.streamingData.hlsManifestUrl || null
} : undefined;
if (data.streamingData) {
// Currently each response with streaming data only has two n param values
// One for the adaptive formats and another for the combined formats
// As they are the same for a response, we only need to decipher them once
// For all futher deciphering calls on formats from that response, we can use the cached output, given the same input n param
const this_response_nsig_cache = new Map<string, string>();
const streaming_data = {
expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000),
formats: parseFormats(data.streamingData.formats, this_response_nsig_cache),
adaptive_formats: parseFormats(data.streamingData.adaptiveFormats, this_response_nsig_cache),
dash_manifest_url: data.streamingData.dashManifestUrl || null,
hls_manifest_url: data.streamingData.hlsManifestUrl || null
};
if (streaming_data) {
parsed_data.streaming_data = streaming_data;
}
@@ -404,20 +425,10 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
parsed_data.cards = cards;
}
const engagement_panels = data.engagementPanels?.map((e) => {
const item = parseItem(e, YTNodes.EngagementPanelSectionList) as YTNodes.EngagementPanelSectionList;
return item;
});
if (engagement_panels) {
const engagement_panels = parseArray(data.engagementPanels, EngagementPanelSectionList);
if (engagement_panels.length) {
parsed_data.engagement_panels = engagement_panels;
}
_createMemo();
const items = parse(data.items);
if (items) {
parsed_data.items = items;
parsed_data.items_memo = _getMemo();
}
_clearMemo();
return parsed_data;
}
@@ -429,7 +440,7 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
*/
export function parseItem<T extends YTNode, K extends YTNodeConstructor<T>[]>(data: RawNode | undefined, validTypes: K): InstanceType<K[number]> | null;
export function parseItem<T extends YTNode>(data: RawNode | undefined, validTypes: YTNodeConstructor<T>): T | null;
export function parseItem(data?: RawNode) : YTNode;
export function parseItem(data?: RawNode): YTNode;
export function parseItem(data?: RawNode, validTypes?: YTNodeConstructor | YTNodeConstructor[]) {
if (!data) return null;
@@ -593,8 +604,8 @@ export function parseActions(data: RawData) {
return new SuperParsedResult(parseItem(data));
}
export function parseFormats(formats: RawNode[]): Format[] {
return formats?.map((format) => new Format(format)) || [];
export function parseFormats(formats: RawNode[], this_response_nsig_cache: Map<string, string>): Format[] {
return formats?.map((format) => new Format(format, this_response_nsig_cache)) || [];
}
export function applyMutations(memo: Memo, mutations: RawNode[]) {

View File

@@ -20,6 +20,7 @@ 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';
export interface IParsedResponse {
actions?: SuperParsedResult<YTNode>;
actions_memo?: Memo;
@@ -63,7 +64,7 @@ export interface IParsedResponse {
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
endscreen?: Endscreen;
cards?: CardCollection;
engagement_panels?: EngagementPanelSectionList[];
engagement_panels?: ObservedArray<EngagementPanelSectionList>;
items?: SuperParsedResult<YTNode>;
}
@@ -106,7 +107,7 @@ export interface INextResponse {
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
on_response_received_endpoints_memo?: Memo;
player_overlays?: SuperParsedResult<YTNode>;
engagement_panels?: EngagementPanelSectionList[];
engagement_panels?: ObservedArray<EngagementPanelSectionList>;
}
export interface IBrowseResponse {
@@ -145,6 +146,11 @@ export interface IResolveURLResponse {
endpoint: NavigationEndpoint;
}
export interface IGetTranscriptResponse {
actions: SuperParsedResult<YTNode>;
actions_memo: Memo;
}
export interface IGetNotificationsMenuResponse {
actions: SuperParsedResult<YTNode>;
actions_memo: Memo;

View File

@@ -9,6 +9,7 @@ import PlaylistSidebarPrimaryInfo from '../classes/PlaylistSidebarPrimaryInfo.js
import PlaylistSidebarSecondaryInfo from '../classes/PlaylistSidebarSecondaryInfo.js';
import PlaylistVideoThumbnail from '../classes/PlaylistVideoThumbnail.js';
import VideoOwner from '../classes/VideoOwner.js';
import Alert from '../classes/Alert.js';
import { InnertubeError } from '../../utils/Utils.js';
import type { ObservedArray } from '../helpers.js';
@@ -17,7 +18,7 @@ import type Actions from '../../core/Actions.js';
import type { ApiResponse } from '../../core/Actions.js';
import type { IBrowseResponse } from '../types/ParsedResponse.js';
class Playlist extends Feed<IBrowseResponse> {
export default class Playlist extends Feed<IBrowseResponse> {
info;
menu;
endpoint?: NavigationEndpoint;
@@ -29,9 +30,13 @@ class Playlist extends Feed<IBrowseResponse> {
const header = this.memo.getType(PlaylistHeader).first();
const primary_info = this.memo.getType(PlaylistSidebarPrimaryInfo).first();
const secondary_info = this.memo.getType(PlaylistSidebarSecondaryInfo).first();
const alert = this.page.alerts?.firstOfType(Alert);
if (!primary_info && !secondary_info)
throw new InnertubeError('This playlist does not exist');
if (alert && alert.alert_type === 'ERROR')
throw new InnertubeError(alert.text.toString(), alert);
if (!primary_info && !secondary_info && Object.keys(this.page).length === 0)
throw new InnertubeError('Got empty continuation response. This is likely the end of the playlist.');
this.info = {
...this.page.metadata?.item().as(PlaylistMetadata),
@@ -69,6 +74,4 @@ class Playlist extends Feed<IBrowseResponse> {
throw new InnertubeError('Could not get continuation data');
return new Playlist(this.actions, page, true);
}
}
export default Playlist;
}

View File

@@ -0,0 +1,55 @@
import type Actions from '../../core/Actions.js';
import { type ApiResponse } from '../../core/Actions.js';
import type { IGetTranscriptResponse } from '../index.js';
import Parser from '../index.js';
import Transcript from '../classes/Transcript.js';
export default class TranscriptInfo {
#page: IGetTranscriptResponse;
#actions: Actions;
transcript: Transcript;
constructor(actions: Actions, response: ApiResponse) {
this.#page = Parser.parseResponse(response.data);
this.#actions = actions;
this.transcript = this.#page.actions_memo.getType(Transcript).first();
}
/**
* Selects a language from the language menu and returns the updated transcript.
* @param language - Language to select.
*/
async selectLanguage(language: string): Promise<TranscriptInfo> {
const target_menu_item = this.transcript.content?.footer?.language_menu?.sub_menu_items?.find((item) => item.title.toString() === language);
if (!target_menu_item)
throw new Error(`Language not found: ${language}`);
if (target_menu_item.selected)
return this;
const response = await this.#actions.execute('/get_transcript', {
params: target_menu_item.continuation
});
return new TranscriptInfo(this.#actions, response);
}
/**
* Returns available languages.
*/
get languages(): string[] {
return this.transcript.content?.footer?.language_menu?.sub_menu_items?.map((item) => item.title.toString()) || [];
}
/**
* Returns the currently selected language.
*/
get selectedLanguage(): string {
return this.transcript.content?.footer?.language_menu?.sub_menu_items?.find((item) => item.selected)?.title.toString() || '';
}
get page() {
return this.#page;
}
}

View File

@@ -15,17 +15,17 @@ import type { FetchFunction } from '../types/PlatformShim.js';
import path from 'path';
import os from 'os';
import fs from 'fs/promises';
import { readFileSync } from 'fs';
import CustomEvent from './polyfills/node-custom-event.js';
import { fileURLToPath } from 'url';
import evaluate from './jsruntime/jinter.js';
import { $INLINE_JSON } from 'ts-transformer-inline-file';
const meta_url = import.meta.url;
const is_cjs = !meta_url;
const __dirname__ = is_cjs ? __dirname : path.dirname(fileURLToPath(meta_url));
const package_json = JSON.parse(readFileSync(path.resolve(__dirname__, is_cjs ? '../package.json' : '../../package.json'), 'utf-8'));
const repo_url = package_json.homepage?.split('#')[0];
const { homepage, version, bugs } = $INLINE_JSON('../../package.json');
const repo_url = homepage?.split('#')[0];
class Cache implements ICache {
#persistent_directory: string;
@@ -101,8 +101,8 @@ class Cache implements ICache {
Platform.load({
runtime: 'node',
info: {
version: package_json.version,
bugs_url: package_json.bugs?.url || `${repo_url}/issues`,
version: version,
bugs_url: bugs?.url || `${repo_url}/issues`,
repo_url
},
server: true,

View File

@@ -334,18 +334,32 @@ export type EditPlaylistEndpointOptions = {
* The changes to make to the playlist.
*/
actions: {
action: 'ACTION_ADD_VIDEO' | 'ACTION_REMOVE_VIDEO' | 'ACTION_MOVE_VIDEO_AFTER';
action: 'ACTION_ADD_VIDEO' | 'ACTION_REMOVE_VIDEO' | 'ACTION_MOVE_VIDEO_AFTER' | 'ACTION_SET_PLAYLIST_DESCRIPTION' | 'ACTION_SET_PLAYLIST_NAME';
added_video_id?: string;
set_video_id?: string;
moved_set_video_id_predecessor?: string;
playlist_description?: string;
playlist_name?: string;
}[];
}
export interface IEditPlaylistRequest extends ObjectSnakeToCamel<EditPlaylistEndpointOptions> {
actions: {
action: 'ACTION_ADD_VIDEO' | 'ACTION_REMOVE_VIDEO' | 'ACTION_MOVE_VIDEO_AFTER';
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 BlocklistPickerRequestEndpointOptions = {
channel_id: string;
}
export type IBlocklistPickerRequest = {
blockedForKidsContent: {
external_channel_id: string;
}
}

View File

@@ -98,7 +98,10 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"plugins": [
{ "transform": "ts-transformer-inline-file/transformer" }
]
},
"include": [
"src/**/*.ts",