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