diff --git a/src/core/mixins/Feed.ts b/src/core/mixins/Feed.ts index ddf95e4e..11d3b1eb 100644 --- a/src/core/mixins/Feed.ts +++ b/src/core/mixins/Feed.ts @@ -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 { * 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 { 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 { async getContinuationData(): Promise { 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(this.#actions, { parse: true }); } diff --git a/src/core/mixins/FilterableFeed.ts b/src/core/mixins/FilterableFeed.ts index a78b2f01..925d0267 100644 --- a/src/core/mixins/FilterableFeed.ts +++ b/src/core/mixins/FilterableFeed.ts @@ -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; + secondary_filters?: ObservedArray; +} export default class FilterableFeed extends Feed { - #chips?: ObservedArray; + #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 { - if (this.#chips) - return this.#chips || []; + get filter_nodes(): FilterNodes { + if (this.#filter_nodes) + return this.#filter_nodes; + + let primary_filters: ObservedArray | undefined; + let secondary_filters: ObservedArray | 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> { - let target_filter: ChipCloudChip | undefined; + async getFilteredFeed(filter: string | ChipCloudChip | ChipView | ListItemView, secondaryFilter?: string | ChipView): Promise> { + 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); } diff --git a/src/parser/classes/DownloadListItemView.ts b/src/parser/classes/DownloadListItemView.ts new file mode 100644 index 00000000..4f1d70f9 --- /dev/null +++ b/src/parser/classes/DownloadListItemView.ts @@ -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); + } + } +} \ No newline at end of file diff --git a/src/parser/classes/ListView.ts b/src/parser/classes/ListView.ts index 95b6c181..b387fcab 100644 --- a/src/parser/classes/ListView.ts +++ b/src/parser/classes/ListView.ts @@ -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; + public items: ObservedArray; 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); diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index 321da4ce..dd2d59fe 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -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'; diff --git a/src/parser/youtube/Channel.ts b/src/parser/youtube/Channel.ts index c19b1bfd..f78e06c1 100644 --- a/src/parser/youtube/Channel.ts +++ b/src/parser/youtube/Channel.ts @@ -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 { public header?: C4TabbedHeader | CarouselHeader | InteractiveTabbedHeader | PageHeader; @@ -39,6 +49,8 @@ export default class Channel extends TabbedFeed { 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 { } /** - * 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 { - 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(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(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 { + async applySort(sortFilter: string): Promise { 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 { 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 { 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 | undefined; + let secondary_filters: ObservedArray | 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 { } export class FilteredChannelList extends FilterableFeed { - 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 { * Applies given filter to the list. * @param filter - The filter to apply */ - async applyFilter(filter: string | ChipCloudChip): Promise { - const feed = await super.getFilteredFeed(filter); + async applyFilter(filter: string | ChipView | ChipCloudChip | ListItemView, secondaryFilter?: string | ChipView): Promise { + const feed = await super.getFilteredFeed(filter, secondaryFilter); return new FilteredChannelList(this.actions, feed.page, true); } @@ -358,9 +504,23 @@ export class FilteredChannelList extends FilterableFeed { 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); }