Skip to content

Commit 50ef712

Browse files
authored
feat(Channel): add support for sorting the playlist tab (#295)
1 parent d6c5a9b commit 50ef712

File tree

6 files changed

+103
-3
lines changed

6 files changed

+103
-3
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,13 @@ Retrieves contents for a given channel.
482482
- `<channel>#getChannels()`
483483
- `<channel>#getAbout()`
484484
- `<channel>#search(query)`
485+
- `<channel>#applyFilter(filter)`
486+
- `<channel>#applyContentTypeFilter(content_type_filter)`
487+
- `<channel>#applySort(sort)`
485488
- `<channel>#getContinuation()`
486489
- `<channel>#filters`
490+
- `<channel>#content_type_filters`
491+
- `<channel>#sort_filters`
487492
- `<channel>#page`
488493

489494
</p>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Parser from '..';
2+
import NavigationEndpoint from './NavigationEndpoint';
3+
import { YTNode } from '../helpers';
4+
5+
class ChannelSubMenu extends YTNode {
6+
static type = 'ChannelSubMenu';
7+
8+
content_type_sub_menu_items: {
9+
endpoint: NavigationEndpoint;
10+
selected: boolean;
11+
title: string;
12+
}[];
13+
14+
sort_setting;
15+
16+
constructor(data: any) {
17+
super();
18+
this.content_type_sub_menu_items = data.contentTypeSubMenuItems.map((item: any) => ({
19+
endpoint: new NavigationEndpoint(item.navigationEndpoint || item.endpoint),
20+
selected: item.selected,
21+
title: item.title
22+
}));
23+
this.sort_setting = Parser.parseItem(data.sortSetting);
24+
}
25+
}
26+
27+
export default ChannelSubMenu;

src/parser/classes/SectionList.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class SectionList extends YTNode {
88
contents;
99
continuation?: string;
1010
header;
11+
sub_menu;
1112

1213
constructor(data: any) {
1314
super();
@@ -29,6 +30,10 @@ class SectionList extends YTNode {
2930
if (data.header) {
3031
this.header = Parser.parse(data.header);
3132
}
33+
34+
if (data.subMenu) {
35+
this.sub_menu = Parser.parseItem(data.subMenu);
36+
}
3237
}
3338
}
3439

src/parser/classes/SortFilterSubMenu.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class SortFilterSubMenu extends YTNode {
4141
title: item.title,
4242
selected: item.selected,
4343
continuation: item.continuation?.reloadContinuationData?.continuation,
44-
endpoint: new NavigationEndpoint(item.serviceEndpoint),
44+
endpoint: new NavigationEndpoint(item.serviceEndpoint || item.navigationEndpoint),
4545
subtitle: item.subtitle || null
4646
}));
4747
}

src/parser/map.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { default as ChannelHeaderLinks } from './classes/ChannelHeaderLinks';
3737
import { default as ChannelMetadata } from './classes/ChannelMetadata';
3838
import { default as ChannelMobileHeader } from './classes/ChannelMobileHeader';
3939
import { default as ChannelOptions } from './classes/ChannelOptions';
40+
import { default as ChannelSubMenu } from './classes/ChannelSubMenu';
4041
import { default as ChannelThumbnailWithLink } from './classes/ChannelThumbnailWithLink';
4142
import { default as ChannelVideoPlayer } from './classes/ChannelVideoPlayer';
4243
import { default as Chapter } from './classes/Chapter';
@@ -354,6 +355,7 @@ export const YTNodes = {
354355
ChannelMetadata,
355356
ChannelMobileHeader,
356357
ChannelOptions,
358+
ChannelSubMenu,
357359
ChannelThumbnailWithLink,
358360
ChannelVideoPlayer,
359361
Chapter,

src/parser/youtube/Channel.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import ChannelMetadata from '../classes/ChannelMetadata';
77
import InteractiveTabbedHeader from '../classes/InteractiveTabbedHeader';
88
import MicroformatData from '../classes/MicroformatData';
99
import SubscribeButton from '../classes/SubscribeButton';
10+
import ExpandableTab from '../classes/ExpandableTab';
11+
import SectionList from '../classes/SectionList';
1012
import Tab from '../classes/Tab';
1113

1214
import Feed from '../../core/Feed';
1315
import FilterableFeed from '../../core/FilterableFeed';
1416
import ChipCloudChip from '../classes/ChipCloudChip';
15-
import ExpandableTab from '../classes/ExpandableTab';
1617
import FeedFilterChipBar from '../classes/FeedFilterChipBar';
18+
import ChannelSubMenu from '../classes/ChannelSubMenu';
19+
import SortFilterSubMenu from '../classes/SortFilterSubMenu';
20+
1721
import { InnertubeError } from '../../utils/Utils';
1822

1923
import type { AppendContinuationItemsAction, ReloadContinuationItemsCommand } from '..';
@@ -49,7 +53,7 @@ export default class Channel extends TabbedFeed {
4953
async applyFilter(filter: string | ChipCloudChip): Promise<FilteredChannelList> {
5054
let target_filter: ChipCloudChip | undefined;
5155

52-
const filter_chipbar = this.memo.getType(FeedFilterChipBar)?.[0];
56+
const filter_chipbar = this.memo.getType(FeedFilterChipBar).first();
5357

5458
if (typeof filter === 'string') {
5559
target_filter = filter_chipbar?.contents.get({ text: filter });
@@ -63,13 +67,70 @@ export default class Channel extends TabbedFeed {
6367
throw new InnertubeError('Invalid filter', filter);
6468

6569
const page = await target_filter.endpoint?.call(this.actions, { parse: true });
70+
6671
return new FilteredChannelList(this.actions, page, true);
6772
}
6873

74+
/**
75+
* Applies given sort filter to the list. Use {@link sort_filters} to get available filters.
76+
* @param sort - The sort filter to apply
77+
*/
78+
async applySort(sort: string): Promise<Channel> {
79+
const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu).first();
80+
81+
if (!sort_filter_sub_menu)
82+
throw new InnertubeError('No sort filter sub menu found');
83+
84+
const target_sort = sort_filter_sub_menu?.sub_menu_items?.find((item) => item.title === sort);
85+
86+
if (!target_sort)
87+
throw new InnertubeError(`Sort filter ${sort} not found`, { available_sort_filters: this.sort_filters });
88+
89+
if (target_sort.selected)
90+
return this;
91+
92+
const page = await target_sort.endpoint?.call(this.actions, { parse: true });
93+
94+
return new Channel(this.actions, page, true);
95+
}
96+
97+
/**
98+
* Applies given content type filter to the list. Use {@link content_type_filters} to get available filters.
99+
* @param content_type_filter - The content type filter to apply
100+
*/
101+
async applyContentTypeFilter(content_type_filter: string): Promise<Channel> {
102+
const sub_menu = this.current_tab?.content?.as(SectionList).sub_menu?.as(ChannelSubMenu);
103+
104+
if (!sub_menu)
105+
throw new InnertubeError('Sub menu not found');
106+
107+
const item = sub_menu.content_type_sub_menu_items.find((item) => item.title === content_type_filter);
108+
109+
if (!item)
110+
throw new InnertubeError(`Sub menu item ${content_type_filter} not found`, { available_filters: this.content_type_filters });
111+
112+
if (item.selected)
113+
return this;
114+
115+
const page = await item.endpoint?.call(this.actions, { parse: true });
116+
117+
return new Channel(this.actions, page, true);
118+
}
119+
69120
get filters(): string[] {
70121
return this.memo.getType(FeedFilterChipBar)?.[0]?.contents.filterType(ChipCloudChip).map((chip) => chip.text) || [];
71122
}
72123

124+
get sort_filters(): string[] {
125+
const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu).first();
126+
return sort_filter_sub_menu?.sub_menu_items?.map((item) => item.title) || [];
127+
}
128+
129+
get content_type_filters(): string[] {
130+
const sub_menu = this.current_tab?.content?.as(SectionList).sub_menu?.as(ChannelSubMenu);
131+
return sub_menu?.content_type_sub_menu_items.map((item) => item.title) || [];
132+
}
133+
73134
async getHome(): Promise<Channel> {
74135
const tab = await this.getTabByURL('featured');
75136
return new Channel(this.actions, tab.page, true);

0 commit comments

Comments
 (0)