diff --git a/README.md b/README.md index 0e42fd9b..20f4de5b 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,32 @@ See [`./examples/comments`](https://github.com/LuanRT/YouTube.js/blob/main/examp ### getHomeFeed() Retrieves YouTube's home feed. -**Returns**: `Promise.` +**Returns**: `Promise.` + +
+Methods & Getters +

+ +- `#videos` + - Returns all videos in the home feed. + +- `#posts` + - Returns all posts in the home feed. + +- `#shelfs` + - Returns all shelfs in the home feed. + +- `#filters` + - Returns available filters. + +- `#applyFilter(name | ChipCloudChip)` + - Applies given filter and returns a new HomeFeed instance. + +- `#getContinuation()` + - Retrieves feed continuation. + +

+
### getLibrary() diff --git a/src/Innertube.ts b/src/Innertube.ts index bf5db46d..fd1798a8 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -17,10 +17,10 @@ import { ActionsResponse } from './core/Actions'; import Feed from './core/Feed'; import YTMusic from './core/Music'; import Studio from './core/Studio'; +import HomeFeed from './parser/youtube/HomeFeed'; import AccountManager from './core/AccountManager'; import PlaylistManager from './core/PlaylistManager'; import InteractionManager from './core/InteractionManager'; -import FilterableFeed from './core/FilterableFeed'; import TabbedFeed from './core/TabbedFeed'; import Constants from './utils/Constants'; import Proto from './proto/index'; @@ -155,7 +155,7 @@ class Innertube { */ async getHomeFeed() { const response = await this.actions.execute('/browse', { browseId: 'FEwhat_to_watch' }); - return new FilterableFeed(this.actions, response.data); + return new HomeFeed(this.actions, response.data); } /** diff --git a/src/core/Feed.ts b/src/core/Feed.ts index eaa94c5c..17b10108 100644 --- a/src/core/Feed.ts +++ b/src/core/Feed.ts @@ -115,7 +115,7 @@ class Feed { /** * Returns contents from the page. */ - get contents() { + get page_contents() { 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]; diff --git a/src/core/FilterableFeed.ts b/src/core/FilterableFeed.ts index cbbc63f8..929d6b92 100644 --- a/src/core/FilterableFeed.ts +++ b/src/core/FilterableFeed.ts @@ -13,7 +13,7 @@ class FilterableFeed extends Feed { } /** - * Get filters for the feed + * Returns the filter chips. */ get filter_chips() { if (this.#chips) @@ -30,6 +30,9 @@ class FilterableFeed extends Feed { return this.#chips || []; } + /** + * Returns available filters. + */ get filters() { return this.filter_chips.map((chip) => chip.text.toString()) || []; } @@ -42,9 +45,7 @@ class FilterableFeed extends Feed { if (typeof filter === 'string') { if (!this.filters.includes(filter)) - throw new InnertubeError('Filter not found', { - available_filters: this.filters - }); + 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; @@ -54,6 +55,7 @@ class FilterableFeed extends Feed { if (!target_filter) throw new InnertubeError('Filter not found'); + if (target_filter.is_selected) return this; diff --git a/src/parser/classes/FeedFilterChipBar.ts b/src/parser/classes/FeedFilterChipBar.ts index 6983ac82..2f315c04 100644 --- a/src/parser/classes/FeedFilterChipBar.ts +++ b/src/parser/classes/FeedFilterChipBar.ts @@ -1,15 +1,14 @@ import Parser from '../index'; import { YTNode } from '../helpers'; +import ChipCloudChip from './ChipCloudChip'; -class FeedFilterChipBar extends YTNode { +export default class FeedFilterChipBar extends YTNode { static type = 'FeedFilterChipBar'; contents; constructor(data: any) { super(); - this.contents = Parser.parse(data.contents); + this.contents = Parser.parseArray(data.contents, ChipCloudChip); } -} - -export default FeedFilterChipBar; \ No newline at end of file +} \ No newline at end of file diff --git a/src/parser/classes/RichGrid.ts b/src/parser/classes/RichGrid.ts index 06d75698..db12b067 100644 --- a/src/parser/classes/RichGrid.ts +++ b/src/parser/classes/RichGrid.ts @@ -12,8 +12,8 @@ class RichGrid extends YTNode { super(); // XXX: we don't parse the masthead since it is usually an advertisement // XXX: reflowOptions aren't parsed, I think its only used internally for layout - this.header = Parser.parse(data.header); - this.contents = Parser.parse(data.contents); + this.header = Parser.parseItem(data.header); + this.contents = Parser.parseArray(data.contents); } } diff --git a/src/parser/classes/RichItem.ts b/src/parser/classes/RichItem.ts index 6289322d..df4c3f82 100644 --- a/src/parser/classes/RichItem.ts +++ b/src/parser/classes/RichItem.ts @@ -8,8 +8,7 @@ class RichItem extends YTNode { constructor(data: any) { super(); - // TODO: check this - this.content = Parser.parse(data.content); + this.content = Parser.parseItem(data.content); } } diff --git a/src/parser/classes/RichSection.ts b/src/parser/classes/RichSection.ts index f17eb41a..5ff292ba 100644 --- a/src/parser/classes/RichSection.ts +++ b/src/parser/classes/RichSection.ts @@ -4,11 +4,11 @@ import { YTNode } from '../helpers'; class RichSection extends YTNode { static type = 'RichSection'; - contents; + content; constructor(data: any) { super(); - this.contents = Parser.parse(data.content); + this.content = Parser.parseItem(data.content); } } diff --git a/src/parser/classes/RichShelf.ts b/src/parser/classes/RichShelf.ts index 9fb870e9..cf1ab91e 100644 --- a/src/parser/classes/RichShelf.ts +++ b/src/parser/classes/RichShelf.ts @@ -13,7 +13,7 @@ class RichShelf extends YTNode { constructor(data: any) { super(); this.title = new Text(data.title); - this.contents = Parser.parse(data.contents); + this.contents = Parser.parseArray(data.contents); this.endpoint = data.endpoint ? new NavigationEndpoint(data.endpoint) : null; } } diff --git a/src/parser/youtube/HomeFeed.ts b/src/parser/youtube/HomeFeed.ts new file mode 100644 index 00000000..4010bcad --- /dev/null +++ b/src/parser/youtube/HomeFeed.ts @@ -0,0 +1,42 @@ +import Actions from '../../core/Actions'; +import FilterableFeed from '../../core/FilterableFeed'; +import ChipCloudChip from '../classes/ChipCloudChip'; +import FeedTabbedHeader from '../classes/FeedTabbedHeader'; +import RichGrid from '../classes/RichGrid'; + +import { ReloadContinuationItemsCommand, AppendContinuationItemsAction } from '..'; + +export default class HomeFeed extends FilterableFeed { + contents: RichGrid | AppendContinuationItemsAction | ReloadContinuationItemsCommand; + header: FeedTabbedHeader; + + constructor(actions: Actions, data: any, already_parsed = false) { + super(actions, data, already_parsed); + this.header = this.memo.getType(FeedTabbedHeader)?.[0]; + this.contents = + this.memo.getType(RichGrid)?.[0] || + this.page.on_response_received_actions?.[0]; + } + + /** + * Applies given filter to the feed. + * @param filter - Filter to apply. + */ + async applyFilter(filter: string | ChipCloudChip): Promise { + const feed = await super.getFilteredFeed(filter); + return new HomeFeed(this.actions, feed.page, true); + } + + /** + * Retrieves next batch of contents. + */ + async getContinuation(): Promise { + const feed = await super.getContinuation(); + + // Keep the page header + feed.page.header = this.page.header; + feed.page.header_memo.set(this.header.type, [ this.header ]); + + return new HomeFeed(this.actions, feed.page, true); + } +} \ No newline at end of file diff --git a/test/main.test.ts b/test/main.test.ts index d1b8fbc2..86503057 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -92,6 +92,8 @@ describe('YouTube.js Tests', () => { it('should retrieve home feed', async () => { const homefeed = await yt.getHomeFeed(); + expect(homefeed.header).toBeDefined(); + expect(homefeed.contents).toBeDefined(); expect(homefeed.videos.length).toBeGreaterThan(0); });