From 8c06d03396c85be29b53b5fbb56ce77071e77119 Mon Sep 17 00:00:00 2001 From: LuanRT Date: Fri, 27 Jan 2023 06:20:23 -0300 Subject: [PATCH] feat(Channel): add support for sorting the playlist tab --- README.md | 5 ++ src/parser/classes/ChannelSubMenu.ts | 27 ++++++++++ src/parser/classes/SectionList.ts | 5 ++ src/parser/classes/SortFilterSubMenu.ts | 2 +- src/parser/map.ts | 2 + src/parser/youtube/Channel.ts | 65 ++++++++++++++++++++++++- 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 src/parser/classes/ChannelSubMenu.ts diff --git a/README.md b/README.md index 542f4d70d..6dc279644 100644 --- a/README.md +++ b/README.md @@ -482,8 +482,13 @@ Retrieves contents for a given channel. - `#getChannels()` - `#getAbout()` - `#search(query)` +- `#applyFilter(filter)` +- `#applyContentTypeFilter(content_type_filter)` +- `#applySort(sort)` - `#getContinuation()` - `#filters` +- `#content_type_filters` +- `#sort_filters` - `#page`

diff --git a/src/parser/classes/ChannelSubMenu.ts b/src/parser/classes/ChannelSubMenu.ts new file mode 100644 index 000000000..195bec50a --- /dev/null +++ b/src/parser/classes/ChannelSubMenu.ts @@ -0,0 +1,27 @@ +import Parser from '..'; +import NavigationEndpoint from './NavigationEndpoint'; +import { YTNode } from '../helpers'; + +class ChannelSubMenu extends YTNode { + static type = 'ChannelSubMenu'; + + content_type_sub_menu_items: { + endpoint: NavigationEndpoint; + selected: boolean; + title: string; + }[]; + + sort_setting; + + constructor(data: any) { + super(); + this.content_type_sub_menu_items = data.contentTypeSubMenuItems.map((item: any) => ({ + endpoint: new NavigationEndpoint(item.navigationEndpoint || item.endpoint), + selected: item.selected, + title: item.title + })); + this.sort_setting = Parser.parseItem(data.sortSetting); + } +} + +export default ChannelSubMenu; \ No newline at end of file diff --git a/src/parser/classes/SectionList.ts b/src/parser/classes/SectionList.ts index cd1432f2a..faa5872c4 100644 --- a/src/parser/classes/SectionList.ts +++ b/src/parser/classes/SectionList.ts @@ -8,6 +8,7 @@ class SectionList extends YTNode { contents; continuation?: string; header; + sub_menu; constructor(data: any) { super(); @@ -29,6 +30,10 @@ class SectionList extends YTNode { if (data.header) { this.header = Parser.parse(data.header); } + + if (data.subMenu) { + this.sub_menu = Parser.parseItem(data.subMenu); + } } } diff --git a/src/parser/classes/SortFilterSubMenu.ts b/src/parser/classes/SortFilterSubMenu.ts index 1ad2a8c85..ca9147fbe 100644 --- a/src/parser/classes/SortFilterSubMenu.ts +++ b/src/parser/classes/SortFilterSubMenu.ts @@ -41,7 +41,7 @@ class SortFilterSubMenu extends YTNode { title: item.title, selected: item.selected, continuation: item.continuation?.reloadContinuationData?.continuation, - endpoint: new NavigationEndpoint(item.serviceEndpoint), + endpoint: new NavigationEndpoint(item.serviceEndpoint || item.navigationEndpoint), subtitle: item.subtitle || null })); } diff --git a/src/parser/map.ts b/src/parser/map.ts index acc02a2fa..927ba51f5 100644 --- a/src/parser/map.ts +++ b/src/parser/map.ts @@ -37,6 +37,7 @@ 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 ChannelSubMenu } from './classes/ChannelSubMenu'; import { default as ChannelThumbnailWithLink } from './classes/ChannelThumbnailWithLink'; import { default as ChannelVideoPlayer } from './classes/ChannelVideoPlayer'; import { default as Chapter } from './classes/Chapter'; @@ -354,6 +355,7 @@ export const YTNodes = { ChannelMetadata, ChannelMobileHeader, ChannelOptions, + ChannelSubMenu, ChannelThumbnailWithLink, ChannelVideoPlayer, Chapter, diff --git a/src/parser/youtube/Channel.ts b/src/parser/youtube/Channel.ts index 059c6aa48..53f0c0c3c 100644 --- a/src/parser/youtube/Channel.ts +++ b/src/parser/youtube/Channel.ts @@ -7,13 +7,17 @@ import ChannelMetadata from '../classes/ChannelMetadata'; import InteractiveTabbedHeader from '../classes/InteractiveTabbedHeader'; import MicroformatData from '../classes/MicroformatData'; import SubscribeButton from '../classes/SubscribeButton'; +import ExpandableTab from '../classes/ExpandableTab'; +import SectionList from '../classes/SectionList'; import Tab from '../classes/Tab'; import Feed from '../../core/Feed'; import FilterableFeed from '../../core/FilterableFeed'; import ChipCloudChip from '../classes/ChipCloudChip'; -import ExpandableTab from '../classes/ExpandableTab'; import FeedFilterChipBar from '../classes/FeedFilterChipBar'; +import ChannelSubMenu from '../classes/ChannelSubMenu'; +import SortFilterSubMenu from '../classes/SortFilterSubMenu'; + import { InnertubeError } from '../../utils/Utils'; import type { AppendContinuationItemsAction, ReloadContinuationItemsCommand } from '..'; @@ -49,7 +53,7 @@ export default class Channel extends TabbedFeed { async applyFilter(filter: string | ChipCloudChip): Promise { let target_filter: ChipCloudChip | undefined; - const filter_chipbar = this.memo.getType(FeedFilterChipBar)?.[0]; + const filter_chipbar = this.memo.getType(FeedFilterChipBar).first(); if (typeof filter === 'string') { target_filter = filter_chipbar?.contents.get({ text: filter }); @@ -63,13 +67,70 @@ export default class Channel extends TabbedFeed { throw new InnertubeError('Invalid filter', filter); const page = await target_filter.endpoint?.call(this.actions, { parse: true }); + return new FilteredChannelList(this.actions, page, true); } + /** + * Applies given sort filter to the list. Use {@link sort_filters} to get available filters. + * @param sort - The sort filter to apply + */ + async applySort(sort: string): Promise { + const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu).first(); + + if (!sort_filter_sub_menu) + throw new InnertubeError('No sort filter sub menu found'); + + const target_sort = sort_filter_sub_menu?.sub_menu_items?.find((item) => item.title === sort); + + if (!target_sort) + throw new InnertubeError(`Sort filter ${sort} not found`, { available_sort_filters: this.sort_filters }); + + if (target_sort.selected) + return this; + + const page = await target_sort.endpoint?.call(this.actions, { parse: true }); + + return new Channel(this.actions, page, true); + } + + /** + * Applies given content type filter to the list. Use {@link content_type_filters} to get available filters. + * @param content_type_filter - The content type filter to apply + */ + async applyContentTypeFilter(content_type_filter: string): Promise { + const sub_menu = this.current_tab?.content?.as(SectionList).sub_menu?.as(ChannelSubMenu); + + if (!sub_menu) + throw new InnertubeError('Sub menu not found'); + + 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 }); + + if (item.selected) + return this; + + const page = await item.endpoint?.call(this.actions, { parse: true }); + + return new Channel(this.actions, page, true); + } + get filters(): string[] { return this.memo.getType(FeedFilterChipBar)?.[0]?.contents.filterType(ChipCloudChip).map((chip) => chip.text) || []; } + get sort_filters(): string[] { + const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu).first(); + return sort_filter_sub_menu?.sub_menu_items?.map((item) => item.title) || []; + } + + get content_type_filters(): string[] { + const sub_menu = this.current_tab?.content?.as(SectionList).sub_menu?.as(ChannelSubMenu); + return sub_menu?.content_type_sub_menu_items.map((item) => item.title) || []; + } + async getHome(): Promise { const tab = await this.getTabByURL('featured'); return new Channel(this.actions, tab.page, true);