diff --git a/README.md b/README.md
index daddbb69..b9434b60 100644
--- a/README.md
+++ b/README.md
@@ -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).
diff --git a/deno/package.json b/deno/package.json
index 98d7d988..39505cd1 100644
--- a/deno/package.json
+++ b/deno/package.json
@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
- "version": "6.4.1",
+ "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 \".ts';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'./DashManifest.js';\" \"'./DashManifest.js';\" ./deno -r && npx replace \"'https://esm.sh/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": {
diff --git a/deno/src/core/clients/Kids.ts b/deno/src/core/clients/Kids.ts
index ddd8ddbe..2418e858 100644
--- a/deno/src/core/clients/Kids.ts
+++ b/deno/src/core/clients/Kids.ts
@@ -1,16 +1,22 @@
+import Parser from '../../parser/index.ts';
import Channel from '../../parser/ytkids/Channel.ts';
import HomeFeed from '../../parser/ytkids/HomeFeed.ts';
import Search from '../../parser/ytkids/Search.ts';
import VideoInfo from '../../parser/ytkids/VideoInfo.ts';
import type Session from '../Session.ts';
+import { type ApiResponse } from '../Actions.ts';
-import { generateRandomString } from '../../utils/Utils.ts';
+import { InnertubeError, generateRandomString } from '../../utils/Utils.ts';
import {
BrowseEndpoint, NextEndpoint,
PlayerEndpoint, SearchEndpoint
} from '../endpoints/index.ts';
+import { BlocklistPickerEndpoint } from '../endpoints/kids/index.ts';
+
+import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.ts';
+
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 {
+ 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;
+ }
}
\ No newline at end of file
diff --git a/deno/src/core/clients/Music.ts b/deno/src/core/clients/Music.ts
index 0d22d229..de1faa9b 100644
--- a/deno/src/core/clients/Music.ts
+++ b/deno/src/core/clients/Music.ts
@@ -20,7 +20,7 @@ import SectionList from '../../parser/classes/SectionList.ts';
import Tab from '../../parser/classes/Tab.ts';
import * as Proto from '../../proto/index.ts';
-import type { ObservedArray, YTNode } from '../../parser/helpers.ts';
+import type { ObservedArray } from '../../parser/helpers.ts';
import type { MusicSearchFilters } from '../../types/index.ts';
import { InnertubeError, generateRandomString, throwIfMissing } from '../../utils/Utils.ts';
import type Actions from '../Actions.ts';
@@ -355,17 +355,17 @@ export default class Music {
* Retrieves search suggestions for the given query.
* @param query - The query.
*/
- async getSearchSuggestions(query: string): Promise> {
+ async getSearchSuggestions(query: string): Promise> {
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;
- 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;
}
}
\ No newline at end of file
diff --git a/deno/src/core/endpoints/index.ts b/deno/src/core/endpoints/index.ts
index e8ff4e2d..8e64dd6e 100644
--- a/deno/src/core/endpoints/index.ts
+++ b/deno/src/core/endpoints/index.ts
@@ -15,4 +15,5 @@ export * as Music from './music/index.ts';
export * as Notification from './notification/index.ts';
export * as Playlist from './playlist/index.ts';
export * as Subscription from './subscription/index.ts';
-export * as Upload from './upload/index.ts';
\ No newline at end of file
+export * as Upload from './upload/index.ts';
+export * as Kids from './kids/index.ts';
\ No newline at end of file
diff --git a/deno/src/core/endpoints/kids/BlocklistPickerEndpoint.ts b/deno/src/core/endpoints/kids/BlocklistPickerEndpoint.ts
new file mode 100644
index 00000000..ca0d6f7c
--- /dev/null
+++ b/deno/src/core/endpoints/kids/BlocklistPickerEndpoint.ts
@@ -0,0 +1,12 @@
+import type { IBlocklistPickerRequest, BlocklistPickerRequestEndpointOptions } from '../../../types/index.ts';
+
+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 } };
+}
\ No newline at end of file
diff --git a/deno/src/core/endpoints/kids/index.ts b/deno/src/core/endpoints/kids/index.ts
new file mode 100644
index 00000000..570c3d6f
--- /dev/null
+++ b/deno/src/core/endpoints/kids/index.ts
@@ -0,0 +1 @@
+export * as BlocklistPickerEndpoint from './BlocklistPickerEndpoint.ts';
\ No newline at end of file
diff --git a/deno/src/core/mixins/MediaInfo.ts b/deno/src/core/mixins/MediaInfo.ts
index 63001406..cb7f57eb 100644
--- a/deno/src/core/mixins/MediaInfo.ts
+++ b/deno/src/core/mixins/MediaInfo.ts
@@ -46,10 +46,16 @@ export default class MediaInfo {
* @returns DASH manifest
*/
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise {
+ 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);
@@ -83,6 +89,12 @@ export default class MediaInfo {
* @param options - Download options.
*/
async download(options: DownloadOptions = {}): Promise> {
+ 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);
}
diff --git a/deno/src/parser/classes/PlayerControlsOverlay.ts b/deno/src/parser/classes/PlayerControlsOverlay.ts
new file mode 100644
index 00000000..d67319b0
--- /dev/null
+++ b/deno/src/parser/classes/PlayerControlsOverlay.ts
@@ -0,0 +1,14 @@
+import { YTNode } from '../helpers.ts';
+import { Parser, type RawNode } from '../index.ts';
+import PlayerOverflow from './PlayerOverflow.ts';
+
+export default class PlayerControlsOverlay extends YTNode {
+ static type = 'PlayerControlsOverlay';
+
+ overflow: PlayerOverflow | null;
+
+ constructor(data: RawNode) {
+ super();
+ this.overflow = Parser.parseItem(data.overflow, PlayerOverflow);
+ }
+}
\ No newline at end of file
diff --git a/deno/src/parser/classes/PlayerOverflow.ts b/deno/src/parser/classes/PlayerOverflow.ts
new file mode 100644
index 00000000..0053fde0
--- /dev/null
+++ b/deno/src/parser/classes/PlayerOverflow.ts
@@ -0,0 +1,16 @@
+import { YTNode } from '../helpers.ts';
+import type { RawNode } from '../index.ts';
+import NavigationEndpoint from './NavigationEndpoint.ts';
+
+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;
+ }
+}
\ No newline at end of file
diff --git a/deno/src/parser/classes/ToggleButton.ts b/deno/src/parser/classes/ToggleButton.ts
index ea28107b..e39b00be 100644
--- a/deno/src/parser/classes/ToggleButton.ts
+++ b/deno/src/parser/classes/ToggleButton.ts
@@ -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 ||
diff --git a/deno/src/parser/classes/livechat/UpdateViewershipAction.ts b/deno/src/parser/classes/livechat/UpdateViewershipAction.ts
index 0294e552..5f667457 100644
--- a/deno/src/parser/classes/livechat/UpdateViewershipAction.ts
+++ b/deno/src/parser/classes/livechat/UpdateViewershipAction.ts
@@ -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;
}
}
\ No newline at end of file
diff --git a/deno/src/parser/classes/ytkids/KidsBlocklistPicker.ts b/deno/src/parser/classes/ytkids/KidsBlocklistPicker.ts
new file mode 100644
index 00000000..0a00f95d
--- /dev/null
+++ b/deno/src/parser/classes/ytkids/KidsBlocklistPicker.ts
@@ -0,0 +1,22 @@
+import Text from '../misc/Text.ts';
+import { YTNode } from '../../helpers.ts';
+import Button from '../Button.ts';
+import Parser, { type RawNode } from '../../index.ts';
+import KidsBlocklistPickerItem from './KidsBlocklistPickerItem.ts';
+
+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);
+ }
+}
\ No newline at end of file
diff --git a/deno/src/parser/classes/ytkids/KidsBlocklistPickerItem.ts b/deno/src/parser/classes/ytkids/KidsBlocklistPickerItem.ts
new file mode 100644
index 00000000..99379b5a
--- /dev/null
+++ b/deno/src/parser/classes/ytkids/KidsBlocklistPickerItem.ts
@@ -0,0 +1,49 @@
+import Text from '../misc/Text.ts';
+import { YTNode } from '../../helpers.ts';
+import Parser, { type RawNode } from '../../index.ts';
+import ToggleButton from '../ToggleButton.ts';
+import Thumbnail from '../misc/Thumbnail.ts';
+import type Actions from '../../../core/Actions.ts';
+import { InnertubeError } from '../../../utils/Utils.ts';
+import { type ApiResponse } from '../../../core/Actions.ts';
+
+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 {
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/deno/src/parser/nodes.ts b/deno/src/parser/nodes.ts
index 47aeda90..1081ec70 100644
--- a/deno/src/parser/nodes.ts
+++ b/deno/src/parser/nodes.ts
@@ -252,11 +252,13 @@ export { default as PageHeaderView } from './classes/PageHeaderView.ts';
export { default as PageIntroduction } from './classes/PageIntroduction.ts';
export { default as PlayerAnnotationsExpanded } from './classes/PlayerAnnotationsExpanded.ts';
export { default as PlayerCaptionsTracklist } from './classes/PlayerCaptionsTracklist.ts';
+export { default as PlayerControlsOverlay } from './classes/PlayerControlsOverlay.ts';
export { default as PlayerErrorMessage } from './classes/PlayerErrorMessage.ts';
export { default as PlayerLegacyDesktopYpcOffer } from './classes/PlayerLegacyDesktopYpcOffer.ts';
export { default as PlayerLegacyDesktopYpcTrailer } from './classes/PlayerLegacyDesktopYpcTrailer.ts';
export { default as PlayerLiveStoryboardSpec } from './classes/PlayerLiveStoryboardSpec.ts';
export { default as PlayerMicroformat } from './classes/PlayerMicroformat.ts';
+export { default as PlayerOverflow } from './classes/PlayerOverflow.ts';
export { default as PlayerOverlay } from './classes/PlayerOverlay.ts';
export { default as PlayerOverlayAutoplay } from './classes/PlayerOverlayAutoplay.ts';
export { default as PlayerStoryboardSpec } from './classes/PlayerStoryboardSpec.ts';
@@ -391,6 +393,8 @@ export { default as WatchNextEndScreen } from './classes/WatchNextEndScreen.ts';
export { default as WatchNextTabbedResults } from './classes/WatchNextTabbedResults.ts';
export { default as YpcTrailer } from './classes/YpcTrailer.ts';
export { default as AnchoredSection } from './classes/ytkids/AnchoredSection.ts';
+export { default as KidsBlocklistPicker } from './classes/ytkids/KidsBlocklistPicker.ts';
+export { default as KidsBlocklistPickerItem } from './classes/ytkids/KidsBlocklistPickerItem.ts';
export { default as KidsCategoriesHeader } from './classes/ytkids/KidsCategoriesHeader.ts';
export { default as KidsCategoryTab } from './classes/ytkids/KidsCategoryTab.ts';
export { default as KidsHomeScreen } from './classes/ytkids/KidsHomeScreen.ts';
diff --git a/deno/src/platform/node.ts b/deno/src/platform/node.ts
index 29b63d52..3086840d 100644
--- a/deno/src/platform/node.ts
+++ b/deno/src/platform/node.ts
@@ -15,17 +15,17 @@ import type { FetchFunction } from '../types/PlatformShim.ts';
import path from 'path';
import os from 'os';
import fs from 'fs/promises';
-import { readFileSync } from 'fs';
import CustomEvent from './polyfills/node-custom-event.ts';
import { fileURLToPath } from 'url';
import evaluate from './jsruntime/jinter.ts';
+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,
diff --git a/deno/src/types/Endpoints.ts b/deno/src/types/Endpoints.ts
index b732373e..7b14eeb0 100644
--- a/deno/src/types/Endpoints.ts
+++ b/deno/src/types/Endpoints.ts
@@ -352,4 +352,14 @@ export interface IEditPlaylistRequest extends ObjectSnakeToCamel