diff --git a/docs/API/interaction-manager.md b/docs/API/interaction-manager.md
index 43a2db33f..a13df4e93 100644
--- a/docs/API/interaction-manager.md
+++ b/docs/API/interaction-manager.md
@@ -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)
### like(video_id)
diff --git a/docs/API/kids.md b/docs/API/kids.md
index 4a02e6f52..df2ea2dcd 100644
--- a/docs/API/kids.md
+++ b/docs/API/kids.md
@@ -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)
### search(query)
@@ -110,4 +111,17 @@ Retrieves the home feed.
- Returns available categories.
- `#page`
- - Returns the original InnerTube response(s), parsed and sanitized.
\ No newline at end of file
+ - Returns the original InnerTube response(s), parsed and sanitized.
+
+
+
+
+### 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.`
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channel_id | `string` | Channel id |
\ No newline at end of file
diff --git a/examples/blockchannel/index.js b/examples/blockchannel/index.js
new file mode 100644
index 000000000..be9afc1e2
--- /dev/null
+++ b/examples/blockchannel/index.js
@@ -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.');
+})();
\ No newline at end of file
diff --git a/src/core/clients/Kids.ts b/src/core/clients/Kids.ts
index 3f58a802a..5981f3ef3 100644
--- a/src/core/clients/Kids.ts
+++ b/src/core/clients/Kids.ts
@@ -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 {
+ 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/src/core/endpoints/index.ts b/src/core/endpoints/index.ts
index 8837334c0..ad8599602 100644
--- a/src/core/endpoints/index.ts
+++ b/src/core/endpoints/index.ts
@@ -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';
\ No newline at end of file
+export * as Upload from './upload/index.js';
+export * as Kids from './kids/index.js';
\ No newline at end of file
diff --git a/src/core/endpoints/kids/BlocklistPickerEndpoint.ts b/src/core/endpoints/kids/BlocklistPickerEndpoint.ts
new file mode 100644
index 000000000..06f0678ab
--- /dev/null
+++ b/src/core/endpoints/kids/BlocklistPickerEndpoint.ts
@@ -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 } };
+}
\ No newline at end of file
diff --git a/src/core/endpoints/kids/index.ts b/src/core/endpoints/kids/index.ts
new file mode 100644
index 000000000..04b9ed5eb
--- /dev/null
+++ b/src/core/endpoints/kids/index.ts
@@ -0,0 +1 @@
+export * as BlocklistPickerEndpoint from './BlocklistPickerEndpoint.js';
\ No newline at end of file
diff --git a/src/parser/classes/ToggleButton.ts b/src/parser/classes/ToggleButton.ts
index f8a3b8162..f0a9b8ac1 100644
--- a/src/parser/classes/ToggleButton.ts
+++ b/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/src/parser/classes/ytkids/KidsBlocklistPicker.ts b/src/parser/classes/ytkids/KidsBlocklistPicker.ts
new file mode 100644
index 000000000..51e29c661
--- /dev/null
+++ b/src/parser/classes/ytkids/KidsBlocklistPicker.ts
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/parser/classes/ytkids/KidsBlocklistPickerItem.ts b/src/parser/classes/ytkids/KidsBlocklistPickerItem.ts
new file mode 100644
index 000000000..0933bfe07
--- /dev/null
+++ b/src/parser/classes/ytkids/KidsBlocklistPickerItem.ts
@@ -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 {
+ 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/src/parser/nodes.ts b/src/parser/nodes.ts
index ac04857ba..afa97a91d 100644
--- a/src/parser/nodes.ts
+++ b/src/parser/nodes.ts
@@ -393,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';
diff --git a/src/types/Endpoints.ts b/src/types/Endpoints.ts
index 1de4de848..124d6ec5e 100644
--- a/src/types/Endpoints.ts
+++ b/src/types/Endpoints.ts
@@ -352,4 +352,14 @@ export interface IEditPlaylistRequest extends ObjectSnakeToCamel