mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-19 12:31:17 +00:00
feat: add settings page parser (#154)
* feat: add settings page parsers * fix(AccountManager): small ts error * feat: add `CopyLink` & `SettingsCheckbox` * deps: remove “flat” dependency
This commit is contained in:
24
src/parser/classes/ChannelOptions.ts
Normal file
24
src/parser/classes/ChannelOptions.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class ChannelOptions extends YTNode {
|
||||
static type = 'ChannelOptions';
|
||||
|
||||
avatar: Thumbnail[];
|
||||
endpoint: NavigationEndpoint;
|
||||
name: string;
|
||||
links: Text[];
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
this.endpoint = new NavigationEndpoint(data.avatarEndpoint);
|
||||
this.name = data.name;
|
||||
this.links = data.links.map((link: any) => new Text(link));
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelOptions;
|
||||
20
src/parser/classes/CopyLink.ts
Normal file
20
src/parser/classes/CopyLink.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import Parser from '../index';
|
||||
import Button from './Button';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class CopyLink extends YTNode {
|
||||
static type = 'CopyLink';
|
||||
|
||||
copy_button: Button | null;
|
||||
short_url: string;
|
||||
style: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.copy_button = Parser.parseItem<Button>(data.copyButton, Button);
|
||||
this.short_url = data.shortUrl;
|
||||
this.style = data.style;
|
||||
}
|
||||
}
|
||||
|
||||
export default CopyLink;
|
||||
@@ -235,29 +235,14 @@ class NavigationEndpoint extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call endpoint. (This is an experiment and may replace {@link call} in the future.).
|
||||
*/
|
||||
async callTest(actions: Actions, args: {
|
||||
parse: false;
|
||||
params?: object
|
||||
}): Promise<ActionsResponse>;
|
||||
async callTest(actions: Actions, args?: {
|
||||
parse?: true;
|
||||
params?: object
|
||||
}): Promise<ParsedResponse>;
|
||||
async callTest(actions: Actions, args: {
|
||||
parse?: boolean;
|
||||
params?: object
|
||||
} = { parse: true, params: {} }): Promise<ParsedResponse | ActionsResponse> {
|
||||
callTest(actions: Actions, args: { [ key: string ]: any; parse: true }): Promise<ParsedResponse>;
|
||||
callTest(actions: Actions, args?: { [ key: string ]: any; parse?: false }): Promise<ActionsResponse>;
|
||||
callTest(actions: Actions, args?: { [ key: string ]: any; parse?: boolean }): Promise<ParsedResponse | ActionsResponse> {
|
||||
if (!actions)
|
||||
throw new Error('An active caller must be provided');
|
||||
if (!this.metadata.api_url)
|
||||
throw new Error('Expected an api_url, but none was found, this is a bug.');
|
||||
|
||||
const response = await actions.execute(this.metadata.api_url, { ...this.payload, ...args.params, parse: args.parse });
|
||||
|
||||
return response;
|
||||
return actions.execute(this.metadata.api_url, { ...this.payload, ...args });
|
||||
}
|
||||
|
||||
// TODO: replace client with an enum or something
|
||||
|
||||
21
src/parser/classes/PageIntroduction.ts
Normal file
21
src/parser/classes/PageIntroduction.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class PageIntroduction extends YTNode {
|
||||
static type = 'PageIntroduction';
|
||||
|
||||
header_text: string;
|
||||
body_text: string;
|
||||
page_title: string;
|
||||
header_icon_type: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.header_text = new Text(data.headerText).toString();
|
||||
this.body_text = new Text(data.bodyText).toString();
|
||||
this.page_title = new Text(data.pageTitle).toString();
|
||||
this.header_icon_type = data.headerIcon.iconType;
|
||||
}
|
||||
}
|
||||
|
||||
export default PageIntroduction;
|
||||
23
src/parser/classes/SettingsCheckbox.ts
Normal file
23
src/parser/classes/SettingsCheckbox.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class SettingsCheckbox extends YTNode {
|
||||
static type = 'SettingsCheckbox';
|
||||
|
||||
title: Text;
|
||||
help_text: Text;
|
||||
enabled: boolean;
|
||||
disabled: boolean;
|
||||
id: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.help_text = new Text(data.helpText);
|
||||
this.enabled = data.enabled;
|
||||
this.disabled = data.disabled;
|
||||
this.id = data.id;
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsCheckbox;
|
||||
36
src/parser/classes/SettingsOptions.ts
Normal file
36
src/parser/classes/SettingsOptions.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import Parser from '..';
|
||||
|
||||
import Text from './misc/Text';
|
||||
import Dropdown from './Dropdown';
|
||||
import SettingsSwitch from './SettingsSwitch';
|
||||
import SettingsCheckbox from './SettingsCheckbox';
|
||||
import ChannelOptions from './ChannelOptions';
|
||||
import CopyLink from './CopyLink';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class SettingsOptions extends YTNode {
|
||||
static type = 'SettingsOptions';
|
||||
|
||||
title: Text;
|
||||
text?: string;
|
||||
options?;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
|
||||
if (Reflect.has(data, 'text')) {
|
||||
this.text = new Text(data.text).toString();
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'options')) {
|
||||
this.options = Parser.parseArray<SettingsSwitch | Dropdown | CopyLink | SettingsCheckbox | ChannelOptions>(data.options, [
|
||||
SettingsSwitch, Dropdown, CopyLink,
|
||||
SettingsCheckbox, ChannelOptions
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsOptions;
|
||||
23
src/parser/classes/SettingsSidebar.ts
Normal file
23
src/parser/classes/SettingsSidebar.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import CompactLink from './CompactLink';
|
||||
import { ObservedArray, YTNode } from '../helpers';
|
||||
|
||||
class SettingsSidebar extends YTNode {
|
||||
static type = 'SettingsSidebar';
|
||||
|
||||
title: Text;
|
||||
items: ObservedArray<CompactLink>;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.items = Parser.parseArray<CompactLink>(data.items, CompactLink);
|
||||
}
|
||||
|
||||
get contents() {
|
||||
return this.items;
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsSidebar;
|
||||
24
src/parser/classes/SettingsSwitch.ts
Normal file
24
src/parser/classes/SettingsSwitch.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Text from './misc/Text';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class SettingsSwitch extends YTNode {
|
||||
static type = 'SettingsSwitch';
|
||||
|
||||
title: Text;
|
||||
subtitle: Text;
|
||||
enabled: boolean;
|
||||
enable_endpoint: NavigationEndpoint;
|
||||
disable_endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.subtitle = new Text(data.subtitle);
|
||||
this.enabled = data.enabled;
|
||||
this.enable_endpoint = new NavigationEndpoint(data.enableServiceEndpoint);
|
||||
this.disable_endpoint = new NavigationEndpoint(data.disableServiceEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsSwitch;
|
||||
@@ -122,9 +122,7 @@ class Comment extends YTNode {
|
||||
const dialog_button = dialog.item().as(CommentReplyDialog).reply_button.item().as(ToggleButton);
|
||||
|
||||
const payload = {
|
||||
params: {
|
||||
commentText: text
|
||||
}
|
||||
commentText: text
|
||||
};
|
||||
|
||||
const response = await dialog_button.endpoint.callTest(this.#actions, payload);
|
||||
|
||||
@@ -35,7 +35,7 @@ class CommentThread extends YTNode {
|
||||
throw new InnertubeError('This comment has no replies.', { comment_id: this.comment?.comment_id });
|
||||
|
||||
const continuation = this.#replies.key('contents').parsed().array().get({ type: 'ContinuationItem' })?.as(ContinuationItem);
|
||||
const response = await continuation?.endpoint.callTest(this.#actions);
|
||||
const response = await continuation?.endpoint.callTest(this.#actions, { parse: true });
|
||||
|
||||
this.replies = response?.on_response_received_endpoints_memo?.getType(Comment).map((comment) => {
|
||||
comment.setActions(this.#actions);
|
||||
@@ -60,7 +60,7 @@ class CommentThread extends YTNode {
|
||||
if (!this.#actions)
|
||||
throw new InnertubeError('Actions not set for this CommentThread.');
|
||||
|
||||
const response = await this.#continuation.button?.item().key('endpoint').nodeOfType(NavigationEndpoint).callTest(this.#actions);
|
||||
const response = await this.#continuation.button?.item().key('endpoint').nodeOfType(NavigationEndpoint).callTest(this.#actions, { parse: true });
|
||||
|
||||
this.replies = response?.on_response_received_endpoints_memo.getType(Comment).map((comment) => {
|
||||
comment.setActions(this.#actions);
|
||||
|
||||
@@ -32,6 +32,7 @@ import { default as ChannelFeaturedContent } from './classes/ChannelFeaturedCont
|
||||
import { default as ChannelHeaderLinks } from './classes/ChannelHeaderLinks';
|
||||
import { default as ChannelMetadata } from './classes/ChannelMetadata';
|
||||
import { default as ChannelMobileHeader } from './classes/ChannelMobileHeader';
|
||||
import { default as ChannelOptions } from './classes/ChannelOptions';
|
||||
import { default as ChannelThumbnailWithLink } from './classes/ChannelThumbnailWithLink';
|
||||
import { default as ChannelVideoPlayer } from './classes/ChannelVideoPlayer';
|
||||
import { default as ChildVideo } from './classes/ChildVideo';
|
||||
@@ -52,6 +53,7 @@ import { default as CompactMix } from './classes/CompactMix';
|
||||
import { default as CompactPlaylist } from './classes/CompactPlaylist';
|
||||
import { default as CompactVideo } from './classes/CompactVideo';
|
||||
import { default as ContinuationItem } from './classes/ContinuationItem';
|
||||
import { default as CopyLink } from './classes/CopyLink';
|
||||
import { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog';
|
||||
import { default as DidYouMean } from './classes/DidYouMean';
|
||||
import { default as DownloadButton } from './classes/DownloadButton';
|
||||
@@ -162,6 +164,7 @@ import { default as MusicThumbnail } from './classes/MusicThumbnail';
|
||||
import { default as MusicTwoRowItem } from './classes/MusicTwoRowItem';
|
||||
import { default as NavigationEndpoint } from './classes/NavigationEndpoint';
|
||||
import { default as Notification } from './classes/Notification';
|
||||
import { default as PageIntroduction } from './classes/PageIntroduction';
|
||||
import { default as PlayerAnnotationsExpanded } from './classes/PlayerAnnotationsExpanded';
|
||||
import { default as PlayerCaptionsTracklist } from './classes/PlayerCaptionsTracklist';
|
||||
import { default as PlayerErrorMessage } from './classes/PlayerErrorMessage';
|
||||
@@ -203,6 +206,10 @@ import { default as SearchSuggestionsSection } from './classes/SearchSuggestions
|
||||
import { default as SecondarySearchContainer } from './classes/SecondarySearchContainer';
|
||||
import { default as SectionList } from './classes/SectionList';
|
||||
import { default as SettingBoolean } from './classes/SettingBoolean';
|
||||
import { default as SettingsCheckbox } from './classes/SettingsCheckbox';
|
||||
import { default as SettingsOptions } from './classes/SettingsOptions';
|
||||
import { default as SettingsSidebar } from './classes/SettingsSidebar';
|
||||
import { default as SettingsSwitch } from './classes/SettingsSwitch';
|
||||
import { default as Shelf } from './classes/Shelf';
|
||||
import { default as ShowingResultsFor } from './classes/ShowingResultsFor';
|
||||
import { default as SimpleCardContent } from './classes/SimpleCardContent';
|
||||
@@ -285,6 +292,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
ChannelHeaderLinks,
|
||||
ChannelMetadata,
|
||||
ChannelMobileHeader,
|
||||
ChannelOptions,
|
||||
ChannelThumbnailWithLink,
|
||||
ChannelVideoPlayer,
|
||||
ChildVideo,
|
||||
@@ -305,6 +313,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
CompactPlaylist,
|
||||
CompactVideo,
|
||||
ContinuationItem,
|
||||
CopyLink,
|
||||
CreatePlaylistDialog,
|
||||
DidYouMean,
|
||||
DownloadButton,
|
||||
@@ -415,6 +424,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MusicTwoRowItem,
|
||||
NavigationEndpoint,
|
||||
Notification,
|
||||
PageIntroduction,
|
||||
PlayerAnnotationsExpanded,
|
||||
PlayerCaptionsTracklist,
|
||||
PlayerErrorMessage,
|
||||
@@ -456,6 +466,10 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
SecondarySearchContainer,
|
||||
SectionList,
|
||||
SettingBoolean,
|
||||
SettingsCheckbox,
|
||||
SettingsOptions,
|
||||
SettingsSidebar,
|
||||
SettingsSwitch,
|
||||
Shelf,
|
||||
ShowingResultsFor,
|
||||
SimpleCardContent,
|
||||
|
||||
@@ -51,10 +51,7 @@ class Comments {
|
||||
throw new InnertubeError('Could not find target button.');
|
||||
|
||||
const response = await button.endpoint.callTest(this.#actions, {
|
||||
params: {
|
||||
commentText: text
|
||||
},
|
||||
parse: false
|
||||
commentText: text
|
||||
});
|
||||
|
||||
return response;
|
||||
@@ -67,7 +64,7 @@ class Comments {
|
||||
if (!this.#continuation)
|
||||
throw new InnertubeError('Continuation not found');
|
||||
|
||||
const data = await this.#continuation.endpoint.callTest(this.#actions);
|
||||
const data = await this.#continuation.endpoint.callTest(this.#actions, { parse: true });
|
||||
|
||||
// Copy the previous page so we can keep the header.
|
||||
const page = Object.assign({}, this.#page);
|
||||
|
||||
118
src/parser/youtube/Settings.ts
Normal file
118
src/parser/youtube/Settings.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import Parser from '..';
|
||||
import Actions, { AxioslikeResponse } from '../../core/Actions';
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
|
||||
import Tab from '../classes/Tab';
|
||||
import TwoColumnBrowseResults from '../classes/TwoColumnBrowseResults';
|
||||
import SectionList from '../classes/SectionList';
|
||||
import ItemSection from '../classes/ItemSection';
|
||||
|
||||
import PageIntroduction from '../classes/PageIntroduction';
|
||||
import SettingsOptions from '../classes/SettingsOptions';
|
||||
import SettingsSwitch from '../classes/SettingsSwitch';
|
||||
import SettingsSidebar from '../classes/SettingsSidebar';
|
||||
|
||||
class Settings {
|
||||
#page;
|
||||
#actions;
|
||||
|
||||
sidebar: SettingsSidebar | null | undefined;
|
||||
introduction: PageIntroduction | null | undefined;
|
||||
sections;
|
||||
|
||||
constructor(actions: Actions, response: AxioslikeResponse) {
|
||||
this.#actions = actions;
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
|
||||
this.sidebar = this.#page.sidebar?.as(SettingsSidebar);
|
||||
|
||||
const tab = this.#page.contents.item().as(TwoColumnBrowseResults).tabs.array().as(Tab).get({ selected: true });
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Target tab not found');
|
||||
|
||||
const contents = tab.content?.as(SectionList).contents.array().as(ItemSection);
|
||||
|
||||
this.introduction = contents?.shift()?.contents?.get({ type: 'PageIntroduction' })?.as(PageIntroduction);
|
||||
|
||||
this.sections = contents?.map((el: ItemSection) => ({
|
||||
title: el.header?.title.toString() || null,
|
||||
contents: el.contents
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an item from the sidebar menu. Use {@link sidebar_items} to see available items.
|
||||
*/
|
||||
async selectSidebarItem(name: string) {
|
||||
if (!this.sidebar)
|
||||
throw new InnertubeError('Sidebar not available');
|
||||
|
||||
const item = this.sidebar.items.get({ title: name });
|
||||
|
||||
if (!item)
|
||||
throw new InnertubeError(`Item "${name}" not found`, { available_items: this.sidebar_items });
|
||||
|
||||
const response = await item.endpoint.callTest(this.#actions, { parse: false });
|
||||
|
||||
return new Settings(this.#actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a setting by name and returns it. Use {@link setting_options} to see available options.
|
||||
*/
|
||||
getSettingOption(name: string) {
|
||||
if (!this.sections)
|
||||
throw new InnertubeError('Sections not available');
|
||||
|
||||
for (const section of this.sections) {
|
||||
if (!section.contents) continue;
|
||||
for (const el of section.contents) {
|
||||
const options = el.as(SettingsOptions).options;
|
||||
if (options) {
|
||||
for (const option of options) {
|
||||
if (
|
||||
option.is(SettingsSwitch) &&
|
||||
option.title?.toString() === name
|
||||
)
|
||||
return option;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InnertubeError(`Option "${name}" not found`, { available_options: this.setting_options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns settings available in the page.
|
||||
*/
|
||||
get setting_options(): string[] {
|
||||
if (!this.sections)
|
||||
throw new InnertubeError('Sections not available');
|
||||
|
||||
let options: any[] = [];
|
||||
|
||||
for (const section of this.sections) {
|
||||
if (!section.contents) continue;
|
||||
for (const el of section.contents) {
|
||||
if (el.as(SettingsOptions).options)
|
||||
options = options.concat(el.as(SettingsOptions).options);
|
||||
}
|
||||
}
|
||||
|
||||
return options.map((opt) => opt.title?.toString()).filter((el) => el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns options available in the sidebar.
|
||||
*/
|
||||
get sidebar_items(): string[] {
|
||||
if (!this.sidebar)
|
||||
throw new InnertubeError('Sidebar not available');
|
||||
|
||||
return this.sidebar.items.map((item) => item.title.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
Reference in New Issue
Block a user