Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Channel): add support for sorting the playlist tab #295

Merged
merged 1 commit into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,13 @@ Retrieves contents for a given channel.
- `<channel>#getChannels()`
- `<channel>#getAbout()`
- `<channel>#search(query)`
- `<channel>#applyFilter(filter)`
- `<channel>#applyContentTypeFilter(content_type_filter)`
- `<channel>#applySort(sort)`
- `<channel>#getContinuation()`
- `<channel>#filters`
- `<channel>#content_type_filters`
- `<channel>#sort_filters`
- `<channel>#page`

</p>
Expand Down
27 changes: 27 additions & 0 deletions src/parser/classes/ChannelSubMenu.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions src/parser/classes/SectionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class SectionList extends YTNode {
contents;
continuation?: string;
header;
sub_menu;

constructor(data: any) {
super();
Expand All @@ -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);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/parser/classes/SortFilterSubMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}));
}
Expand Down
2 changes: 2 additions & 0 deletions src/parser/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -354,6 +355,7 @@ export const YTNodes = {
ChannelMetadata,
ChannelMobileHeader,
ChannelOptions,
ChannelSubMenu,
ChannelThumbnailWithLink,
ChannelVideoPlayer,
Chapter,
Expand Down
65 changes: 63 additions & 2 deletions src/parser/youtube/Channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '..';
Expand Down Expand Up @@ -49,7 +53,7 @@ export default class Channel extends TabbedFeed {
async applyFilter(filter: string | ChipCloudChip): Promise<FilteredChannelList> {
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 });
Expand All @@ -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<Channel> {
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<Channel> {
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<Channel> {
const tab = await this.getTabByURL('featured');
return new Channel(this.actions, tab.page, true);
Expand Down