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