From 2cc7b8bcd6938c7fb3af4f854a1d78b86d153873 Mon Sep 17 00:00:00 2001 From: Patrick Kan <55383971+patrickkfkan@users.noreply.github.com> Date: Sat, 4 Mar 2023 17:23:17 +0800 Subject: [PATCH] feat(yt): add `getGuide()` (#335) * feat(yt): add `getGuide()` * chore: lint * fix(Guide): wrong prop * fix(Guide): include subscription section * fix(Guide): wrong import * feat(Guide): add `page` --- README.md | 7 ++++ src/Innertube.ts | 9 +++++ src/parser/classes/GuideCollapsibleEntry.ts | 35 +++++++++++++++++++ .../classes/GuideCollapsibleSectionEntry.ts | 23 ++++++++++++ src/parser/classes/GuideDownloadsEntry.ts | 14 ++++++++ src/parser/classes/GuideEntry.ts | 33 +++++++++++++++++ src/parser/classes/GuideSection.ts | 20 +++++++++++ .../classes/GuideSubscriptionsSection.ts | 7 ++++ src/parser/map.ts | 18 ++++++++++ src/parser/parser.ts | 11 +++++- src/parser/types/ParsedResponse.ts | 7 ++++ src/parser/types/RawResponse.ts | 1 + src/parser/youtube/Guide.ts | 20 +++++++++++ 13 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/parser/classes/GuideCollapsibleEntry.ts create mode 100644 src/parser/classes/GuideCollapsibleSectionEntry.ts create mode 100644 src/parser/classes/GuideDownloadsEntry.ts create mode 100644 src/parser/classes/GuideEntry.ts create mode 100644 src/parser/classes/GuideSection.ts create mode 100644 src/parser/classes/GuideSubscriptionsSection.ts create mode 100644 src/parser/youtube/Guide.ts diff --git a/README.md b/README.md index 000ec116b..a3c77b054 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,7 @@ const yt = await Innertube.create({ * [.getSearchSuggestions(query)](#getsearchsuggestions) * [.getComments(video_id, sort_by?)](#getcomments) * [.getHomeFeed()](#gethomefeed) + * [.getGuide()](#getguide) * [.getLibrary()](#getlibrary) * [.getHistory()](#gethistory) * [.getTrending()](#gettrending) @@ -426,6 +427,12 @@ Retrieves YouTube's home feed.

+ +### getGuide() +Retrieves YouTube's content guide. + +**Returns**: `Promise` + ### getLibrary() Retrieves the account's library. diff --git a/src/Innertube.ts b/src/Innertube.ts index 6bb0ac3e2..18fd36c4e 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -21,6 +21,7 @@ import PlaylistManager from './core/PlaylistManager.js'; import YTStudio from './core/Studio.js'; import TabbedFeed from './core/TabbedFeed.js'; import HomeFeed from './parser/youtube/HomeFeed.js'; +import Guide from './parser/youtube/Guide.js'; import Proto from './proto/index.js'; import Constants from './utils/Constants.js'; @@ -170,6 +171,14 @@ class Innertube { return new HomeFeed(this.actions, response); } + /** + * Retrieves YouTube's content guide. + */ + async getGuide(): Promise { + const response = await this.actions.execute('/guide'); + return new Guide(response.data); + } + /** * Returns the account's library. */ diff --git a/src/parser/classes/GuideCollapsibleEntry.ts b/src/parser/classes/GuideCollapsibleEntry.ts new file mode 100644 index 000000000..cece67e62 --- /dev/null +++ b/src/parser/classes/GuideCollapsibleEntry.ts @@ -0,0 +1,35 @@ +import Text from './misc/Text.js'; +import { YTNode } from '../helpers.js'; +import Parser from '../parser.js'; + +class GuideCollapsibleEntry extends YTNode { + static type = 'GuideCollapsibleEntry'; + + expander_item: { + title: string, + icon_type: string + }; + collapser_item: { + title: string, + icon_type: string + }; + expandable_items; + + constructor(data: any) { + super(); + + this.expander_item = { + title: new Text(data.expanderItem.guideEntryRenderer.formattedTitle).toString(), + icon_type: data.expanderItem.guideEntryRenderer.icon.iconType + }; + + this.collapser_item = { + title: new Text(data.collapserItem.guideEntryRenderer.formattedTitle).toString(), + icon_type: data.collapserItem.guideEntryRenderer.icon.iconType + }; + + this.expandable_items = Parser.parseArray(data.expandableItems); + } +} + +export default GuideCollapsibleEntry; \ No newline at end of file diff --git a/src/parser/classes/GuideCollapsibleSectionEntry.ts b/src/parser/classes/GuideCollapsibleSectionEntry.ts new file mode 100644 index 000000000..aaab66ab9 --- /dev/null +++ b/src/parser/classes/GuideCollapsibleSectionEntry.ts @@ -0,0 +1,23 @@ +import { YTNode } from '../helpers.js'; +import Parser from '../parser.js'; + +class GuideCollapsibleSectionEntry extends YTNode { + static type = 'GuideCollapsibleSectionEntry'; + + header_entry; + expander_icon: string; + collapser_icon: string; + section_items; + + constructor(data: any) { + super(); + + this.header_entry = Parser.parseItem(data.headerEntry); + this.expander_icon = data.expanderIcon.iconType; + this.collapser_icon = data.collapserIcon.iconType; + this.section_items = Parser.parseArray(data.sectionItems); + + } +} + +export default GuideCollapsibleSectionEntry; \ No newline at end of file diff --git a/src/parser/classes/GuideDownloadsEntry.ts b/src/parser/classes/GuideDownloadsEntry.ts new file mode 100644 index 000000000..8f06a1063 --- /dev/null +++ b/src/parser/classes/GuideDownloadsEntry.ts @@ -0,0 +1,14 @@ +import GuideEntry from './GuideEntry.js'; + +class GuideDownloadsEntry extends GuideEntry { + static type = 'GuideDownloadsEntry'; + + always_show: boolean; + + constructor(data: any) { + super(data.entryRenderer.guideEntryRenderer); + this.always_show = !!data.alwaysShow; + } +} + +export default GuideDownloadsEntry; \ No newline at end of file diff --git a/src/parser/classes/GuideEntry.ts b/src/parser/classes/GuideEntry.ts new file mode 100644 index 000000000..593740111 --- /dev/null +++ b/src/parser/classes/GuideEntry.ts @@ -0,0 +1,33 @@ +import Text from './misc/Text.js'; +import NavigationEndpoint from './NavigationEndpoint.js'; +import { YTNode } from '../helpers.js'; +import Thumbnail from './misc/Thumbnail.js'; + +class GuideEntry extends YTNode { + static type = 'GuideEntry'; + + title: Text; + endpoint: NavigationEndpoint; + icon_type?: string; + thumbnails?: Thumbnail[]; + badges?: any; + is_primary: boolean; + + constructor(data: any) { + super(); + this.title = new Text(data.formattedTitle); + this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint); + if (data.icon?.iconType) { + this.icon_type = data.icon.iconType; + } + if (data.thumbnail) { + this.thumbnails = Thumbnail.fromResponse(data.thumbnail); + } + if (data.badges) { + this.badges = data.badges; + } + this.is_primary = !!data.isPrimary; + } +} + +export default GuideEntry; \ No newline at end of file diff --git a/src/parser/classes/GuideSection.ts b/src/parser/classes/GuideSection.ts new file mode 100644 index 000000000..2bb0f4d08 --- /dev/null +++ b/src/parser/classes/GuideSection.ts @@ -0,0 +1,20 @@ +import Text from './misc/Text.js'; +import { YTNode } from '../helpers.js'; +import Parser from '../parser.js'; + +class GuideSection extends YTNode { + static type = 'GuideSection'; + + title?: Text; + items; + + constructor(data: any) { + super(); + if (data.formattedTitle) { + this.title = new Text(data.formattedTitle); + } + this.items = Parser.parseArray(data.items); + } +} + +export default GuideSection; \ No newline at end of file diff --git a/src/parser/classes/GuideSubscriptionsSection.ts b/src/parser/classes/GuideSubscriptionsSection.ts new file mode 100644 index 000000000..6cd053f4d --- /dev/null +++ b/src/parser/classes/GuideSubscriptionsSection.ts @@ -0,0 +1,7 @@ +import GuideSection from './GuideSection.js'; + +class GuideSubscriptionsSection extends GuideSection { + static type = 'GuideSubscriptionsSection'; +} + +export default GuideSubscriptionsSection; \ No newline at end of file diff --git a/src/parser/map.ts b/src/parser/map.ts index 3fbc46517..a9fc75d03 100644 --- a/src/parser/map.ts +++ b/src/parser/map.ts @@ -196,6 +196,18 @@ import { default as GridPlaylist } from './classes/GridPlaylist.js'; export { GridPlaylist }; import { default as GridVideo } from './classes/GridVideo.js'; export { GridVideo }; +import { default as GuideCollapsibleEntry } from './classes/GuideCollapsibleEntry.js'; +export { GuideCollapsibleEntry }; +import { default as GuideCollapsibleSectionEntry } from './classes/GuideCollapsibleSectionEntry.js'; +export { GuideCollapsibleSectionEntry }; +import { default as GuideDownloadsEntry } from './classes/GuideDownloadsEntry.js'; +export { GuideDownloadsEntry }; +import { default as GuideEntry } from './classes/GuideEntry.js'; +export { GuideEntry }; +import { default as GuideSection } from './classes/GuideSection.js'; +export { GuideSection }; +import { default as GuideSubscriptionsSection } from './classes/GuideSubscriptionsSection.js'; +export { GuideSubscriptionsSection }; import { default as HashtagHeader } from './classes/HashtagHeader.js'; export { HashtagHeader }; import { default as Heatmap } from './classes/Heatmap.js'; @@ -757,6 +769,12 @@ const map: Record = { GridHeader, GridPlaylist, GridVideo, + GuideCollapsibleEntry, + GuideCollapsibleSectionEntry, + GuideDownloadsEntry, + GuideEntry, + GuideSection, + GuideSubscriptionsSection, HashtagHeader, Heatmap, HeatMarker, diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 88aaa68e2..0344eb343 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -262,6 +262,14 @@ export default class Parser { parsed_data.cards = cards; } + this.#createMemo(); + const items = this.parse(data.items); + if (items) { + parsed_data.items = items; + parsed_data.items_memo = this.#getMemo(); + } + this.#clearMemo(); + return parsed_data; } @@ -481,7 +489,8 @@ export default class Parser { 'RunAttestationCommand', 'CompactPromotedVideo', 'StatementBanner', - 'SearchSubMenu' + 'SearchSubMenu', + 'GuideSigninPromo' ]); static shouldIgnore(classname: string) { diff --git a/src/parser/types/ParsedResponse.ts b/src/parser/types/ParsedResponse.ts index bf69a47b1..709efd714 100644 --- a/src/parser/types/ParsedResponse.ts +++ b/src/parser/types/ParsedResponse.ts @@ -29,6 +29,7 @@ export interface IParsedResponse { sidebar_memo?: Memo; live_chat_item_context_menu_supported_renderers?: YTNode; live_chat_item_context_menu_supported_renderers_memo?: Memo; + items_memo?: Memo; on_response_received_actions?: ObservedArray; on_response_received_actions_memo?: Memo; on_response_received_endpoints?: ObservedArray; @@ -72,6 +73,7 @@ export interface IParsedResponse { storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec; endscreen?: Endscreen; cards?: CardCollection; + items?: SuperParsedResult; } export interface IPlayerResponse { @@ -156,4 +158,9 @@ export interface IUpdatedMetadataResponse { actions: SuperParsedResult; actions_memo: Memo; continuation?: Continuation; +} + +export interface IGuideResponse { + items: SuperParsedResult; + items_memo: Memo; } \ No newline at end of file diff --git a/src/parser/types/RawResponse.ts b/src/parser/types/RawResponse.ts index 08d950ac1..92fe7eadc 100644 --- a/src/parser/types/RawResponse.ts +++ b/src/parser/types/RawResponse.ts @@ -51,5 +51,6 @@ export interface IRawResponse { storyboards?: RawNode; endscreen?: RawNode; cards?: RawNode; + items?: RawNode[]; frameworkUpdates?: any; } \ No newline at end of file diff --git a/src/parser/youtube/Guide.ts b/src/parser/youtube/Guide.ts new file mode 100644 index 000000000..4e261bcef --- /dev/null +++ b/src/parser/youtube/Guide.ts @@ -0,0 +1,20 @@ +import type { IGuideResponse } from '../types/ParsedResponse.js'; +import { IRawResponse, Parser } from '../index.js'; +import { ObservedArray } from '../helpers.js'; +import GuideSection from '../classes/GuideSection.js'; +import GuideSubscriptionsSection from '../classes/GuideSubscriptionsSection.js'; + +export default class Guide { + + #page: IGuideResponse; + contents: ObservedArray; + + constructor(data: IRawResponse) { + this.#page = Parser.parseResponse(data); + this.contents = this.#page.items.array().as(GuideSection, GuideSubscriptionsSection); + } + + get page(): IGuideResponse { + return this.#page; + } +} \ No newline at end of file