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:
LuanRT
2022-08-28 05:11:11 -03:00
committed by GitHub
parent 05b4593e0a
commit 13a86cb4e7
21 changed files with 349 additions and 151 deletions

View 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;

View 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;

View File

@@ -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

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);

View 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;