mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-28 09:06:51 +00:00
feat: add MusicSortFilterButton (#151)
This commit is contained in:
@@ -19,7 +19,7 @@ class MusicDetailHeader extends YTNode {
|
||||
badges;
|
||||
author?: {
|
||||
name: string;
|
||||
channel_id: string;
|
||||
channel_id: string | undefined;
|
||||
endpoint: NavigationEndpoint | undefined;
|
||||
};
|
||||
menu;
|
||||
|
||||
@@ -13,6 +13,7 @@ class MusicShelf extends YTNode {
|
||||
endpoint: NavigationEndpoint | null;
|
||||
continuation: string | null;
|
||||
bottom_text: Text | null;
|
||||
subheaders?: Array<any>;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -24,6 +25,9 @@ class MusicShelf extends YTNode {
|
||||
data.continuations?.[0].nextContinuationData?.continuation ||
|
||||
data.continuations?.[0].reloadContinuationData?.continuation || null;
|
||||
this.bottom_text = Reflect.has(data, 'bottomText') ? new Text(data.bottomText) : null;
|
||||
if (data.subheaders) {
|
||||
this.subheaders = Parser.parseArray(data.subheaders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
src/parser/classes/MusicSideAlignedItem.ts
Normal file
19
src/parser/classes/MusicSideAlignedItem.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Parser from '../index';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MusicSideAlignedItem extends YTNode {
|
||||
static type = 'MusicSideAlignedItem';
|
||||
|
||||
start_items?;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
if (data.startItems) {
|
||||
this.start_items = Parser.parseArray(data.startItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicSideAlignedItem;
|
||||
23
src/parser/classes/MusicSortFilterButton.ts
Normal file
23
src/parser/classes/MusicSortFilterButton.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Parser from '../index';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import MusicMultiSelectMenu from './menus/MusicMultiSelectMenu';
|
||||
import Text from './misc/Text';
|
||||
|
||||
class MusicSortFilterButton extends YTNode {
|
||||
static type = 'MusicSortFilterButton';
|
||||
|
||||
title: string;
|
||||
icon_type: string;
|
||||
menu: MusicMultiSelectMenu | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.title = new Text(data.title).text;
|
||||
this.icon_type = data.icon?.icon_type || null;
|
||||
this.menu = Parser.parseItem(data.menu, MusicMultiSelectMenu);
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicSortFilterButton;
|
||||
@@ -25,13 +25,13 @@ class MusicTwoRowItem extends YTNode {
|
||||
|
||||
artists?: {
|
||||
name: string;
|
||||
channel_id: string;
|
||||
channel_id: string | undefined;
|
||||
endpoint: NavigationEndpoint | undefined;
|
||||
}[];
|
||||
|
||||
author?: {
|
||||
name: string;
|
||||
channel_id: string;
|
||||
channel_id: string | undefined;
|
||||
endpoint: NavigationEndpoint | undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,13 @@ class NavigationEndpoint extends YTNode {
|
||||
};
|
||||
|
||||
// TODO: these should be given proper types, currently infered
|
||||
browse;
|
||||
browse?: {
|
||||
id: string,
|
||||
params: string | null,
|
||||
base_url: string | null,
|
||||
page_type: string | null,
|
||||
form_data?: {}
|
||||
};
|
||||
watch;
|
||||
search;
|
||||
subscribe;
|
||||
|
||||
12
src/parser/classes/menus/MusicMenuItemDivider.ts
Normal file
12
src/parser/classes/menus/MusicMenuItemDivider.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { YTNode } from '../../helpers';
|
||||
|
||||
class MusicMenuItemDivider extends YTNode {
|
||||
static type = 'MusicMenuItemDivider';
|
||||
|
||||
// eslint-disable-next-line
|
||||
constructor(data: any) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicMenuItemDivider;
|
||||
21
src/parser/classes/menus/MusicMultiSelectMenu.ts
Normal file
21
src/parser/classes/menus/MusicMultiSelectMenu.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import MusicMultiSelectMenuItem from './MusicMultiSelectMenuItem';
|
||||
import MusicMenuItemDivider from './MusicMenuItemDivider';
|
||||
import { YTNode } from '../../helpers';
|
||||
import Text from '../misc/Text';
|
||||
import Parser from '../..';
|
||||
|
||||
class MusicMultiSelectMenu extends YTNode {
|
||||
static type = 'MusicMultiSelectMenu';
|
||||
|
||||
title: string;
|
||||
options: Array<MusicMultiSelectMenuItem | MusicMenuItemDivider>;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.title = new Text(data.title.musicMenuTitleRenderer?.primaryText).text;
|
||||
this.options = Parser.parseArray(data.options, [ MusicMultiSelectMenuItem, MusicMenuItemDivider ]);
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicMultiSelectMenu;
|
||||
37
src/parser/classes/menus/MusicMultiSelectMenuItem.ts
Normal file
37
src/parser/classes/menus/MusicMultiSelectMenuItem.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { YTNode } from '../../helpers';
|
||||
import Text from '../misc/Text';
|
||||
import NavigationEndpoint from '../NavigationEndpoint';
|
||||
|
||||
class MusicMultiSelectMenuItem extends YTNode {
|
||||
static type = 'MusicMultiSelectMenuItem';
|
||||
|
||||
title: string;
|
||||
form_item_entity_key: string;
|
||||
selected_icon_type: string;
|
||||
endpoint?: NavigationEndpoint;
|
||||
selected: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.title = new Text(data.title).text;
|
||||
this.form_item_entity_key = data.formItemEntityKey;
|
||||
this.selected_icon_type = data.selectedIcon?.iconType || null;
|
||||
const command = data.selectedCommand?.commandExecutorCommand?.commands?.find((command: any) => command.musicBrowseFormBinderCommand?.browseEndpoint);
|
||||
if (command) {
|
||||
/**
|
||||
* At this point, endpoint will still be missing `form_data` field which is required for
|
||||
* selection to take effect. This can only be obtained from the response data which
|
||||
* we don't have here. We shall delegate this task back to `Parser`.
|
||||
*/
|
||||
this.endpoint = new NavigationEndpoint(command.musicBrowseFormBinderCommand);
|
||||
}
|
||||
/**
|
||||
* Inferring selected state from existence of endpoint. `Parser` shall
|
||||
* update this with the definitive value obtained from response data.
|
||||
*/
|
||||
this.selected = !!this.endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicMultiSelectMenuItem;
|
||||
@@ -10,6 +10,7 @@ import { InnertubeError, ParsingError } from '../utils/Utils';
|
||||
import { YTNode, YTNodeConstructor, SuperParsedResult, ObservedArray, observe, Memo } from './helpers';
|
||||
|
||||
import package_json from '../../package.json';
|
||||
import MusicMultiSelectMenuItem from './classes/menus/MusicMultiSelectMenuItem';
|
||||
|
||||
export class AppendContinuationItemsAction extends YTNode {
|
||||
static readonly type = 'appendContinuationItemsAction';
|
||||
@@ -223,6 +224,8 @@ export default class Parser {
|
||||
const actions_memo = this.#getMemo();
|
||||
this.#clearMemo();
|
||||
|
||||
this.applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
|
||||
|
||||
return {
|
||||
actions,
|
||||
actions_memo,
|
||||
@@ -389,6 +392,44 @@ export default class Parser {
|
||||
return new SuperParsedResult(this.parseItem(data, validTypes));
|
||||
}
|
||||
|
||||
static applyMutations(memo: Memo, mutations: Array<any>) {
|
||||
// Apply mutations to MusicMultiSelectMenuItems
|
||||
const musicMultiSelectMenuItems = memo.getType(MusicMultiSelectMenuItem);
|
||||
if (musicMultiSelectMenuItems.length > 0 && !mutations) {
|
||||
console.warn(
|
||||
new InnertubeError(
|
||||
'Mutation data required for processing MusicMultiSelectMenuItems, but none found.\n' +
|
||||
`This is a bug, please report it at ${package_json.bugs.url}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const missingOrInvalidMutations = [];
|
||||
for (const menuItem of musicMultiSelectMenuItems) {
|
||||
const mutation = mutations.find((mutation) => mutation.payload?.musicFormBooleanChoice?.id === menuItem.form_item_entity_key);
|
||||
const choice = mutation?.payload.musicFormBooleanChoice;
|
||||
if (choice?.selected !== undefined && choice?.opaqueToken) {
|
||||
menuItem.selected = choice.selected;
|
||||
if (menuItem.endpoint?.browse) {
|
||||
menuItem.endpoint.browse.form_data = {
|
||||
selectedValues: [ choice.opaqueToken ]
|
||||
};
|
||||
}
|
||||
} else {
|
||||
missingOrInvalidMutations.push(`'${menuItem.title}'`);
|
||||
}
|
||||
}
|
||||
if (missingOrInvalidMutations.length > 0) {
|
||||
console.warn(
|
||||
new InnertubeError(
|
||||
`Mutation data missing or invalid for ${missingOrInvalidMutations.length} out of ${musicMultiSelectMenuItems.length} MusicMultiSelectMenuItems. ` +
|
||||
`The titles of the failed items are: ${missingOrInvalidMutations.join(', ')}.\n` +
|
||||
`This is a bug, please report it at ${package_json.bugs.url}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static formatError({ classname, classdata, err }: { classname: string, classdata: any, err: any }) {
|
||||
if (err.code == 'MODULE_NOT_FOUND') {
|
||||
return console.warn(
|
||||
|
||||
@@ -124,6 +124,9 @@ import { default as MenuServiceItem } from './classes/menus/MenuServiceItem';
|
||||
import { default as MenuServiceItemDownload } from './classes/menus/MenuServiceItemDownload';
|
||||
import { default as MultiPageMenu } from './classes/menus/MultiPageMenu';
|
||||
import { default as MultiPageMenuNotificationSection } from './classes/menus/MultiPageMenuNotificationSection';
|
||||
import { default as MusicMenuItemDivider } from './classes/menus/MusicMenuItemDivider';
|
||||
import { default as MusicMultiSelectMenu } from './classes/menus/MusicMultiSelectMenu';
|
||||
import { default as MusicMultiSelectMenuItem } from './classes/menus/MusicMultiSelectMenuItem';
|
||||
import { default as SimpleMenuHeader } from './classes/menus/SimpleMenuHeader';
|
||||
import { default as MerchandiseItem } from './classes/MerchandiseItem';
|
||||
import { default as MerchandiseShelf } from './classes/MerchandiseShelf';
|
||||
@@ -153,6 +156,8 @@ import { default as MusicResponsiveListItem } from './classes/MusicResponsiveLis
|
||||
import { default as MusicResponsiveListItemFixedColumn } from './classes/MusicResponsiveListItemFixedColumn';
|
||||
import { default as MusicResponsiveListItemFlexColumn } from './classes/MusicResponsiveListItemFlexColumn';
|
||||
import { default as MusicShelf } from './classes/MusicShelf';
|
||||
import { default as MusicSideAlignedItem } from './classes/MusicSideAlignedItem';
|
||||
import { default as MusicSortFilterButton } from './classes/MusicSortFilterButton';
|
||||
import { default as MusicThumbnail } from './classes/MusicThumbnail';
|
||||
import { default as MusicTwoRowItem } from './classes/MusicTwoRowItem';
|
||||
import { default as NavigationEndpoint } from './classes/NavigationEndpoint';
|
||||
@@ -372,6 +377,9 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MenuServiceItemDownload,
|
||||
MultiPageMenu,
|
||||
MultiPageMenuNotificationSection,
|
||||
MusicMenuItemDivider,
|
||||
MusicMultiSelectMenu,
|
||||
MusicMultiSelectMenuItem,
|
||||
SimpleMenuHeader,
|
||||
MerchandiseItem,
|
||||
MerchandiseShelf,
|
||||
@@ -401,6 +409,8 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MusicResponsiveListItemFixedColumn,
|
||||
MusicResponsiveListItemFlexColumn,
|
||||
MusicShelf,
|
||||
MusicSideAlignedItem,
|
||||
MusicSortFilterButton,
|
||||
MusicThumbnail,
|
||||
MusicTwoRowItem,
|
||||
NavigationEndpoint,
|
||||
|
||||
Reference in New Issue
Block a user