mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-22 14:08:06 +00:00
fix(Channel): Add support for new feed filter nodes (#1163)
* feat(Feed): Update `getVideosFromMemo` to support `LockupView` nodes * feat(parser): Add `DownloadListItemView` renderer parser * feat(FilterableFeed): Add support for `ChipView` nodes * feat(Channel): Add support for new feed filter system Honestly the whole Channel class needs a cleanup. Maybe I'll do that at some point in the future.
This commit is contained in:
@@ -29,7 +29,7 @@ import TwoColumnSearchResults from '../../parser/classes/TwoColumnSearchResults.
|
||||
import WatchCardCompactVideo from '../../parser/classes/WatchCardCompactVideo.js';
|
||||
|
||||
import type { Actions, ApiResponse } from '../index.js';
|
||||
import type { Memo, ObservedArray } from '../../parser/helpers.js';
|
||||
import { observe, type Memo, type ObservedArray } from '../../parser/helpers.js';
|
||||
import type MusicQueue from '../../parser/classes/MusicQueue.js';
|
||||
import type RichGrid from '../../parser/classes/RichGrid.js';
|
||||
import type SectionList from '../../parser/classes/SectionList.js';
|
||||
@@ -76,16 +76,17 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
* Get all videos on a given page via memo
|
||||
*/
|
||||
static getVideosFromMemo(memo: Memo) {
|
||||
return memo.getType(
|
||||
return observe(memo.getType(
|
||||
Video,
|
||||
GridVideo,
|
||||
ReelItem,
|
||||
ShortsLockupView,
|
||||
CompactVideo,
|
||||
LockupView,
|
||||
PlaylistVideo,
|
||||
PlaylistPanelVideo,
|
||||
WatchCardCompactVideo
|
||||
);
|
||||
).filter((item) => !item.is(LockupView) || (item.content_type === 'VIDEO' || item.content_type === 'MOVIE' || item.content_type === 'SHORT')));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +146,6 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
const tab_content = this.#memo.getType(Tab)?.[0].content;
|
||||
const reload_continuation_items = this.#memo.getType(ReloadContinuationItemsCommand)[0];
|
||||
const append_continuation_items = this.#memo.getType(AppendContinuationItemsAction)[0];
|
||||
|
||||
return tab_content || reload_continuation_items || append_continuation_items;
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
async getContinuationData(): Promise<T | undefined> {
|
||||
if (this.#continuation) {
|
||||
if (this.#continuation.length === 0)
|
||||
throw new InnertubeError('There are no continuations.');
|
||||
throw new InnertubeError('There are no continuations');
|
||||
|
||||
return await this.#continuation[0].endpoint.call<T>(this.#actions, { parse: true });
|
||||
}
|
||||
|
||||
@@ -1,70 +1,167 @@
|
||||
import Feed from './Feed.js';
|
||||
import ChipCloudChip from '../../parser/classes/ChipCloudChip.js';
|
||||
import FeedFilterChipBar from '../../parser/classes/FeedFilterChipBar.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import ChipView from '../../parser/classes/ChipView.js';
|
||||
import ShowSheetCommand from '../../parser/classes/commands/ShowSheetCommand.js';
|
||||
import SheetView from '../../parser/classes/SheetView.js';
|
||||
import ListView from '../../parser/classes/ListView.js';
|
||||
import ListItemView from '../../parser/classes/ListItemView.js';
|
||||
import type NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js';
|
||||
|
||||
import type { ObservedArray } from '../../parser/helpers.js';
|
||||
import { observe, type ObservedArray } from '../../parser/helpers.js';
|
||||
import type { IParsedResponse } from '../../parser/index.js';
|
||||
import type { ApiResponse, Actions } from '../index.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
export interface FilterNodes {
|
||||
primary_filters?: ObservedArray<ChipCloudChip | ListItemView | ChipView>;
|
||||
secondary_filters?: ObservedArray<ChipView>;
|
||||
}
|
||||
|
||||
export default class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#chips?: ObservedArray<ChipCloudChip>;
|
||||
#filter_nodes?: FilterNodes;
|
||||
|
||||
constructor(actions: Actions, data: ApiResponse | T, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filter chips.
|
||||
* Returns the InnerTube renderer nodes representing filters.
|
||||
*/
|
||||
get filter_chips(): ObservedArray<ChipCloudChip> {
|
||||
if (this.#chips)
|
||||
return this.#chips || [];
|
||||
get filter_nodes(): FilterNodes {
|
||||
if (this.#filter_nodes)
|
||||
return this.#filter_nodes;
|
||||
|
||||
let primary_filters: ObservedArray<ChipCloudChip | ListItemView | ChipView> | undefined;
|
||||
let secondary_filters: ObservedArray<ChipView> | undefined;
|
||||
|
||||
if (this.memo.getType(FeedFilterChipBar)?.length > 1)
|
||||
throw new InnertubeError('There are too many feed filter chipbars, you\'ll need to find the correct one yourself in this.page');
|
||||
|
||||
if (this.memo.getType(FeedFilterChipBar)?.length === 0)
|
||||
throw new InnertubeError('There are no feed filter chipbars');
|
||||
if (this.memo.has('FeedFilterChipBar')) {
|
||||
primary_filters = this.memo.getType(ChipCloudChip);;
|
||||
} else if (this.memo.has('ChipView')) {
|
||||
const chips = this.memo.getType(ChipView);
|
||||
const firstChip = chips[0];
|
||||
|
||||
this.#chips = this.memo.getType(ChipCloudChip);
|
||||
if (firstChip.is(ChipView)) {
|
||||
const hasDropdown =
|
||||
firstChip.display_type === 'CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN' ||
|
||||
firstChip.display_type === 'CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN_WITH_CLEAR';
|
||||
|
||||
return this.#chips || [];
|
||||
if (hasDropdown) {
|
||||
const tapCommand = firstChip.tap_command?.command;
|
||||
if (
|
||||
tapCommand?.is(ShowSheetCommand) &&
|
||||
tapCommand.inline_content?.is(SheetView) &&
|
||||
tapCommand.inline_content.content?.is(ListView)
|
||||
) {
|
||||
primary_filters = tapCommand.inline_content.content.items.as(ListItemView);
|
||||
}
|
||||
|
||||
secondary_filters = observe(chips.slice(1));
|
||||
} else primary_filters = chips;
|
||||
}
|
||||
}
|
||||
|
||||
this.#filter_nodes = {
|
||||
primary_filters,
|
||||
secondary_filters
|
||||
};
|
||||
|
||||
return this.#filter_nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns available filters.
|
||||
* Returns the available primary filters as strings.
|
||||
*/
|
||||
get filters(): string[] {
|
||||
return this.filter_chips.map((chip) => chip.text.toString()) || [];
|
||||
return this.filter_nodes?.primary_filters?.map((chip) => {
|
||||
if (chip.is(ChipView) || chip.is(ChipCloudChip)) {
|
||||
return chip.text?.toString() || '';
|
||||
} else if (chip.is(ListItemView)) {
|
||||
return chip.title?.toString() || '';
|
||||
}
|
||||
return '';
|
||||
}) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available secondary filters as strings.
|
||||
*/
|
||||
get secondary_filters(): string[] {
|
||||
return this.filter_nodes?.secondary_filters?.map((chip) => chip.text?.toString() || '') || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies given filter and returns a new {@link Feed} object.
|
||||
*/
|
||||
async getFilteredFeed(filter: string | ChipCloudChip): Promise<Feed<T>> {
|
||||
let target_filter: ChipCloudChip | undefined;
|
||||
async getFilteredFeed(filter: string | ChipCloudChip | ChipView | ListItemView, secondaryFilter?: string | ChipView): Promise<Feed<T>> {
|
||||
let filterEndpoint: NavigationEndpoint | undefined;
|
||||
let isSelected: boolean = false;
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
if (!this.filters.includes(filter))
|
||||
throw new InnertubeError('Filter not found', { available_filters: this.filters });
|
||||
target_filter = this.filter_chips.find((chip) => chip.text.toString() === filter);
|
||||
} else if (filter.type === 'ChipCloudChip') {
|
||||
target_filter = filter;
|
||||
throw new InnertubeError(`Filter '${filter}' not found`, { available_filters: this.filters });
|
||||
|
||||
for (const primaryFilterNode of this.filter_nodes.primary_filters || []) {
|
||||
if ((primaryFilterNode.is(ChipView) || primaryFilterNode.is(ChipCloudChip)) && primaryFilterNode.text?.toString() === filter) {
|
||||
filterEndpoint = primaryFilterNode.is(ChipView) ? primaryFilterNode.tap_command : primaryFilterNode.endpoint;
|
||||
isSelected = primaryFilterNode.is(ChipView) ? primaryFilterNode.selected : primaryFilterNode.is_selected;
|
||||
} else if (primaryFilterNode.is(ListItemView) && primaryFilterNode.title?.toString() === filter) {
|
||||
filterEndpoint = primaryFilterNode?.renderer_context?.command_context?.on_tap;
|
||||
isSelected = primaryFilterNode.is_selected;
|
||||
}
|
||||
}
|
||||
} else if ([ 'ChipCloudChip', 'ChipView', 'ListItemView' ].includes(filter.type)) {
|
||||
if (filter.is(ChipView)) {
|
||||
filterEndpoint = filter.tap_command;
|
||||
} else if (filter.is(ChipCloudChip)) {
|
||||
filterEndpoint = filter.endpoint;
|
||||
} else if (filter.is(ListItemView)) {
|
||||
filterEndpoint = filter?.renderer_context?.command_context?.on_tap;
|
||||
}
|
||||
isSelected = filter.is(ChipView) ? filter.selected : filter.is_selected;
|
||||
} else {
|
||||
throw new InnertubeError('Invalid filter');
|
||||
throw new InnertubeError('Invalid primary filter type');
|
||||
}
|
||||
|
||||
if (!target_filter)
|
||||
throw new InnertubeError('Filter not found');
|
||||
if (!filterEndpoint)
|
||||
throw new InnertubeError('Could not find endpoint for the specified filter');
|
||||
|
||||
if (target_filter.is_selected)
|
||||
if (isSelected && !secondaryFilter)
|
||||
return this;
|
||||
|
||||
const response = await target_filter.endpoint?.call(this.actions, { parse: true });
|
||||
// No need to make a request if the filter is already selected...
|
||||
let response = isSelected ? this.page : await filterEndpoint.call(this.actions, { parse: true });
|
||||
|
||||
if (secondaryFilter) {
|
||||
const feed = new FilterableFeed(this.actions, response, true);
|
||||
|
||||
let secondaryFilterNode: ChipView | undefined;
|
||||
|
||||
if (typeof secondaryFilter === 'string') {
|
||||
if (!feed.secondary_filters.includes(secondaryFilter))
|
||||
throw new InnertubeError(`Secondary filter '${secondaryFilter}' not found`, { available_filters: feed.secondary_filters });
|
||||
secondaryFilterNode = feed.filter_nodes.secondary_filters?.find((chip) => chip.text?.toString() === secondaryFilter);
|
||||
} else if (secondaryFilter.is(ChipView)) {
|
||||
secondaryFilterNode = secondaryFilter;
|
||||
} else {
|
||||
throw new InnertubeError('Invalid secondary filter type');
|
||||
}
|
||||
|
||||
if (secondaryFilterNode && !secondaryFilterNode.selected) {
|
||||
const secondaryFilterEndpoint = secondaryFilterNode?.tap_command || secondaryFilterNode?.endpoint;
|
||||
|
||||
if (!secondaryFilterEndpoint)
|
||||
throw new InnertubeError('Could not find an endpoint for the specified secondary filter');
|
||||
|
||||
response = await secondaryFilterEndpoint.call(this.actions, { parse: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Failed to get filtered feed');
|
||||
throw new InnertubeError('Failed to fetch data for the specified filter');
|
||||
|
||||
return new Feed(this.actions, response, true);
|
||||
}
|
||||
|
||||
16
src/parser/classes/DownloadListItemView.ts
Normal file
16
src/parser/classes/DownloadListItemView.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../types/RawResponse.js';
|
||||
import RendererContext from './misc/RendererContext.js';
|
||||
|
||||
export default class DownloadListItemView extends YTNode {
|
||||
static type = 'DownloadListItemView';
|
||||
|
||||
public renderer_context?: RendererContext;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
if ('rendererContext' in data) {
|
||||
this.renderer_context = new RendererContext(data.rendererContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,19 @@ import { YTNode } from '../helpers.js';
|
||||
import { Parser } from '../index.js';
|
||||
|
||||
import ListItemView from './ListItemView.js';
|
||||
import DownloadListItemView from './DownloadListItemView.js';
|
||||
import RendererContext from './misc/RendererContext.js';
|
||||
|
||||
export default class ListView extends YTNode {
|
||||
static type = 'ListView';
|
||||
|
||||
public items: ObservedArray<ListItemView>;
|
||||
public items: ObservedArray<ListItemView | DownloadListItemView>;
|
||||
public renderer_context?: RendererContext;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.items = Parser.parseArray(data.listItems, ListItemView);
|
||||
this.items = Parser.parseArray(data.listItems, [ ListItemView, DownloadListItemView ]);
|
||||
|
||||
if ('rendererContext' in data) {
|
||||
this.renderer_context = new RendererContext(data.rendererContext);
|
||||
|
||||
@@ -130,6 +130,7 @@ export { default as DislikeButtonView } from './classes/DislikeButtonView.js';
|
||||
export { default as DismissableDialog } from './classes/DismissableDialog.js';
|
||||
export { default as DismissableDialogContentSection } from './classes/DismissableDialogContentSection.js';
|
||||
export { default as DownloadButton } from './classes/DownloadButton.js';
|
||||
export { default as DownloadListItemView } from './classes/DownloadListItemView.js';
|
||||
export { default as Dropdown } from './classes/Dropdown.js';
|
||||
export { default as DropdownItem } from './classes/DropdownItem.js';
|
||||
export { default as DropdownView } from './classes/DropdownView.js';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Feed from '../../core/mixins/Feed.js';
|
||||
import type { FilterNodes } from '../../core/mixins/FilterableFeed.js';
|
||||
import FilterableFeed from '../../core/mixins/FilterableFeed.js';
|
||||
import { ChannelError, InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
@@ -22,6 +23,12 @@ import ChannelSubMenu from '../classes/ChannelSubMenu.js';
|
||||
import SortFilterSubMenu from '../classes/SortFilterSubMenu.js';
|
||||
import ContinuationItem from '../classes/ContinuationItem.js';
|
||||
import NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
import SheetView from '../classes/SheetView.js';
|
||||
import ListView from '../classes/ListView.js';
|
||||
import ChipBarView from '../classes/ChipBarView.js';
|
||||
import ShowSheetCommand from '../classes/commands/ShowSheetCommand.js';
|
||||
import ChipView from '../classes/ChipView.js';
|
||||
import ListItemView from '../classes/ListItemView.js';
|
||||
|
||||
import type {
|
||||
AppendContinuationItemsAction,
|
||||
@@ -29,9 +36,12 @@ import type {
|
||||
ReloadContinuationItemsCommand,
|
||||
ShowMiniplayerCommand
|
||||
} from '../index.js';
|
||||
|
||||
import type { ApiResponse, Actions } from '../../core/index.js';
|
||||
import type { IBrowseResponse } from '../types/index.js';
|
||||
import type OpenPopupAction from '../classes/actions/OpenPopupAction.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { observe } from '../helpers.js';
|
||||
|
||||
export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
public header?: C4TabbedHeader | CarouselHeader | InteractiveTabbedHeader | PageHeader;
|
||||
@@ -39,6 +49,8 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
public subscribe_button?: SubscribeButton;
|
||||
public current_tab?: Tab | ExpandableTab;
|
||||
|
||||
#filter_nodes?: FilterNodes;
|
||||
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
@@ -66,47 +78,89 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies given filter to the list. Use {@link filters} to get available filters.
|
||||
* @param filter - The filter to apply
|
||||
* Applies a filter to the channel list. {@link filters}, {@link secondary_filters}, and {@link filter_nodes} can be used to get available filters.
|
||||
*
|
||||
* @param primaryFilter - The primary filter to apply. Can be a string representing the filter name,
|
||||
* a {@link ChipView} instance, or a {@link ListItemView} instance.
|
||||
* @param secondaryFilter - An optional secondary filter to apply after the primary filter.
|
||||
* Can be a string representing the filter name or a {@link ChipView} instance.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Apply a primary filter by name.
|
||||
* const filtered = await videos.applyFilter('Oldest');
|
||||
*
|
||||
* // Apply a primary and secondary filter by name.
|
||||
* const filtered = await videos.applyFilter('Oldest', 'Members only');
|
||||
*
|
||||
* // Since we're using `filtered`, the following will return the latest members-only videos,
|
||||
* // unless the secondary filter is explicitly changed.
|
||||
* const latestMembersOnly = await filtered.applyFilter('Latest');
|
||||
* ```
|
||||
*/
|
||||
async applyFilter(filter: string | ChipCloudChip): Promise<FilteredChannelList> {
|
||||
let target_filter: ChipCloudChip | undefined;
|
||||
async applyFilter(primaryFilter: string | ChipView | ListItemView, secondaryFilter?: string | ChipView) {
|
||||
const chipBarView = this.memo.getType(ChipBarView)[0];
|
||||
|
||||
const filter_chipbar = this.memo.getType(FeedFilterChipBar)[0];
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
target_filter = filter_chipbar?.contents.find((chip) => chip.text === filter);
|
||||
if (!target_filter)
|
||||
throw new InnertubeError(`Filter ${filter} not found`, { available_filters: this.filters });
|
||||
} else {
|
||||
target_filter = filter;
|
||||
if (!chipBarView) {
|
||||
throw new InnertubeError('Filter chip bar not found');
|
||||
}
|
||||
|
||||
if (!target_filter.endpoint)
|
||||
throw new InnertubeError('Invalid filter', filter);
|
||||
let endpoint: NavigationEndpoint | undefined;
|
||||
|
||||
const page = await target_filter.endpoint.call<IBrowseResponse>(this.actions, { parse: true });
|
||||
if (typeof primaryFilter === 'string') {
|
||||
if (!this.filters.includes(primaryFilter))
|
||||
throw new InnertubeError(`Filter '${primaryFilter}' not found`, { available_filters: this.filters });
|
||||
|
||||
if (!page)
|
||||
throw new InnertubeError('No page returned', { filter: target_filter });
|
||||
const dropdownMenu = chipBarView.chips.find((chip) => chip.display_type === 'CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN');
|
||||
|
||||
return new FilteredChannelList(this.actions, page, true);
|
||||
if (dropdownMenu) {
|
||||
const tapCommand = dropdownMenu.tap_command?.command;
|
||||
|
||||
if (tapCommand?.is(ShowSheetCommand) && tapCommand.inline_content?.is(SheetView) && tapCommand.inline_content.content?.is(ListView)) {
|
||||
const listViewItems = tapCommand.inline_content.content.items;
|
||||
const matchingListItem = listViewItems.as(ListItemView).find((item) => item.title?.toString() === primaryFilter);
|
||||
endpoint = matchingListItem?.renderer_context?.command_context?.on_tap?.as(NavigationEndpoint);
|
||||
}
|
||||
} else {
|
||||
endpoint = chipBarView.chips.find((chip) => chip.text === primaryFilter)?.endpoint;
|
||||
}
|
||||
} else if (primaryFilter.is(ChipView)) {
|
||||
endpoint = primaryFilter.endpoint;
|
||||
} else if (primaryFilter.is(ListItemView)) {
|
||||
endpoint = primaryFilter.renderer_context?.command_context?.on_tap?.as(NavigationEndpoint);
|
||||
} else {
|
||||
throw new InnertubeError('Invalid primary filter type');
|
||||
}
|
||||
|
||||
if (!endpoint)
|
||||
throw new InnertubeError('Could not find endpoint for the specified filter');
|
||||
|
||||
const page = await endpoint.call<IBrowseResponse>(this.actions, { parse: true });
|
||||
|
||||
let filteredChannelList = new FilteredChannelList(this.actions, page, true);
|
||||
|
||||
// Then apply secondary filter if provided.
|
||||
if (secondaryFilter) {
|
||||
filteredChannelList = await filteredChannelList.applyFilter(primaryFilter, secondaryFilter);
|
||||
}
|
||||
|
||||
return filteredChannelList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies given sort filter to the list. Use {@link sort_filters} to get available filters.
|
||||
* @param sort - The sort filter to apply
|
||||
* Applies a sort filter to the list. Use {@link sort_filters} to get available filters.
|
||||
* @param sortFilter - The sort filter to apply
|
||||
*/
|
||||
async applySort(sort: string): Promise<Channel> {
|
||||
async applySort(sortFilter: string): Promise<Channel> {
|
||||
const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu)[0];
|
||||
|
||||
if (!sort_filter_sub_menu || !sort_filter_sub_menu.sub_menu_items)
|
||||
throw new InnertubeError('No sort filter sub menu found');
|
||||
|
||||
const target_sort = sort_filter_sub_menu.sub_menu_items.find((item) => item.title === sort);
|
||||
const target_sort = sort_filter_sub_menu.sub_menu_items.find((item) => item.title === sortFilter);
|
||||
|
||||
if (!target_sort)
|
||||
throw new InnertubeError(`Sort filter ${sort} not found`, { available_sort_filters: this.sort_filters });
|
||||
throw new InnertubeError(`Sort filter '${sortFilter}' not found`, { available_sort_filters: this.sort_filters });
|
||||
|
||||
if (target_sort.selected)
|
||||
return this;
|
||||
@@ -129,7 +183,7 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
const item = sub_menu.content_type_sub_menu_items.find((item) => item.title === content_type_filter);
|
||||
|
||||
if (!item)
|
||||
throw new InnertubeError(`Sub menu item ${content_type_filter} not found`, { available_filters: this.content_type_filters });
|
||||
throw new InnertubeError(`Sub menu item '${content_type_filter}' not found`, { available_filters: this.content_type_filters });
|
||||
|
||||
if (item.selected)
|
||||
return this;
|
||||
@@ -139,8 +193,77 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
return new Channel(this.actions, page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the InnerTube renderer nodes representing filters.
|
||||
*/
|
||||
get filter_nodes(): FilterNodes {
|
||||
if (this.#filter_nodes)
|
||||
return this.#filter_nodes;
|
||||
|
||||
let primary_filters: ObservedArray<ChipCloudChip | ListItemView | ChipView> | undefined;
|
||||
let secondary_filters: ObservedArray<ChipView> | undefined;
|
||||
|
||||
if (this.memo.getType(FeedFilterChipBar)?.length > 1)
|
||||
throw new InnertubeError('There are too many feed filter chipbars, you\'ll need to find the correct one yourself in this.page');
|
||||
|
||||
if (this.memo.has('FeedFilterChipBar')) {
|
||||
primary_filters = this.memo.getType(ChipCloudChip);;
|
||||
} else if (this.memo.has('ChipView')) {
|
||||
const chips = this.memo.getType(ChipView);
|
||||
const firstChip = chips[0];
|
||||
|
||||
if (firstChip.is(ChipView)) {
|
||||
const hasDropdown =
|
||||
firstChip.display_type === 'CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN' ||
|
||||
firstChip.display_type === 'CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN_WITH_CLEAR';
|
||||
|
||||
if (hasDropdown) {
|
||||
const tapCommand = firstChip.tap_command?.command;
|
||||
if (
|
||||
tapCommand?.is(ShowSheetCommand) &&
|
||||
tapCommand.inline_content?.is(SheetView) &&
|
||||
tapCommand.inline_content.content?.is(ListView)
|
||||
) {
|
||||
primary_filters = tapCommand.inline_content.content.items.as(ListItemView);
|
||||
}
|
||||
|
||||
secondary_filters = observe(chips.slice(1) as ChipView[]);
|
||||
} else primary_filters = chips;
|
||||
}
|
||||
}
|
||||
|
||||
this.#filter_nodes = {
|
||||
primary_filters,
|
||||
secondary_filters
|
||||
};
|
||||
|
||||
return this.#filter_nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available primary filters as strings.
|
||||
*/
|
||||
get filters(): string[] {
|
||||
return this.memo.getType(FeedFilterChipBar)?.[0]?.contents.filterType(ChipCloudChip).map((chip) => chip.text) || [];
|
||||
return this.filter_nodes?.primary_filters?.map((chip) => {
|
||||
if (chip.is(ChipView) || chip.is(ChipCloudChip)) {
|
||||
return chip.text?.toString() || '';
|
||||
} else if (chip.is(ListItemView)) {
|
||||
return chip.title?.toString() || '';
|
||||
}
|
||||
return '';
|
||||
}) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available secondary filters as strings.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* NOTE:
|
||||
* Not all channels have secondary filters!
|
||||
*/
|
||||
get secondary_filters(): string[] {
|
||||
return this.filter_nodes?.secondary_filters?.map((chip) => chip.text?.toString() || '') || [];
|
||||
}
|
||||
|
||||
get sort_filters(): string[] {
|
||||
@@ -324,13 +447,36 @@ export class ChannelListContinuation extends Feed<IBrowseResponse> {
|
||||
}
|
||||
|
||||
export class FilteredChannelList extends FilterableFeed<IBrowseResponse> {
|
||||
applied_filter?: ChipCloudChip;
|
||||
contents?: AppendContinuationItemsAction | OpenPopupAction | NavigateAction | ShowMiniplayerCommand | ReloadContinuationItemsCommand;
|
||||
public filter?: ChipView | ListItemView;
|
||||
public secondary_filter?: ChipView;
|
||||
public contents?: AppendContinuationItemsAction | OpenPopupAction | NavigateAction | ShowMiniplayerCommand | ReloadContinuationItemsCommand;
|
||||
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
this.applied_filter = this.memo.getType(ChipCloudChip).find((chip) => chip.is_selected);
|
||||
const chipBarView = this.memo.getType(ChipBarView)[0];
|
||||
|
||||
if (chipBarView) {
|
||||
const firstChip = chipBarView.chips[0];
|
||||
|
||||
const hasDropdown =
|
||||
firstChip.display_type === 'CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN' ||
|
||||
firstChip.display_type === 'CHIP_VIEW_MODEL_DISPLAY_TYPE_DROP_DOWN_WITH_CLEAR';
|
||||
|
||||
if (hasDropdown) {
|
||||
this.secondary_filter = chipBarView.chips.slice(1).find((chip) => chip.selected);
|
||||
const tapCommand = firstChip.tap_command?.command;
|
||||
if (
|
||||
tapCommand?.is(ShowSheetCommand) &&
|
||||
tapCommand.inline_content?.is(SheetView) &&
|
||||
tapCommand.inline_content.content?.is(ListView)
|
||||
) {
|
||||
this.filter = tapCommand.inline_content.content.items.as(ListItemView).find((item) => item.is_selected);
|
||||
}
|
||||
} else {
|
||||
this.filter = chipBarView.chips.find((chip) => chip.selected);
|
||||
}
|
||||
}
|
||||
|
||||
// Removes the filter chipbar from the actions list
|
||||
if (
|
||||
@@ -347,8 +493,8 @@ export class FilteredChannelList extends FilterableFeed<IBrowseResponse> {
|
||||
* Applies given filter to the list.
|
||||
* @param filter - The filter to apply
|
||||
*/
|
||||
async applyFilter(filter: string | ChipCloudChip): Promise<FilteredChannelList> {
|
||||
const feed = await super.getFilteredFeed(filter);
|
||||
async applyFilter(filter: string | ChipView | ChipCloudChip | ListItemView, secondaryFilter?: string | ChipView): Promise<FilteredChannelList> {
|
||||
const feed = await super.getFilteredFeed(filter, secondaryFilter);
|
||||
return new FilteredChannelList(this.actions, feed.page, true);
|
||||
}
|
||||
|
||||
@@ -358,9 +504,23 @@ export class FilteredChannelList extends FilterableFeed<IBrowseResponse> {
|
||||
if (!page?.on_response_received_actions_memo)
|
||||
throw new InnertubeError('Unexpected continuation data', page);
|
||||
|
||||
// Keep the filters
|
||||
page.on_response_received_actions_memo.set('FeedFilterChipBar', this.memo.getType(FeedFilterChipBar));
|
||||
page.on_response_received_actions_memo.set('ChipCloudChip', this.memo.getType(ChipCloudChip));
|
||||
// Legacy filter system. Keep it here in case YouTube changes its mind.
|
||||
if (this.memo.has('FeedFilterChipBar')) {
|
||||
page.on_response_received_actions_memo.set('FeedFilterChipBar', this.memo.getType(FeedFilterChipBar));
|
||||
}
|
||||
|
||||
if (this.memo.has('ChipCloudChip')) {
|
||||
page.on_response_received_actions_memo.set('ChipCloudChip', this.memo.getType(ChipCloudChip));
|
||||
}
|
||||
|
||||
// New filter system
|
||||
if (this.memo.has('ChipBarView')) {
|
||||
page.on_response_received_actions_memo.set('ChipBarView', this.memo.getType(ChipBarView));
|
||||
}
|
||||
|
||||
if (this.memo.has('ChipView')) {
|
||||
page.on_response_received_actions_memo.set('ChipView', this.memo.getType(ChipView));
|
||||
}
|
||||
|
||||
return new FilteredChannelList(this.actions, page, true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user