From d69d70186925e6b5c97cad29ea6f6c7a6d1d2f6c Mon Sep 17 00:00:00 2001 From: LuanRT Date: Thu, 5 Jan 2023 19:09:16 -0300 Subject: [PATCH] fix(VideoInfo): watch next feed not being parsed when logged out (#276) --- .../classes/TwoColumnWatchNextResults.ts | 6 ++-- src/parser/helpers.ts | 9 ++++++ src/parser/youtube/VideoInfo.ts | 30 ++++++++++++++----- test/main.test.ts | 4 +++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/parser/classes/TwoColumnWatchNextResults.ts b/src/parser/classes/TwoColumnWatchNextResults.ts index 10a5c4bf..cab2450c 100644 --- a/src/parser/classes/TwoColumnWatchNextResults.ts +++ b/src/parser/classes/TwoColumnWatchNextResults.ts @@ -10,9 +10,9 @@ class TwoColumnWatchNextResults extends YTNode { constructor(data: any) { super(); - this.results = Parser.parse(data.results?.results.contents, true); - this.secondary_results = Parser.parse(data.secondaryResults?.secondaryResults.results, true); - this.conversation_bar = Parser.parse(data?.conversationBar); + this.results = Parser.parseArray(data.results?.results.contents); + this.secondary_results = Parser.parseArray(data.secondaryResults?.secondaryResults.results); + this.conversation_bar = Parser.parseItem(data?.conversationBar); } } diff --git a/src/parser/helpers.ts b/src/parser/helpers.ts index a3f8e6b9..564920d1 100644 --- a/src/parser/helpers.ts +++ b/src/parser/helpers.ts @@ -375,6 +375,10 @@ export type ObservedArray = Array & { * Get the first of a specific type */ firstOfType[]>(...types: K): InstanceType | undefined; + /** + * Get the first item + */ + first: () => T | undefined; /** * This is similar to filter but throws if there's a type mismatch. */ @@ -435,6 +439,7 @@ export function observe(obj: Array): ObservedArray { }; } + if (prop == 'firstOfType') { return (...types: YTNodeConstructor[]) => { return target.find((node: YTNode) => { @@ -445,6 +450,10 @@ export function observe(obj: Array): ObservedArray { }; } + if (prop == 'first') { + return () => target[0]; + } + if (prop == 'as') { return (...types: YTNodeConstructor[]) => { return observe(target.map((node: YTNode) => { diff --git a/src/parser/youtube/VideoInfo.ts b/src/parser/youtube/VideoInfo.ts index 0baefb58..599c9eda 100644 --- a/src/parser/youtube/VideoInfo.ts +++ b/src/parser/youtube/VideoInfo.ts @@ -124,11 +124,11 @@ class VideoInfo { this.basic_info = { // This type is inferred so no need for an explicit type ...info.video_details, + /** + * Microformat is a bit redundant, so only + * a few things there are interesting to us. + */ ...{ - /** - * Microformat is a bit redundant, so only - * a few things there are interesting to us. - */ embed: info.microformat?.is(PlayerMicroformat) ? info.microformat?.embed : null, channel: info.microformat?.is(PlayerMicroformat) ? info.microformat?.channel : null, is_unlisted: info.microformat?.is_unlisted, @@ -162,7 +162,7 @@ class VideoInfo { this.merchandise = results.firstOfType(MerchandiseShelf); this.related_chip_cloud = secondary_results.firstOfType(RelatedChipCloud)?.content.item().as(ChipCloud); - this.watch_next_feed = secondary_results.firstOfType(ItemSection)?.contents; + this.watch_next_feed = actions.session.logged_in ? secondary_results.firstOfType(ItemSection)?.contents : secondary_results; if (this.watch_next_feed && Array.isArray(this.watch_next_feed)) this.#watch_next_continuation = this.watch_next_feed.pop()?.as(ContinuationItem); @@ -178,7 +178,7 @@ class VideoInfo { const comments_entry_point = results.get({ target_id: 'comments-entry-point' })?.as(ItemSection); this.comments_entry_point_header = comments_entry_point?.contents?.firstOfType(CommentsEntryPointHeader); - this.livechat = next?.contents_memo.getType(LiveChat)?.[0]; + this.livechat = next?.contents_memo.getType(LiveChat).first(); } } @@ -187,6 +187,9 @@ class VideoInfo { * @param target_filter - Filter to apply. */ async selectFilter(target_filter: string | ChipCloudChip | undefined): Promise { + if (!this.related_chip_cloud) + throw new InnertubeError('Chip cloud not found, cannot apply filter'); + let cloud_chip: ChipCloudChip; if (typeof target_filter === 'string') { @@ -236,15 +239,19 @@ class VideoInfo { return response; } + /** * Retrieves watch next feed continuation. */ async getWatchNextContinuation(): Promise { + if (!this.#watch_next_continuation) + throw new InnertubeError('Watch next feed continuation not found'); + const response = await this.#watch_next_continuation?.endpoint.call(this.#actions, { parse: true }); const data = response?.on_response_received_endpoints?.get({ type: 'appendContinuationItemsAction' }); if (!data) - throw new InnertubeError('Continuation not found'); + throw new InnertubeError('AppendContinuationItemsAction not found'); this.watch_next_feed = data?.contents; this.#watch_next_continuation = this.watch_next_feed?.pop()?.as(ContinuationItem); @@ -342,10 +349,17 @@ class VideoInfo { return this.#cpn; } + /** + * Checks if continuation is available for the watch next feed. + */ + get wn_has_continuation(): boolean { + return !!this.#watch_next_continuation; + } + /** * Original parsed InnerTube response. */ - get page(): [ ParsedResponse, ParsedResponse? ] { + get page(): [ParsedResponse, ParsedResponse?] { return this.#page; } diff --git a/test/main.test.ts b/test/main.test.ts index 4ebffb8c..0a86abff 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -40,6 +40,10 @@ describe('YouTube.js Tests', () => { expect(heatmap).toBeDefined(); }); + it('should have watch next feed', () => { + expect(info.watch_next_feed).toBeDefined(); + }); + it('should retrieve basic video info', async () => { const b_info = await yt.getBasicInfo(VIDEOS[0].ID); expect(b_info.basic_info.id).toBe(VIDEOS[0].ID);