mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 01:22:11 +00:00
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>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 |
|
||||
23
examples/blockchannel/index.js
Normal file
23
examples/blockchannel/index.js
Normal 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.');
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
12
src/core/endpoints/kids/BlocklistPickerEndpoint.ts
Normal file
12
src/core/endpoints/kids/BlocklistPickerEndpoint.ts
Normal 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 } };
|
||||
}
|
||||
1
src/core/endpoints/kids/index.ts
Normal file
1
src/core/endpoints/kids/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as BlocklistPickerEndpoint from './BlocklistPickerEndpoint.js';
|
||||
@@ -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 ||
|
||||
|
||||
22
src/parser/classes/ytkids/KidsBlocklistPicker.ts
Normal file
22
src/parser/classes/ytkids/KidsBlocklistPicker.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
49
src/parser/classes/ytkids/KidsBlocklistPickerItem.ts
Normal file
49
src/parser/classes/ytkids/KidsBlocklistPickerItem.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -352,4 +352,14 @@ export interface IEditPlaylistRequest extends ObjectSnakeToCamel<EditPlaylistEnd
|
||||
playlistDescription?: string;
|
||||
playlistName?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type BlocklistPickerRequestEndpointOptions = {
|
||||
channel_id: string;
|
||||
}
|
||||
|
||||
export type IBlocklistPickerRequest = {
|
||||
blockedForKidsContent: {
|
||||
external_channel_id: string;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user