From 7397aa3f6425cb2f3dcc625502fd1ce5a5db6db3 Mon Sep 17 00:00:00 2001 From: Luan Date: Thu, 21 Nov 2024 18:24:16 -0300 Subject: [PATCH] refactor(parser)!: Implement endpoint/command parsers (#812) --- eslint.config.js | 1 - src/Innertube.ts | 236 +++++----- src/core/Actions.ts | 19 +- src/core/OAuth2.ts | 28 +- src/core/clients/Kids.ts | 85 ++-- src/core/clients/Music.ts | 214 +++------- src/core/clients/Studio.ts | 59 ++- src/core/endpoints/BrowseEndpoint.ts | 19 - .../endpoints/GetNotificationMenuEndpoint.ts | 16 - src/core/endpoints/GuideEndpoint.ts | 1 - src/core/endpoints/NextEndpoint.ts | 21 - src/core/endpoints/PlayerEndpoint.ts | 54 --- src/core/endpoints/ResolveURLEndpoint.ts | 16 - src/core/endpoints/SearchEndpoint.ts | 19 - .../endpoints/account/AccountListEndpoint.ts | 14 - src/core/endpoints/account/index.ts | 1 - .../endpoints/browse/EditPlaylistEndpoint.ts | 24 -- src/core/endpoints/browse/index.ts | 1 - .../channel/EditDescriptionEndpoint.ts | 15 - .../endpoints/channel/EditNameEndpoint.ts | 15 - src/core/endpoints/channel/index.ts | 2 - .../comment/CreateCommentEndpoint.ts | 18 - .../comment/PerformCommentActionEndpoint.ts | 17 - src/core/endpoints/comment/index.ts | 2 - src/core/endpoints/index.ts | 20 - .../endpoints/kids/BlocklistPickerEndpoint.ts | 12 - src/core/endpoints/kids/index.ts | 1 - src/core/endpoints/like/DislikeEndpoint.ts | 19 - src/core/endpoints/like/LikeEndpoint.ts | 19 - src/core/endpoints/like/RemoveLikeEndpoint.ts | 19 - src/core/endpoints/like/index.ts | 3 - .../music/GetSearchSuggestionsEndpoint.ts | 15 - src/core/endpoints/music/index.ts | 1 - .../notification/GetUnseenCountEndpoint.ts | 1 - .../ModifyChannelPreferenceEndpoint.ts | 17 - src/core/endpoints/notification/index.ts | 2 - src/core/endpoints/playlist/CreateEndpoint.ts | 15 - src/core/endpoints/playlist/DeleteEndpoint.ts | 14 - src/core/endpoints/playlist/index.ts | 2 - .../endpoints/reel/ReelItemWatchEndpoint.ts | 20 - .../reel/ReelWatchSequenceEndpoint.ts | 14 - src/core/endpoints/reel/index.ts | 2 - .../subscription/SubscribeEndpoint.ts | 18 - .../subscription/UnsubscribeEndpoint.ts | 18 - src/core/endpoints/subscription/index.ts | 2 - .../endpoints/upload/CreateVideoEndpoint.ts | 37 -- src/core/endpoints/upload/index.ts | 1 - src/core/index.ts | 1 - src/core/managers/AccountManager.ts | 121 +----- src/core/managers/InteractionManager.ts | 117 +++-- src/core/managers/PlaylistManager.ts | 124 +++--- src/parser/classes/ContentMetadataView.ts | 2 +- .../classes/CreatePlaylistDialogFormView.ts | 25 ++ src/parser/classes/DialogHeaderView.ts | 14 + src/parser/classes/DialogView.ts | 20 + src/parser/classes/DropdownView.ts | 46 ++ src/parser/classes/FormFooterView.ts | 18 + src/parser/classes/NavigationEndpoint.ts | 38 +- src/parser/classes/PanelFooterView.ts | 18 + .../classes/PlaylistSidebarPrimaryInfo.ts | 12 +- src/parser/classes/PremiereTrailerBadge.ts | 14 + src/parser/classes/SharedPost.ts | 23 +- src/parser/classes/TextFieldView.ts | 62 +++ src/parser/classes/UnifiedSharePanel.ts | 20 +- .../classes/actions/SendFeedbackAction.ts | 13 + .../classes/commands/AddToPlaylistCommand.ts | 22 + .../commands/CommandExecutorCommand.ts | 14 + .../classes/commands/ContinuationCommand.ts | 58 +++ .../commands/GetKidsBlocklistPickerCommand.ts | 27 ++ .../classes/commands/ShowDialogCommand.ts | 15 + .../endpoints/AddToPlaylistEndpoint.ts | 10 + .../endpoints/AddToPlaylistServiceEndpoint.ts | 34 ++ .../classes/endpoints/BrowseEndpoint.ts | 55 +++ .../endpoints/CreateCommentEndpoint.ts | 47 ++ .../CreatePlaylistServiceEndpoint.ts | 42 ++ .../endpoints/DeletePlaylistEndpoint.ts | 27 ++ .../classes/endpoints/FeedbackEndpoint.ts | 33 ++ .../GetAccountsListInnertubeEndpoint.ts | 54 +++ src/parser/classes/endpoints/LikeEndpoint.ts | 48 +++ .../LiveChatItemContextMenuEndpoint.ts | 27 ++ ...fyChannelNotificationPreferenceEndpoint.ts | 30 ++ .../endpoints/PerformCommentActionEndpoint.ts | 30 ++ .../classes/endpoints/PlaylistEditEndpoint.ts | 33 ++ .../classes/endpoints/PrefetchWatchCommand.ts | 10 + .../classes/endpoints/ReelWatchEndpoint.ts | 49 +++ .../classes/endpoints/SearchEndpoint.ts | 36 ++ src/parser/classes/endpoints/ShareEndpoint.ts | 10 + .../classes/endpoints/ShareEntityEndpoint.ts | 10 + .../endpoints/ShareEntityServiceEndpoint.ts | 30 ++ .../endpoints/SignalServiceEndpoint.ts | 20 + .../classes/endpoints/SubscribeEndpoint.ts | 39 ++ .../classes/endpoints/UnsubscribeEndpoint.ts | 33 ++ src/parser/classes/endpoints/WatchEndpoint.ts | 45 ++ .../classes/endpoints/WatchNextEndpoint.ts | 39 ++ src/parser/classes/menus/Menu.ts | 6 +- src/parser/classes/menus/MenuFlexibleItem.ts | 10 +- src/parser/classes/misc/Format.ts | 100 ++--- src/parser/generator.ts | 328 +++++++------- src/parser/helpers.ts | 89 ++-- src/parser/nodes.ts | 38 ++ src/parser/parser.ts | 106 ++++- src/parser/types/CommandEndpoints.ts | 197 +++++++++ src/parser/types/index.ts | 3 +- src/parser/ytshorts/ShortFormVideoInfo.ts | 13 +- src/types/Endpoints.ts | 404 ------------------ src/types/StreamingInfoOptions.ts | 2 +- src/types/index.ts | 1 - src/utils/DashManifest.tsx | 2 +- src/utils/FormatUtils.ts | 19 +- src/utils/HTTPClient.ts | 6 +- src/utils/Log.ts | 32 +- src/utils/ProtoUtils.ts | 5 +- src/utils/StreamingInfo.ts | 25 +- src/utils/Utils.ts | 2 +- 114 files changed, 2222 insertions(+), 1950 deletions(-) delete mode 100644 src/core/endpoints/BrowseEndpoint.ts delete mode 100644 src/core/endpoints/GetNotificationMenuEndpoint.ts delete mode 100644 src/core/endpoints/GuideEndpoint.ts delete mode 100644 src/core/endpoints/NextEndpoint.ts delete mode 100644 src/core/endpoints/PlayerEndpoint.ts delete mode 100644 src/core/endpoints/ResolveURLEndpoint.ts delete mode 100644 src/core/endpoints/SearchEndpoint.ts delete mode 100644 src/core/endpoints/account/AccountListEndpoint.ts delete mode 100644 src/core/endpoints/account/index.ts delete mode 100644 src/core/endpoints/browse/EditPlaylistEndpoint.ts delete mode 100644 src/core/endpoints/browse/index.ts delete mode 100644 src/core/endpoints/channel/EditDescriptionEndpoint.ts delete mode 100644 src/core/endpoints/channel/EditNameEndpoint.ts delete mode 100644 src/core/endpoints/channel/index.ts delete mode 100644 src/core/endpoints/comment/CreateCommentEndpoint.ts delete mode 100644 src/core/endpoints/comment/PerformCommentActionEndpoint.ts delete mode 100644 src/core/endpoints/comment/index.ts delete mode 100644 src/core/endpoints/index.ts delete mode 100644 src/core/endpoints/kids/BlocklistPickerEndpoint.ts delete mode 100644 src/core/endpoints/kids/index.ts delete mode 100644 src/core/endpoints/like/DislikeEndpoint.ts delete mode 100644 src/core/endpoints/like/LikeEndpoint.ts delete mode 100644 src/core/endpoints/like/RemoveLikeEndpoint.ts delete mode 100644 src/core/endpoints/like/index.ts delete mode 100644 src/core/endpoints/music/GetSearchSuggestionsEndpoint.ts delete mode 100644 src/core/endpoints/music/index.ts delete mode 100644 src/core/endpoints/notification/GetUnseenCountEndpoint.ts delete mode 100644 src/core/endpoints/notification/ModifyChannelPreferenceEndpoint.ts delete mode 100644 src/core/endpoints/notification/index.ts delete mode 100644 src/core/endpoints/playlist/CreateEndpoint.ts delete mode 100644 src/core/endpoints/playlist/DeleteEndpoint.ts delete mode 100644 src/core/endpoints/playlist/index.ts delete mode 100644 src/core/endpoints/reel/ReelItemWatchEndpoint.ts delete mode 100644 src/core/endpoints/reel/ReelWatchSequenceEndpoint.ts delete mode 100644 src/core/endpoints/reel/index.ts delete mode 100644 src/core/endpoints/subscription/SubscribeEndpoint.ts delete mode 100644 src/core/endpoints/subscription/UnsubscribeEndpoint.ts delete mode 100644 src/core/endpoints/subscription/index.ts delete mode 100644 src/core/endpoints/upload/CreateVideoEndpoint.ts delete mode 100644 src/core/endpoints/upload/index.ts create mode 100644 src/parser/classes/CreatePlaylistDialogFormView.ts create mode 100644 src/parser/classes/DialogHeaderView.ts create mode 100644 src/parser/classes/DialogView.ts create mode 100644 src/parser/classes/DropdownView.ts create mode 100644 src/parser/classes/FormFooterView.ts create mode 100644 src/parser/classes/PanelFooterView.ts create mode 100644 src/parser/classes/PremiereTrailerBadge.ts create mode 100644 src/parser/classes/TextFieldView.ts create mode 100644 src/parser/classes/actions/SendFeedbackAction.ts create mode 100644 src/parser/classes/commands/AddToPlaylistCommand.ts create mode 100644 src/parser/classes/commands/CommandExecutorCommand.ts create mode 100644 src/parser/classes/commands/ContinuationCommand.ts create mode 100644 src/parser/classes/commands/GetKidsBlocklistPickerCommand.ts create mode 100644 src/parser/classes/commands/ShowDialogCommand.ts create mode 100644 src/parser/classes/endpoints/AddToPlaylistEndpoint.ts create mode 100644 src/parser/classes/endpoints/AddToPlaylistServiceEndpoint.ts create mode 100644 src/parser/classes/endpoints/BrowseEndpoint.ts create mode 100644 src/parser/classes/endpoints/CreateCommentEndpoint.ts create mode 100644 src/parser/classes/endpoints/CreatePlaylistServiceEndpoint.ts create mode 100644 src/parser/classes/endpoints/DeletePlaylistEndpoint.ts create mode 100644 src/parser/classes/endpoints/FeedbackEndpoint.ts create mode 100644 src/parser/classes/endpoints/GetAccountsListInnertubeEndpoint.ts create mode 100644 src/parser/classes/endpoints/LikeEndpoint.ts create mode 100644 src/parser/classes/endpoints/LiveChatItemContextMenuEndpoint.ts create mode 100644 src/parser/classes/endpoints/ModifyChannelNotificationPreferenceEndpoint.ts create mode 100644 src/parser/classes/endpoints/PerformCommentActionEndpoint.ts create mode 100644 src/parser/classes/endpoints/PlaylistEditEndpoint.ts create mode 100644 src/parser/classes/endpoints/PrefetchWatchCommand.ts create mode 100644 src/parser/classes/endpoints/ReelWatchEndpoint.ts create mode 100644 src/parser/classes/endpoints/SearchEndpoint.ts create mode 100644 src/parser/classes/endpoints/ShareEndpoint.ts create mode 100644 src/parser/classes/endpoints/ShareEntityEndpoint.ts create mode 100644 src/parser/classes/endpoints/ShareEntityServiceEndpoint.ts create mode 100644 src/parser/classes/endpoints/SignalServiceEndpoint.ts create mode 100644 src/parser/classes/endpoints/SubscribeEndpoint.ts create mode 100644 src/parser/classes/endpoints/UnsubscribeEndpoint.ts create mode 100644 src/parser/classes/endpoints/WatchEndpoint.ts create mode 100644 src/parser/classes/endpoints/WatchNextEndpoint.ts create mode 100644 src/parser/types/CommandEndpoints.ts delete mode 100644 src/types/Endpoints.ts diff --git a/eslint.config.js b/eslint.config.js index ac70d2bd5..5b262a685 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -83,7 +83,6 @@ export default [ "space-infix-ops": "error", "template-curly-spacing": "error", "wrap-regex": "error", - "capitalized-comments": "error", "prefer-template": "error", "keyword-spacing": ["error", { before: true, diff --git a/src/Innertube.ts b/src/Innertube.ts index 83502eb3d..1860ce109 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -3,18 +3,6 @@ import { Kids, Music, Studio } from './core/clients/index.js'; import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js'; import { Feed, TabbedFeed } from './core/mixins/index.js'; -import { - BrowseEndpoint, - GetNotificationMenuEndpoint, - GuideEndpoint, - NextEndpoint, - PlayerEndpoint, - ResolveURLEndpoint, - SearchEndpoint, - Reel, - Notification -} from './core/endpoints/index.js'; - import { Channel, Comments, @@ -34,21 +22,23 @@ import { ShortFormVideoInfo } from './parser/ytshorts/index.js'; import NavigationEndpoint from './parser/classes/NavigationEndpoint.js'; import * as Constants from './utils/Constants.js'; -import { InnertubeError, generateRandomString, throwIfMissing, u8ToBase64 } from './utils/Utils.js'; +import { generateRandomString, InnertubeError, throwIfMissing, u8ToBase64 } from './utils/Utils.js'; import type { ApiResponse } from './core/Actions.js'; -import type { InnerTubeConfig, InnerTubeClient, SearchFilters, INextRequest } from './types/index.js'; -import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js'; -import type { DownloadOptions, FormatOptions } from './types/FormatUtils.js'; +import type { DownloadOptions, FormatOptions, InnerTubeClient, InnerTubeConfig, SearchFilters } from './types/index.js'; +import type { IBrowseResponse, IParsedResponse } from './parser/index.js'; import type Format from './parser/classes/misc/Format.js'; import { - SearchFilter_SortBy, - SearchFilter_Filters_UploadDate, + GetCommentsSectionParams, + Hashtag, + ReelSequence, + SearchFilter, + SearchFilter_Filters_Duration, SearchFilter_Filters_SearchType, - SearchFilter_Filters_Duration + SearchFilter_Filters_UploadDate, + SearchFilter_SortBy } from '../protos/generated/misc/params.js'; -import { Hashtag, SearchFilter, ReelSequence, GetCommentsSectionParams } from '../protos/generated/misc/params.js'; /** * Provides access to various services and modules in the YouTube API. @@ -60,7 +50,7 @@ import { Hashtag, SearchFilter, ReelSequence, GetCommentsSectionParams } from '. * ``` */ export default class Innertube { - #session: Session; + readonly #session: Session; constructor(session: Session) { this.#session = session; @@ -71,39 +61,38 @@ export default class Innertube { } async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise { - throwIfMissing({ target: target }); - - let next_payload: INextRequest; - - if (target instanceof NavigationEndpoint) { - next_payload = NextEndpoint.build({ - video_id: target.payload?.videoId, - playlist_id: target.payload?.playlistId, - params: target.payload?.params, - playlist_index: target.payload?.index - }); - } else if (typeof target === 'string') { - next_payload = NextEndpoint.build({ - video_id: target - }); - } else { - throw new InnertubeError('Invalid target. Expected a video id or NavigationEndpoint.', target); - } + throwIfMissing({ target }); + + const payload = { + videoId: target instanceof NavigationEndpoint ? target.payload?.videoId : target, + playlistId: target instanceof NavigationEndpoint ? target.payload?.playlistId : undefined, + playlistIndex: target instanceof NavigationEndpoint ? target.payload?.playlistIndex : undefined, + params: target instanceof NavigationEndpoint ? target.payload?.params : undefined, + racyCheckOk: true, + contentCheckOk: true + }; - if (!next_payload.videoId) - throw new InnertubeError('Video id cannot be empty', next_payload); + const watch_endpoint = new NavigationEndpoint({ watchEndpoint: payload }); + const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: payload }); - const player_payload = PlayerEndpoint.build({ - video_id: next_payload.videoId, - playlist_id: next_payload?.playlistId, - client: client, - sts: this.#session.player?.sts, - po_token: this.#session.po_token + const watch_response = watch_endpoint.call(this.#session.actions, { + playbackContext: { + contentPlaybackContext: { + vis: 0, + splay: false, + lactMilliseconds: '-1', + signatureTimestamp: this.#session.player?.sts + } + }, + serviceIntegrityDimensions: { + poToken: this.#session.po_token + }, + client }); - const player_response = this.actions.execute(PlayerEndpoint.PATH, player_payload); - const next_response = this.actions.execute(NextEndpoint.PATH, next_payload); - const response = await Promise.all([ player_response, next_response ]); + const watch_next_response = watch_next_endpoint.call(this.#session.actions); + + const response = await Promise.all([ watch_response, watch_next_response ]); const cpn = generateRandomString(16); @@ -113,27 +102,41 @@ export default class Innertube { async getBasicInfo(video_id: string, client?: InnerTubeClient): Promise { throwIfMissing({ video_id }); - const response = await this.actions.execute( - PlayerEndpoint.PATH, PlayerEndpoint.build({ - video_id: video_id, - client: client, - sts: this.#session.player?.sts, - po_token: this.#session.po_token - }) - ); + const watch_endpoint = new NavigationEndpoint({ watchEndpoint: { videoId: video_id } }); + + const watch_response = await watch_endpoint.call(this.#session.actions, { + playbackContext: { + contentPlaybackContext: { + vis: 0, + splay: false, + lactMilliseconds: '-1', + signatureTimestamp: this.#session.player?.sts + } + }, + serviceIntegrityDimensions: { + poToken: this.#session.po_token + }, + client + }); const cpn = generateRandomString(16); - return new VideoInfo([ response ], this.actions, cpn); + return new VideoInfo([ watch_response ], this.actions, cpn); } async getShortsVideoInfo(video_id: string, client?: InnerTubeClient): Promise { throwIfMissing({ video_id }); - const watch_response = this.actions.execute( - Reel.ReelItemWatchEndpoint.PATH, Reel.ReelItemWatchEndpoint.build({ video_id, client }) - ); + const reel_watch_endpoint = new NavigationEndpoint({ + reelWatchEndpoint: { + disablePlayerResponse: false, + params: 'CAUwAg%3D%3D', + videoId: video_id + } + }); + const reel_watch_response = reel_watch_endpoint.call(this.#session.actions, { client }); + const writer = ReelSequence.encode({ shortId: video_id, params: { @@ -145,13 +148,9 @@ export default class Innertube { const params = encodeURIComponent(u8ToBase64(writer.finish())); - const sequence_response = this.actions.execute( - Reel.ReelWatchSequenceEndpoint.PATH, Reel.ReelWatchSequenceEndpoint.build({ - sequence_params: params - }) - ); + const sequence_response = this.actions.execute('/reel/reel_watch_sequence', { sequenceParams: params }); - const response = await Promise.all([ watch_response, sequence_response ]); + const response = await Promise.all([ reel_watch_response, sequence_response ]); const cpn = generateRandomString(16); @@ -223,11 +222,8 @@ export default class Innertube { } } - const response = await this.actions.execute( - SearchEndpoint.PATH, SearchEndpoint.build({ - query, params: filters ? encodeURIComponent(u8ToBase64(SearchFilter.encode(search_filter).finish())) : undefined - }) - ); + const search_endpoint = new NavigationEndpoint({ searchEndpoint: { query, params: filters ? encodeURIComponent(u8ToBase64(SearchFilter.encode(search_filter).finish())) : undefined } }); + const response = await search_endpoint.call(this.#session.actions); return new Search(this.actions, response); } @@ -248,9 +244,7 @@ export default class Innertube { const response_data = await response.text(); const data = JSON.parse(response_data.replace(')]}\'', '')); - const suggestions = data[1].map((suggestion: any) => suggestion[0]); - - return suggestions; + return data[1].map((suggestion: any) => suggestion[0]); } async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST', comment_id?: string): Promise { @@ -261,7 +255,7 @@ export default class Innertube { NEWEST_FIRST: 1 }; - const writer = GetCommentsSectionParams.encode({ + const token = GetCommentsSectionParams.encode({ ctx: { videoId: video_id }, @@ -277,89 +271,81 @@ export default class Innertube { } }); - const continuation = encodeURIComponent(u8ToBase64(writer.finish())); + const continuation = encodeURIComponent(u8ToBase64(token.finish())); - const response = await this.actions.execute(NextEndpoint.PATH, NextEndpoint.build({ continuation })); + const continuation_command = new NavigationEndpoint({ + continuationCommand: { + request: 'CONTINUATION_REQUEST_TYPE_WATCH_NEXT', + token: continuation + } + }); + + const response = await continuation_command.call(this.#session.actions); return new Comments(this.actions, response.data); } async getHomeFeed(): Promise { - const response = await this.actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEwhat_to_watch' }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEwhat_to_watch' } }); + const response = await browse_endpoint.call(this.#session.actions); return new HomeFeed(this.actions, response); } - - /** - * Retrieves YouTube's content guide. - */ + async getGuide(): Promise { - const response = await this.actions.execute(GuideEndpoint.PATH); + const response = await this.actions.execute('/guide'); return new Guide(response.data); } async getLibrary(): Promise { - const response = await this.actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FElibrary' }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FElibrary' } }); + const response = await browse_endpoint.call(this.#session.actions); return new Library(this.actions, response); } async getHistory(): Promise { - const response = await this.actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEhistory' }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEhistory' } }); + const response = await browse_endpoint.call(this.#session.actions); return new History(this.actions, response); } async getTrending(): Promise> { - const response = await this.actions.execute( - BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEtrending' }), parse: true } - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEtrending' } }); + const response = await browse_endpoint.call(this.#session.actions); return new TabbedFeed(this.actions, response); } async getCourses(): Promise> { - const response = await this.actions.execute( - BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEcourses_destination' }), parse: true } - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEcourses_destination' } }); + const response = await browse_endpoint.call(this.#session.actions, { parse: true }); return new Feed(this.actions, response); } async getSubscriptionsFeed(): Promise> { - const response = await this.actions.execute( - BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEsubscriptions' }), parse: true } - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEsubscriptions' } }); + const response = await browse_endpoint.call(this.#session.actions, { parse: true }); return new Feed(this.actions, response); } async getChannelsFeed(): Promise> { - const response = await this.actions.execute( - BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEchannels' }), parse: true } - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEchannels' } }); + const response = await browse_endpoint.call(this.#session.actions, { parse: true }); return new Feed(this.actions, response); } async getChannel(id: string): Promise { throwIfMissing({ id }); - const response = await this.actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: id }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: id } }); + const response = await browse_endpoint.call(this.#session.actions); return new Channel(this.actions, response); } async getNotifications(): Promise { - const response = await this.actions.execute( - GetNotificationMenuEndpoint.PATH, GetNotificationMenuEndpoint.build({ - notifications_menu_request_type: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' - }) - ); + const response = await this.actions.execute('/notification/get_notification_menu', { notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' }); return new NotificationsMenu(this.actions, response); } async getUnseenNotificationsCount(): Promise { - const response = await this.actions.execute(Notification.GetUnseenCountEndpoint.PATH); + const response = await this.actions.execute('/notification/get_unseen_count'); // FIXME: properly parse this. return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0; } @@ -368,9 +354,8 @@ export default class Innertube { * Retrieves the user's playlists. */ async getPlaylists(): Promise> { - const response = await this.actions.execute( - BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEplaylist_aggregation' }), parse: true } - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEplaylist_aggregation' } }); + const response = await browse_endpoint.call(this.#session.actions, { parse: true }); return new Feed(this.actions, response); } @@ -381,9 +366,8 @@ export default class Innertube { id = `VL${id}`; } - const response = await this.actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: id }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: id } }); + const response = await browse_endpoint.call(this.#session.actions); return new Playlist(this.actions, response); } @@ -400,12 +384,8 @@ export default class Innertube { const params = encodeURIComponent(u8ToBase64(writer.finish())); - const response = await this.actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - browse_id: 'FEhashtag', - params - }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEhashtag', params } }); + const response = await browse_endpoint.call(this.#session.actions); return new HashtagFeed(this.actions, response); } @@ -442,9 +422,7 @@ export default class Innertube { * Resolves the given URL. */ async resolveURL(url: string): Promise { - const response = await this.actions.execute( - ResolveURLEndpoint.PATH, { ...ResolveURLEndpoint.build({ url }), parse: true } - ); + const response = await this.actions.execute('/navigation/resolve_url', { url, parse: true }); if (!response.endpoint) throw new InnertubeError('Failed to resolve URL. Expected a NavigationEndpoint but got undefined', response); diff --git a/src/core/Actions.ts b/src/core/Actions.ts index de49bd886..76e75a855 100644 --- a/src/core/Actions.ts +++ b/src/core/Actions.ts @@ -1,15 +1,14 @@ -import { Parser, NavigateAction } from '../parser/index.js'; +import type { + IBrowseResponse, IGetNotificationsMenuResponse, INextResponse, + IParsedResponse, IPlayerResponse, IRawResponse, + IResolveURLResponse, ISearchResponse, IUpdatedMetadataResponse +} from '../parser/index.js'; + +import { NavigateAction, Parser } from '../parser/index.js'; import { InnertubeError } from '../utils/Utils.js'; import type { Session } from './index.js'; -import type { - IBrowseResponse, IGetNotificationsMenuResponse, - INextResponse, IPlayerResponse, IResolveURLResponse, - ISearchResponse, IUpdatedMetadataResponse, - IParsedResponse, IRawResponse -} from '../parser/types/index.js'; - export interface ApiResponse { success: boolean; status_code: number; @@ -65,9 +64,7 @@ export default class Actions { s_url.searchParams.set(key, params[key]); } - const response = await this.session.http.fetch(s_url); - - return response; + return await this.session.http.fetch(s_url); } /** diff --git a/src/core/OAuth2.ts b/src/core/OAuth2.ts index d4871510e..408e1189b 100644 --- a/src/core/OAuth2.ts +++ b/src/core/OAuth2.ts @@ -35,13 +35,13 @@ export type OAuth2AuthErrorEventHandler = (err: OAuth2Error) => void; export default class OAuth2 { #session: Session; - YTTV_URL: URL; - AUTH_SERVER_CODE_URL: URL; - AUTH_SERVER_TOKEN_URL: URL; - AUTH_SERVER_REVOKE_TOKEN_URL: URL; + public YTTV_URL: URL; + public AUTH_SERVER_CODE_URL: URL; + public AUTH_SERVER_TOKEN_URL: URL; + public AUTH_SERVER_REVOKE_TOKEN_URL: URL; - client_id: OAuth2ClientID | undefined; - oauth2_tokens: OAuth2Tokens | undefined; + public client_id: OAuth2ClientID | undefined; + public oauth2_tokens: OAuth2Tokens | undefined; constructor(session: Session) { this.#session = session; @@ -127,7 +127,7 @@ export default class OAuth2 { await this.#session.cache?.remove('youtubei_oauth_credentials'); } - async pollForAccessToken(device_and_user_code: DeviceAndUserCode): Promise { + pollForAccessToken(device_and_user_code: DeviceAndUserCode): void { if (!this.client_id) throw new OAuth2Error('Client ID is missing.'); @@ -317,19 +317,7 @@ export default class OAuth2 { } validateTokens(tokens: OAuth2Tokens): boolean { - const propertiesAreValid = ( - Boolean(tokens.access_token) && - Boolean(tokens.expiry_date) && - Boolean(tokens.refresh_token) - ); - - const typesAreValid = ( - typeof tokens.access_token === 'string' && - typeof tokens.expiry_date === 'string' && - typeof tokens.refresh_token === 'string' - ); - - return typesAreValid && propertiesAreValid; + return !(!tokens.access_token || !tokens.refresh_token || !tokens.expiry_date); } get #http() { diff --git a/src/core/clients/Kids.ts b/src/core/clients/Kids.ts index 43caedc36..70bb61f29 100644 --- a/src/core/clients/Kids.ts +++ b/src/core/clients/Kids.ts @@ -1,17 +1,11 @@ import { Parser } from '../../parser/index.js'; import { Channel, HomeFeed, Search, VideoInfo } from '../../parser/ytkids/index.js'; import { InnertubeError, generateRandomString } from '../../utils/Utils.js'; -import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.js'; - -import { - BrowseEndpoint, NextEndpoint, - PlayerEndpoint, SearchEndpoint -} from '../endpoints/index.js'; - -import { BlocklistPickerEndpoint } from '../endpoints/kids/index.js'; - import type { Session, ApiResponse } from '../index.js'; +import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js'; +import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.js'; + export default class Kids { #session: Session; @@ -19,66 +13,46 @@ export default class Kids { this.#session = session; } - /** - * Searches the given query. - * @param query - The query. - */ async search(query: string): Promise { - const response = await this.#session.actions.execute( - SearchEndpoint.PATH, SearchEndpoint.build({ client: 'YTKIDS', query }) - ); + const search_endpoint = new NavigationEndpoint({ searchEndpoint: { query } }); + const response = await search_endpoint.call(this.#session.actions, { client: 'YTKIDS' }); return new Search(this.#session.actions, response); } - /** - * Retrieves video info. - * @param video_id - The video id. - */ async getInfo(video_id: string): Promise { - const player_payload = PlayerEndpoint.build({ - sts: this.#session.player?.sts, - client: 'YTKIDS', - video_id - }); - - const next_payload = NextEndpoint.build({ - video_id, + const payload = { videoId: video_id }; + const watch_endpoint = new NavigationEndpoint({ watchEndpoint: payload }); + const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: payload }); + + const watch_response = watch_endpoint.call(this.#session.actions, { + playbackContext: { + contentPlaybackContext: { + vis: 0, + splay: false, + lactMilliseconds: '-1', + signatureTimestamp: this.#session.player?.sts + } + }, client: 'YTKIDS' }); - const player_response = this.#session.actions.execute(PlayerEndpoint.PATH, player_payload); - const next_response = this.#session.actions.execute(NextEndpoint.PATH, next_payload); - const response = await Promise.all([ player_response, next_response ]); + const watch_next_response = watch_next_endpoint.call(this.#session.actions, { client: 'YTKIDS' }); + const response = await Promise.all([ watch_response, watch_next_response ]); const cpn = generateRandomString(16); return new VideoInfo(response, this.#session.actions, cpn); } - /** - * Retrieves the contents of the given channel. - * @param channel_id - The channel id. - */ async getChannel(channel_id: string): Promise { - const response = await this.#session.actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - browse_id: channel_id, - client: 'YTKIDS' - }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: channel_id } }); + const response = await browse_endpoint.call(this.#session.actions, { client: 'YTKIDS' }); return new Channel(this.#session.actions, response); } - /** - * Retrieves the home feed. - */ async getHomeFeed(): Promise { - const response = await this.#session.actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - browse_id: 'FEkids_home', - client: 'YTKIDS' - }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEkids_home' } }); + const response = await browse_endpoint.call(this.#session.actions, { client: 'YTKIDS' }); return new HomeFeed(this.#session.actions, response); } @@ -92,8 +66,15 @@ export default class Kids { if (!this.#session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const blocklist_payload = BlocklistPickerEndpoint.build({ channel_id: channel_id }); - const response = await this.#session.actions.execute(BlocklistPickerEndpoint.PATH, blocklist_payload ); + const kids_blocklist_picker_command = new NavigationEndpoint({ + getKidsBlocklistPickerCommand: { + blockedForKidsContent: { + external_channel_id: channel_id + } + } + }); + + const response = await kids_blocklist_picker_command.call(this.#session.actions, { client: 'YTKIDS' }); const popup = response.data.command.confirmDialogEndpoint; const popup_fragment = { contents: popup.content, engagementPanels: [] }; const kid_picker = Parser.parseResponse(popup_fragment); diff --git a/src/core/clients/Music.ts b/src/core/clients/Music.ts index 4e4a6e2ee..26ec392f9 100644 --- a/src/core/clients/Music.ts +++ b/src/core/clients/Music.ts @@ -1,9 +1,15 @@ -import { InnertubeError, generateRandomString, throwIfMissing, u8ToBase64 } from '../../utils/Utils.js'; +import { generateRandomString, InnertubeError, throwIfMissing, u8ToBase64 } from '../../utils/Utils.js'; import { - Album, Artist, Explore, - HomeFeed, Library, Playlist, - Recap, Search, TrackInfo + Album, + Artist, + Explore, + HomeFeed, + Library, + Playlist, + Recap, + Search, + TrackInfo } from '../../parser/ytmusic/index.js'; import AutomixPreviewVideo from '../../parser/classes/AutomixPreviewVideo.js'; @@ -18,15 +24,6 @@ import SearchSuggestionsSection from '../../parser/classes/SearchSuggestionsSect import SectionList from '../../parser/classes/SectionList.js'; import Tab from '../../parser/classes/Tab.js'; -import { - BrowseEndpoint, - NextEndpoint, - PlayerEndpoint, - SearchEndpoint -} from '../endpoints/index.js'; - -import { GetSearchSuggestionsEndpoint } from '../endpoints/music/index.js'; - import { SearchFilter } from '../../../protos/generated/misc/params.js'; import type { ObservedArray } from '../../parser/helpers.js'; @@ -35,7 +32,7 @@ import type { Actions, Session } from '../index.js'; export default class Music { #session: Session; - #actions: Actions; + readonly #actions: Actions; constructor(session: Session) { this.#session = session; @@ -53,29 +50,30 @@ export default class Music { return this.#fetchInfoFromEndpoint(target.overlay?.content?.endpoint ?? target.endpoint); } else if (target instanceof NavigationEndpoint) { return this.#fetchInfoFromEndpoint(target); - } else if (typeof target === 'string') { - return this.#fetchInfoFromVideoId(target); - } - - throw new InnertubeError('Invalid target, expected either a video id or a valid MusicTwoRowItem', target); + } + return this.#fetchInfoFromVideoId(target); } async #fetchInfoFromVideoId(video_id: string): Promise { - const player_payload = PlayerEndpoint.build({ - video_id, - sts: this.#session.player?.sts, - client: 'YTMUSIC' - }); + const payload = { videoId: video_id, racyCheckOk: true, contentCheckOk: true }; + const watch_endpoint = new NavigationEndpoint({ watchEndpoint: payload }); + const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: payload }); - const next_payload = NextEndpoint.build({ - video_id, + const watch_response = watch_endpoint.call(this.#actions, { + playbackContext: { + contentPlaybackContext: { + vis: 0, + splay: false, + lactMilliseconds: '-1', + signatureTimestamp: this.#session.player?.sts + } + }, client: 'YTMUSIC' }); - const player_response = this.#actions.execute(PlayerEndpoint.PATH, player_payload); - const next_response = this.#actions.execute(NextEndpoint.PATH, next_payload); - const response = await Promise.all([ player_response, next_response ]); + const watch_next_response = watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC' }); + const response = await Promise.all([ watch_response, watch_next_response ]); const cpn = generateRandomString(16); return new TrackInfo(response, this.#actions, cpn); @@ -108,11 +106,6 @@ export default class Music { return new TrackInfo(response, this.#actions, cpn); } - /** - * Searches on YouTube Music. - * @param query - Search query. - * @param filters - Search filters. - */ async search(query: string, filters: MusicSearchFilters = {}): Promise { throwIfMissing({ query }); @@ -129,134 +122,68 @@ export default class Music { params = encodeURIComponent(u8ToBase64(writer.finish())); } - const response = await this.#actions.execute( - SearchEndpoint.PATH, SearchEndpoint.build({ - query, client: 'YTMUSIC', - params - }) - ); + const search_endpoint = new NavigationEndpoint({ searchEndpoint: { query, params } }); + const response = await search_endpoint.call(this.#actions, { client: 'YTMUSIC' }); return new Search(response, this.#actions, Reflect.has(filters, 'type') && filters.type !== 'all'); } - /** - * Retrieves the home feed. - */ async getHomeFeed(): Promise { - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - browse_id: 'FEmusic_home', - client: 'YTMUSIC' - }) - ); - + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEmusic_home' } }); + const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' }); return new HomeFeed(response, this.#actions); } - /** - * Retrieves the Explore feed. - */ async getExplore(): Promise { - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - client: 'YTMUSIC', - browse_id: 'FEmusic_explore' - }) - ); - + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEmusic_explore' } }); + const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' }); return new Explore(response); // TODO: return new Explore(response, this.#actions); } - /** - * Retrieves the library. - */ async getLibrary(): Promise { - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - client: 'YTMUSIC', - browse_id: 'FEmusic_library_landing' - }) - ); - + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEmusic_library_landing' } }); + const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' }); return new Library(response, this.#actions); } - /** - * Retrieves artist's info & content. - * @param artist_id - The artist id. - */ async getArtist(artist_id: string): Promise { - throwIfMissing({ artist_id }); - - if (!artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist')) + if (!artist_id || !artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist')) throw new InnertubeError('Invalid artist id', artist_id); - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - client: 'YTMUSIC', - browse_id: artist_id - }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: artist_id } }); + const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' }); return new Artist(response, this.#actions); } - /** - * Retrieves album. - * @param album_id - The album id. - */ async getAlbum(album_id: string): Promise { - throwIfMissing({ album_id }); - - if (!album_id.startsWith('MPR') && !album_id.startsWith('FEmusic_library_privately_owned_release')) + if (!album_id || !album_id.startsWith('MPR') && !album_id.startsWith('FEmusic_library_privately_owned_release')) throw new InnertubeError('Invalid album id', album_id); - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - client: 'YTMUSIC', - browse_id: album_id - }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: album_id } }); + const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' }); return new Album(response); } - /** - * Retrieves playlist. - * @param playlist_id - The playlist id. - */ async getPlaylist(playlist_id: string): Promise { - throwIfMissing({ playlist_id }); - - if (!playlist_id.startsWith('VL')) { + if (!playlist_id.startsWith('VL')) playlist_id = `VL${playlist_id}`; - } - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - client: 'YTMUSIC', - browse_id: playlist_id - }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: playlist_id } }); + const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' }); return new Playlist(response, this.#actions); } - /** - * Retrieves up next. - * @param video_id - The video id. - * @param automix - Whether to enable automix. - */ async getUpNext(video_id: string, automix = true): Promise { throwIfMissing({ video_id }); - const response = await this.#actions.execute( - NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true } - ); + const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: { videoId: video_id } }); + const response = await watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true }); const tabs = response.contents_memo?.getType(Tab); - const tab = tabs?.first(); if (!tab) @@ -290,16 +217,11 @@ export default class Music { return playlist_panel; } - /** - * Retrieves related content. - * @param video_id - The video id. - */ async getRelated(video_id: string): Promise { throwIfMissing({ video_id }); - const response = await this.#actions.execute( - NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true } - ); + const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: { videoId: video_id } }); + const response = await watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true }); const tabs = response.contents_memo?.getType(Tab); @@ -313,21 +235,14 @@ export default class Music { if (!page.contents) throw new InnertubeError('Unexpected response', page); - const contents = page.contents.item().as(SectionList, Message); - - return contents; + return page.contents.item().as(SectionList, Message); } - /** - * Retrieves song lyrics. - * @param video_id - The video id. - */ async getLyrics(video_id: string): Promise { throwIfMissing({ video_id }); - const response = await this.#actions.execute( - NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true } - ); + const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: { videoId: video_id } }); + const response = await watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true }); const tabs = response.contents_memo?.getType(Tab); @@ -349,35 +264,18 @@ export default class Music { return section_list.firstOfType(MusicDescriptionShelf); } - /** - * Retrieves recap. - */ async getRecap(): Promise { - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - client: 'YTMUSIC_ANDROID', - browse_id: 'FEmusic_listening_review' - }) - ); - + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'FEmusic_listening_review' } }); + const response = await browse_endpoint.call(this.#actions, { client: 'YTMUSIC' }); return new Recap(response, this.#actions); } - /** - * Retrieves search suggestions for the given query. - * @param query - The query. - */ - async getSearchSuggestions(query: string): Promise> { - const response = await this.#actions.execute( - GetSearchSuggestionsEndpoint.PATH, - { ...GetSearchSuggestionsEndpoint.build({ input: query }), parse: true } - ); + async getSearchSuggestions(input: string): Promise> { + const response = await this.#actions.execute('/music/get_search_suggestions', { input, client: 'YTMUSIC', parse: true }); if (!response.contents_memo) return [] as unknown as ObservedArray; - const search_suggestions_sections = response.contents_memo.getType(SearchSuggestionsSection); - - return search_suggestions_sections; + return response.contents_memo.getType(SearchSuggestionsSection); } } \ No newline at end of file diff --git a/src/core/clients/Studio.ts b/src/core/clients/Studio.ts index 10d1dda16..66e02b87a 100644 --- a/src/core/clients/Studio.ts +++ b/src/core/clients/Studio.ts @@ -1,6 +1,5 @@ import { Constants } from '../../utils/index.js'; import { InnertubeError, Platform } from '../../utils/Utils.js'; -import { CreateVideoEndpoint } from '../endpoints/upload/index.js'; import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Misc.js'; import type { ApiResponse, Session } from '../index.js'; @@ -132,12 +131,10 @@ export default class Studio { const writer = MetadataUpdateRequest.encode(payload); - const response = await this.#session.actions.execute('/video_manager/metadata_update', { + return await this.#session.actions.execute('/video_manager/metadata_update', { protobuf: true, serialized_data: writer.finish() }); - - return response; } /** @@ -158,9 +155,7 @@ export default class Studio { if (upload_result.status !== 'STATUS_SUCCESS') throw new InnertubeError('Could not process video.'); - const response = await this.#setVideoMetadata(initial_data, upload_result, metadata); - - return response; + return await this.#setVideoMetadata(initial_data, upload_result, metadata); } async #getInitialUploadData(): Promise { @@ -213,38 +208,32 @@ export default class Studio { if (!response.ok) throw new InnertubeError('Could not upload video'); - const data = await response.json(); - - return data; + return await response.json(); } async #setVideoMetadata(initial_data: InitialUploadData, upload_result: UploadResult, metadata: UploadedVideoMetadataOptions) { - const response = await this.#session.actions.execute( - CreateVideoEndpoint.PATH, CreateVideoEndpoint.build({ - resource_id: { - scotty_resource_id: { - id: upload_result.scottyResourceId - } + return await this.#session.actions.execute('/upload/createvideo', { + resourceId: { + scottyResourceId: { + id: upload_result.scottyResourceId + } + }, + frontendUploadId: initial_data.frontend_upload_id, + initialMetadata: { + title: { + newTitle: metadata.title }, - frontend_upload_id: initial_data.frontend_upload_id, - initial_metadata: { - title: { - new_title: metadata.title || new Date().toDateString() - }, - description: { - new_description: metadata.description || '', - should_segment: true - }, - privacy: { - new_privacy: metadata.privacy || 'PRIVATE' - }, - draft_state: { - is_draft: metadata.is_draft - } + description: { + newDescription: metadata.description, + shouldSegment: true + }, + privacy: { + newPrivacy: metadata.privacy || 'PRIVATE' + }, + draftState: { + isDraft: !!metadata.is_draft } - }) - ); - - return response; + } + }); } } \ No newline at end of file diff --git a/src/core/endpoints/BrowseEndpoint.ts b/src/core/endpoints/BrowseEndpoint.ts deleted file mode 100644 index be83efccb..000000000 --- a/src/core/endpoints/BrowseEndpoint.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { IBrowseRequest, BrowseEndpointOptions } from '../../types/index.js'; - -export const PATH = '/browse'; - -/** - * Builds a `/browse` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: BrowseEndpointOptions): IBrowseRequest { - return { - ...{ - browseId: opts.browse_id, - params: opts.params, - continuation: opts.continuation, - client: opts.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/GetNotificationMenuEndpoint.ts b/src/core/endpoints/GetNotificationMenuEndpoint.ts deleted file mode 100644 index e2c921bcd..000000000 --- a/src/core/endpoints/GetNotificationMenuEndpoint.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IGetNotificationMenuRequest, GetNotificationMenuEndpointOptions } from '../../types/index.js'; - -export const PATH = '/notification/get_notification_menu'; - -/** - * Builds a `/get_notification_menu` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: GetNotificationMenuEndpointOptions): IGetNotificationMenuRequest { - return { - ...{ - notificationsMenuRequestType: opts.notifications_menu_request_type - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/GuideEndpoint.ts b/src/core/endpoints/GuideEndpoint.ts deleted file mode 100644 index 041982671..000000000 --- a/src/core/endpoints/GuideEndpoint.ts +++ /dev/null @@ -1 +0,0 @@ -export const PATH = '/guide'; \ No newline at end of file diff --git a/src/core/endpoints/NextEndpoint.ts b/src/core/endpoints/NextEndpoint.ts deleted file mode 100644 index d3aec2bd6..000000000 --- a/src/core/endpoints/NextEndpoint.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { INextRequest, NextEndpointOptions } from '../../types/index.js'; - -export const PATH = '/next'; - -/** - * Builds a `/next` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: NextEndpointOptions): INextRequest { - return { - ...{ - videoId: opts.video_id, - playlistId: opts.playlist_id, - params: opts.params, - playlistIndex: opts.playlist_index, - client: opts.client, - continuation: opts.continuation - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/PlayerEndpoint.ts b/src/core/endpoints/PlayerEndpoint.ts deleted file mode 100644 index 1c4c5152b..000000000 --- a/src/core/endpoints/PlayerEndpoint.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { IPlayerRequest, PlayerEndpointOptions } from '../../types/index.js'; - -export const PATH = '/player'; - -/** - * Builds a `/player` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: PlayerEndpointOptions): IPlayerRequest { - const payload: IPlayerRequest = { - playbackContext: { - contentPlaybackContext: { - vis: 0, - splay: false, - referer: opts.playlist_id ? - `https://www.youtube.com/watch?v=${opts.video_id}&list=${opts.playlist_id}` : - `https://www.youtube.com/watch?v=${opts.video_id}`, - currentUrl: opts.playlist_id ? - `/watch?v=${opts.video_id}&list=${opts.playlist_id}` : - `/watch?v=${opts.video_id}`, - autonavState: 'STATE_ON', - autoCaptionsDefaultOn: false, - html5Preference: 'HTML5_PREF_WANTS', - lactMilliseconds: '-1', - ...{ - signatureTimestamp: opts.sts - } - } - }, - attestationRequest: { - omitBotguardData: true - }, - racyCheckOk: true, - contentCheckOk: true, - videoId: opts.video_id - }; - - if (opts.client) - payload.client = opts.client; - - if (opts.playlist_id) - payload.playlistId = opts.playlist_id; - - if (opts.params) - payload.params = opts.params; - - if (opts.po_token) - payload.serviceIntegrityDimensions = { - poToken: opts.po_token - }; - - return payload; -} \ No newline at end of file diff --git a/src/core/endpoints/ResolveURLEndpoint.ts b/src/core/endpoints/ResolveURLEndpoint.ts deleted file mode 100644 index de58674b3..000000000 --- a/src/core/endpoints/ResolveURLEndpoint.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { IResolveURLRequest, ResolveURLEndpointOptions } from '../../types/index.js'; - -export const PATH = '/navigation/resolve_url'; - -/** - * Builds a `/resolve_url` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: ResolveURLEndpointOptions): IResolveURLRequest { - return { - ...{ - url: opts.url - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/SearchEndpoint.ts b/src/core/endpoints/SearchEndpoint.ts deleted file mode 100644 index 3f2af35e0..000000000 --- a/src/core/endpoints/SearchEndpoint.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ISearchRequest, SearchEndpointOptions } from '../../types/index.js'; - -export const PATH = '/search'; - -/** - * Builds a `/search` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: SearchEndpointOptions): ISearchRequest { - return { - ...{ - query: opts.query, - params: opts.params, - continuation: opts.continuation, - client: opts.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/account/AccountListEndpoint.ts b/src/core/endpoints/account/AccountListEndpoint.ts deleted file mode 100644 index d464235c5..000000000 --- a/src/core/endpoints/account/AccountListEndpoint.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IAccountListRequest } from '../../../types/index.js'; - -export const PATH = '/account/accounts_list'; - -/** - * Builds a `/account/accounts_list` request payload. - * @returns The payload. - */ -export function build(): IAccountListRequest { - return { - client: 'TV', - callCircumstance: 2 - }; -} \ No newline at end of file diff --git a/src/core/endpoints/account/index.ts b/src/core/endpoints/account/index.ts deleted file mode 100644 index afab49f1b..000000000 --- a/src/core/endpoints/account/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as AccountListEndpoint from './AccountListEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/browse/EditPlaylistEndpoint.ts b/src/core/endpoints/browse/EditPlaylistEndpoint.ts deleted file mode 100644 index f03c3bebf..000000000 --- a/src/core/endpoints/browse/EditPlaylistEndpoint.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IEditPlaylistRequest, EditPlaylistEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/browse/edit_playlist'; - -/** - * Builds a `/browse/edit_playlist` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: EditPlaylistEndpointOptions): IEditPlaylistRequest { - return { - playlistId: opts.playlist_id, - actions: opts.actions.map((action) => ({ - action: action.action, - ...{ - addedVideoId: action.added_video_id, - setVideoId: action.set_video_id, - movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor, - playlistDescription: action.playlist_description, - playlistName: action.playlist_name - } - })) - }; -} \ No newline at end of file diff --git a/src/core/endpoints/browse/index.ts b/src/core/endpoints/browse/index.ts deleted file mode 100644 index 42e892e85..000000000 --- a/src/core/endpoints/browse/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as EditPlaylistEndpoint from './EditPlaylistEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/channel/EditDescriptionEndpoint.ts b/src/core/endpoints/channel/EditDescriptionEndpoint.ts deleted file mode 100644 index cb2967d42..000000000 --- a/src/core/endpoints/channel/EditDescriptionEndpoint.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { IChannelEditDescriptionRequest, ChannelEditDescriptionEndpointOptions } from '../../../types/Endpoints.js'; - -export const PATH = '/channel/edit_description'; - -/** - * Builds a `/channel/edit_description` request payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: ChannelEditDescriptionEndpointOptions): IChannelEditDescriptionRequest { - return { - givenDescription: options.given_description, - client: 'ANDROID' - }; -} \ No newline at end of file diff --git a/src/core/endpoints/channel/EditNameEndpoint.ts b/src/core/endpoints/channel/EditNameEndpoint.ts deleted file mode 100644 index 35e952907..000000000 --- a/src/core/endpoints/channel/EditNameEndpoint.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { IChannelEditNameRequest, ChannelEditNameEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/channel/edit_name'; - -/** - * Builds a `/channel/edit_name` request payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: ChannelEditNameEndpointOptions): IChannelEditNameRequest { - return { - givenName: options.given_name, - client: 'ANDROID' - }; -} \ No newline at end of file diff --git a/src/core/endpoints/channel/index.ts b/src/core/endpoints/channel/index.ts deleted file mode 100644 index 5169c9416..000000000 --- a/src/core/endpoints/channel/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as EditNameEndpoint from './EditNameEndpoint.js'; -export * as EditDescriptionEndpoint from './EditDescriptionEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/comment/CreateCommentEndpoint.ts b/src/core/endpoints/comment/CreateCommentEndpoint.ts deleted file mode 100644 index b925659f9..000000000 --- a/src/core/endpoints/comment/CreateCommentEndpoint.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ICreateCommentRequest, CreateCommentEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/comment/create_comment'; - -/** - * Builds a `/comment/create_comment` request payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: CreateCommentEndpointOptions): ICreateCommentRequest { - return { - commentText: options.comment_text, - createCommentParams: options.create_comment_params, - ...{ - client: options.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/comment/PerformCommentActionEndpoint.ts b/src/core/endpoints/comment/PerformCommentActionEndpoint.ts deleted file mode 100644 index ba982b41c..000000000 --- a/src/core/endpoints/comment/PerformCommentActionEndpoint.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { IPerformCommentActionRequest, PerformCommentActionEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/comment/perform_comment_action'; - -/** - * Builds a `/comment/perform_comment_action` request payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: PerformCommentActionEndpointOptions): IPerformCommentActionRequest { - return { - actions: options.actions, - ...{ - client: options.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/comment/index.ts b/src/core/endpoints/comment/index.ts deleted file mode 100644 index 859be2124..000000000 --- a/src/core/endpoints/comment/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as PerformCommentActionEndpoint from './PerformCommentActionEndpoint.js'; -export * as CreateCommentEndpoint from './CreateCommentEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/index.ts b/src/core/endpoints/index.ts deleted file mode 100644 index 2ef933024..000000000 --- a/src/core/endpoints/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export * as BrowseEndpoint from './BrowseEndpoint.js'; -export * as GetNotificationMenuEndpoint from './GetNotificationMenuEndpoint.js'; -export * as GuideEndpoint from './GuideEndpoint.js'; -export * as NextEndpoint from './NextEndpoint.js'; -export * as PlayerEndpoint from './PlayerEndpoint.js'; -export * as ResolveURLEndpoint from './ResolveURLEndpoint.js'; -export * as SearchEndpoint from './SearchEndpoint.js'; - -export * as Account from './account/index.js'; -export * as Browse from './browse/index.js'; -export * as Channel from './channel/index.js'; -export * as Comment from './comment/index.js'; -export * as Like from './like/index.js'; -export * as Music from './music/index.js'; -export * as Notification from './notification/index.js'; -export * as Playlist from './playlist/index.js'; -export * as Subscription from './subscription/index.js'; -export * as Reel from './reel/index.js'; -export * as Upload from './upload/index.js'; -export * as Kids from './kids/index.js'; \ No newline at end of file diff --git a/src/core/endpoints/kids/BlocklistPickerEndpoint.ts b/src/core/endpoints/kids/BlocklistPickerEndpoint.ts deleted file mode 100644 index 06f0678ab..000000000 --- a/src/core/endpoints/kids/BlocklistPickerEndpoint.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { IBlocklistPickerRequest, BlocklistPickerRequestEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/kids/get_kids_blocklist_picker'; - -/** - * Builds a `/kids/get_kids_blocklist_picker` request payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: BlocklistPickerRequestEndpointOptions): IBlocklistPickerRequest { - return { blockedForKidsContent: { external_channel_id: options.channel_id } }; -} \ No newline at end of file diff --git a/src/core/endpoints/kids/index.ts b/src/core/endpoints/kids/index.ts deleted file mode 100644 index 04b9ed5eb..000000000 --- a/src/core/endpoints/kids/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as BlocklistPickerEndpoint from './BlocklistPickerEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/like/DislikeEndpoint.ts b/src/core/endpoints/like/DislikeEndpoint.ts deleted file mode 100644 index 8cd4c1eca..000000000 --- a/src/core/endpoints/like/DislikeEndpoint.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { IDislikeRequest, DislikeEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/like/dislike'; - -/** - * Builds a `/like/dislike` endpoint payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: DislikeEndpointOptions): IDislikeRequest { - return { - target: { - videoId: options.target.video_id - }, - ...{ - client: options.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/like/LikeEndpoint.ts b/src/core/endpoints/like/LikeEndpoint.ts deleted file mode 100644 index 0f8c879ad..000000000 --- a/src/core/endpoints/like/LikeEndpoint.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ILikeRequest, LikeEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/like/like'; - -/** - * Builds a `/like/like` endpoint payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: LikeEndpointOptions): ILikeRequest { - return { - target: { - videoId: options.target.video_id - }, - ...{ - client: options.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/like/RemoveLikeEndpoint.ts b/src/core/endpoints/like/RemoveLikeEndpoint.ts deleted file mode 100644 index 34403ba54..000000000 --- a/src/core/endpoints/like/RemoveLikeEndpoint.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { IRemoveLikeRequest, RemoveLikeEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/like/removelike'; - -/** - * Builds a `/like/removelike` endpoint payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: RemoveLikeEndpointOptions): IRemoveLikeRequest { - return { - target: { - videoId: options.target.video_id - }, - ...{ - client: options.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/like/index.ts b/src/core/endpoints/like/index.ts deleted file mode 100644 index 8dbda184b..000000000 --- a/src/core/endpoints/like/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as LikeEndpoint from './LikeEndpoint.js'; -export * as DislikeEndpoint from './DislikeEndpoint.js'; -export * as RemoveLikeEndpoint from './RemoveLikeEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/music/GetSearchSuggestionsEndpoint.ts b/src/core/endpoints/music/GetSearchSuggestionsEndpoint.ts deleted file mode 100644 index 3e00d1e03..000000000 --- a/src/core/endpoints/music/GetSearchSuggestionsEndpoint.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { IMusicGetSearchSuggestionsRequest, MusicGetSearchSuggestionsEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/music/get_search_suggestions'; - -/** - * Builds a `/music/get_search_suggestions` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: MusicGetSearchSuggestionsEndpointOptions): IMusicGetSearchSuggestionsRequest { - return { - input: opts.input, - client: 'YTMUSIC' - }; -} \ No newline at end of file diff --git a/src/core/endpoints/music/index.ts b/src/core/endpoints/music/index.ts deleted file mode 100644 index fd9f39bc0..000000000 --- a/src/core/endpoints/music/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as GetSearchSuggestionsEndpoint from './GetSearchSuggestionsEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/notification/GetUnseenCountEndpoint.ts b/src/core/endpoints/notification/GetUnseenCountEndpoint.ts deleted file mode 100644 index 3d3bd902c..000000000 --- a/src/core/endpoints/notification/GetUnseenCountEndpoint.ts +++ /dev/null @@ -1 +0,0 @@ -export const PATH = '/notification/get_unseen_count'; \ No newline at end of file diff --git a/src/core/endpoints/notification/ModifyChannelPreferenceEndpoint.ts b/src/core/endpoints/notification/ModifyChannelPreferenceEndpoint.ts deleted file mode 100644 index ca9c6c4bb..000000000 --- a/src/core/endpoints/notification/ModifyChannelPreferenceEndpoint.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { IModifyChannelPreferenceRequest, ModifyChannelPreferenceEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/notification/modify_channel_preference'; - -/** - * Builds a `/notification/modify_channel_preference` request payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: ModifyChannelPreferenceEndpointOptions): IModifyChannelPreferenceRequest { - return { - params: options.params, - ...{ - client: options.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/notification/index.ts b/src/core/endpoints/notification/index.ts deleted file mode 100644 index 7f66c9c13..000000000 --- a/src/core/endpoints/notification/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as GetUnseenCountEndpoint from './GetUnseenCountEndpoint.js'; -export * as ModifyChannelPreferenceEndpoint from './ModifyChannelPreferenceEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/playlist/CreateEndpoint.ts b/src/core/endpoints/playlist/CreateEndpoint.ts deleted file mode 100644 index 886e16ad3..000000000 --- a/src/core/endpoints/playlist/CreateEndpoint.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ICreatePlaylistRequest, CreatePlaylistEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/playlist/create'; - -/** - * Builds a `/playlist/create` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: CreatePlaylistEndpointOptions): ICreatePlaylistRequest { - return { - title: opts.title, - ids: opts.ids - }; -} \ No newline at end of file diff --git a/src/core/endpoints/playlist/DeleteEndpoint.ts b/src/core/endpoints/playlist/DeleteEndpoint.ts deleted file mode 100644 index 134f5a157..000000000 --- a/src/core/endpoints/playlist/DeleteEndpoint.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IDeletePlaylistRequest, DeletePlaylistEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/playlist/delete'; - -/** - * Builds a `/playlist/delete` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: DeletePlaylistEndpointOptions): IDeletePlaylistRequest { - return { - playlistId: opts.playlist_id - }; -} \ No newline at end of file diff --git a/src/core/endpoints/playlist/index.ts b/src/core/endpoints/playlist/index.ts deleted file mode 100644 index 0869530ad..000000000 --- a/src/core/endpoints/playlist/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as CreateEndpoint from './CreateEndpoint.js'; -export * as DeleteEndpoint from './DeleteEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/reel/ReelItemWatchEndpoint.ts b/src/core/endpoints/reel/ReelItemWatchEndpoint.ts deleted file mode 100644 index fdc2d8de9..000000000 --- a/src/core/endpoints/reel/ReelItemWatchEndpoint.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { IReelItemWatchRequest, ReelItemWatchEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/reel/reel_item_watch'; - -/** - * Builds a `/reel/reel_watch_sequence` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: ReelItemWatchEndpointOptions): IReelItemWatchRequest { - return { - disablePlayerResponse: false, - playerRequest: { - videoId: opts.video_id, - params: opts.params ?? 'CAUwAg%3D%3D' - }, - params: opts.params ?? 'CAUwAg%3D%3D', - client: opts.client - }; -} \ No newline at end of file diff --git a/src/core/endpoints/reel/ReelWatchSequenceEndpoint.ts b/src/core/endpoints/reel/ReelWatchSequenceEndpoint.ts deleted file mode 100644 index c0313e3b5..000000000 --- a/src/core/endpoints/reel/ReelWatchSequenceEndpoint.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IReelWatchSequenceRequest, ReelWatchSequenceEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/reel/reel_watch_sequence'; - -/** - * Builds a `/reel/reel_watch_sequence` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: ReelWatchSequenceEndpointOptions): IReelWatchSequenceRequest { - return { - sequenceParams: opts.sequence_params - }; -} \ No newline at end of file diff --git a/src/core/endpoints/reel/index.ts b/src/core/endpoints/reel/index.ts deleted file mode 100644 index 4fd216d50..000000000 --- a/src/core/endpoints/reel/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as ReelItemWatchEndpoint from './ReelItemWatchEndpoint.js'; -export * as ReelWatchSequenceEndpoint from './ReelWatchSequenceEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/subscription/SubscribeEndpoint.ts b/src/core/endpoints/subscription/SubscribeEndpoint.ts deleted file mode 100644 index 5e5e69265..000000000 --- a/src/core/endpoints/subscription/SubscribeEndpoint.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ISubscribeRequest, SubscribeEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/subscription/subscribe'; - -/** - * Builds a `/subscription/subscribe` endpoint payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: SubscribeEndpointOptions): ISubscribeRequest { - return { - channelIds: options.channel_ids, - ...{ - client: options.client, - params: options.params - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/subscription/UnsubscribeEndpoint.ts b/src/core/endpoints/subscription/UnsubscribeEndpoint.ts deleted file mode 100644 index 4e2c1c62a..000000000 --- a/src/core/endpoints/subscription/UnsubscribeEndpoint.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { IUnsubscribeRequest, UnsubscribeEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/subscription/unsubscribe'; - -/** - * Builds a `/subscription/unsubscribe` endpoint payload. - * @param options - The options to use. - * @returns The payload. - */ -export function build(options: UnsubscribeEndpointOptions): IUnsubscribeRequest { - return { - channelIds: options.channel_ids, - ...{ - client: options.client, - params: options.params - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/subscription/index.ts b/src/core/endpoints/subscription/index.ts deleted file mode 100644 index 927a8bd61..000000000 --- a/src/core/endpoints/subscription/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as SubscribeEndpoint from './SubscribeEndpoint.js'; -export * as UnsubscribeEndpoint from './UnsubscribeEndpoint.js'; \ No newline at end of file diff --git a/src/core/endpoints/upload/CreateVideoEndpoint.ts b/src/core/endpoints/upload/CreateVideoEndpoint.ts deleted file mode 100644 index 8a919763c..000000000 --- a/src/core/endpoints/upload/CreateVideoEndpoint.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { ICreateVideoRequest, CreateVideoEndpointOptions } from '../../../types/index.js'; - -export const PATH = '/upload/createvideo'; - -/** - * Builds a `/upload/createvideo` request payload. - * @param opts - The options to use. - * @returns The payload. - */ -export function build(opts: CreateVideoEndpointOptions): ICreateVideoRequest { - return { - resourceId: { - scottyResourceId: { - id: opts.resource_id.scotty_resource_id.id - } - }, - frontendUploadId: opts.frontend_upload_id, - initialMetadata: { - title: { - newTitle: opts.initial_metadata.title.new_title - }, - description: { - newDescription: opts.initial_metadata.description.new_description, - shouldSegment: opts.initial_metadata.description.should_segment - }, - privacy: { - newPrivacy: opts.initial_metadata.privacy.new_privacy - }, - draftState: { - isDraft: !!opts.initial_metadata.draft_state.is_draft - } - }, - ...{ - client: opts.client - } - }; -} \ No newline at end of file diff --git a/src/core/endpoints/upload/index.ts b/src/core/endpoints/upload/index.ts deleted file mode 100644 index 5798d2bb9..000000000 --- a/src/core/endpoints/upload/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as CreateVideoEndpoint from './CreateVideoEndpoint.js'; \ No newline at end of file diff --git a/src/core/index.ts b/src/core/index.ts index 42c21c466..82910b1a2 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -11,6 +11,5 @@ export { default as OAuth2 } from './OAuth2.js'; export * from './OAuth2.js'; export * as Clients from './clients/index.js'; -export * as Endpoints from './endpoints/index.js'; export * as Managers from './managers/index.js'; export * as Mixins from './mixins/index.js'; \ No newline at end of file diff --git a/src/core/managers/AccountManager.ts b/src/core/managers/AccountManager.ts index e740190d4..2edeb471a 100644 --- a/src/core/managers/AccountManager.ts +++ b/src/core/managers/AccountManager.ts @@ -1,67 +1,16 @@ -import type { Actions, ApiResponse } from '../index.js'; +import type { Actions } from '../index.js'; import AccountInfo from '../../parser/youtube/AccountInfo.js'; -import Analytics from '../../parser/youtube/Analytics.js'; import Settings from '../../parser/youtube/Settings.js'; -import TimeWatched from '../../parser/youtube/TimeWatched.js'; -import CopyLink from '../../parser/classes/CopyLink.js'; +import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js'; -import { InnertubeError, u8ToBase64 } from '../../utils/Utils.js'; -import { Account, BrowseEndpoint, Channel } from '../endpoints/index.js'; - -import { ChannelAnalytics } from '../../../protos/generated/misc/params.js'; +import { InnertubeError } from '../../utils/Utils.js'; export default class AccountManager { - #actions: Actions; - - channel: { - editName: (new_name: string) => Promise; - editDescription: (new_description: string) => Promise; - getBasicAnalytics: () => Promise; - }; + readonly #actions: Actions; constructor(actions: Actions) { this.#actions = actions; - - this.channel = { - /** - * Edits channel name. - * @param new_name - The new channel name. - * @deprecated This method is deprecated and will be removed in a future release. - */ - editName: (new_name: string) => { - if (!this.#actions.session.logged_in) - throw new InnertubeError('You must be signed in to perform this operation.'); - - return this.#actions.execute( - Channel.EditNameEndpoint.PATH, - Channel.EditNameEndpoint.build({ - given_name: new_name - }) - ); - }, - /** - * Edits channel description. - * @param new_description - The new description. - * @deprecated This method is deprecated and will be removed in a future release. - */ - editDescription: (new_description: string) => { - if (!this.#actions.session.logged_in) - throw new InnertubeError('You must be signed in to perform this operation.'); - - return this.#actions.execute( - Channel.EditDescriptionEndpoint.PATH, - Channel.EditDescriptionEndpoint.build({ - given_description: new_description - }) - ); - }, - /** - * Retrieves basic channel analytics. - * @deprecated This method is deprecated and will be removed in a future release. - */ - getBasicAnalytics: () => this.getAnalytics() - }; } /** @@ -70,69 +19,17 @@ export default class AccountManager { async getInfo(): Promise { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - - const response = await this.#actions.execute( - Account.AccountListEndpoint.PATH, - Account.AccountListEndpoint.build() - ); - + const get_accounts_list_endpoint = new NavigationEndpoint({ getAccountsListInnertubeEndpoint: {} }); + const response = await get_accounts_list_endpoint.call(this.#actions, { client: 'TV' }); return new AccountInfo(response); } /** - * Retrieves time watched statistics. - * @deprecated This method is deprecated and will be removed in a future release. - */ - async getTimeWatched(): Promise { - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - browse_id: 'SPtime_watched', - client: 'ANDROID' - }) - ); - - return new TimeWatched(response); - } - - /** - * Opens YouTube settings. + * Gets YouTube settings. */ async getSettings(): Promise { - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - browse_id: 'SPaccount_overview' - }) - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: 'SPaccount_overview' } }); + const response = await browse_endpoint.call(this.#actions); return new Settings(this.#actions, response); } - - /** - * Retrieves basic channel analytics. - * @deprecated This method is deprecated and will be removed in a future release. - */ - async getAnalytics(): Promise { - const advanced_settings = await this.#actions.execute( - BrowseEndpoint.PATH, { browseId: 'SPaccount_advanced', parse: true } - ); - - const copy_link_button = advanced_settings.contents_memo?.getType(CopyLink).find((node) => node.short_url.startsWith('UC')); - - if (!copy_link_button || !copy_link_button.short_url) - throw new InnertubeError('Channel ID not found'); - - const params = encodeURIComponent(u8ToBase64(ChannelAnalytics.encode({ - params: { - channelId: copy_link_button.short_url - } - }).finish())); - - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - browse_id: 'FEanalytics_screen', - params - }) - ); - - return new Analytics(response); - } } \ No newline at end of file diff --git a/src/core/managers/InteractionManager.ts b/src/core/managers/InteractionManager.ts index 514e8f014..07b751852 100644 --- a/src/core/managers/InteractionManager.ts +++ b/src/core/managers/InteractionManager.ts @@ -1,17 +1,11 @@ import * as ProtoUtils from '../../utils/ProtoUtils.js'; - import { throwIfMissing, u8ToBase64 } from '../../utils/Utils.js'; -import { LikeEndpoint, DislikeEndpoint, RemoveLikeEndpoint } from '../endpoints/like/index.js'; -import { SubscribeEndpoint, UnsubscribeEndpoint } from '../endpoints/subscription/index.js'; -import { CreateCommentEndpoint, PerformCommentActionEndpoint } from '../endpoints/comment/index.js'; -import { ModifyChannelPreferenceEndpoint } from '../endpoints/notification/index.js'; - import { CreateCommentParams, NotificationPreferences } from '../../../protos/generated/misc/params.js'; - import type { Actions, ApiResponse } from '../index.js'; +import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js'; export default class InteractionManager { - #actions: Actions; + readonly #actions: Actions; constructor(actions: Actions) { this.#actions = actions; @@ -27,13 +21,14 @@ export default class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute( - LikeEndpoint.PATH, LikeEndpoint.build({ - target: { video_id } - }) - ); + const like_endpoint = new NavigationEndpoint({ + likeEndpoint: { + status: 'LIKE', + target: video_id + } + }); - return action; + return like_endpoint.call(this.#actions, { client: 'TV' }); } /** @@ -46,13 +41,14 @@ export default class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute( - DislikeEndpoint.PATH, DislikeEndpoint.build({ - target: { video_id } - }) - ); + const dislike_endpoint = new NavigationEndpoint({ + likeEndpoint: { + status: 'DISLIKE', + target: video_id + } + }); - return action; + return dislike_endpoint.call(this.#actions, { client: 'TV' }); } /** @@ -65,17 +61,18 @@ export default class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute( - RemoveLikeEndpoint.PATH, RemoveLikeEndpoint.build({ - target: { video_id } - }) - ); + const remove_like_endpoint = new NavigationEndpoint({ + likeEndpoint: { + status: 'INDIFFERENT', + target: video_id + } + }); - return action; + return remove_like_endpoint.call(this.#actions, { client: 'TV' }); } /** - * Subscribes to a given channel. + * Subscribes to the given channel. * @param channel_id - The channel ID */ async subscribe(channel_id: string): Promise { @@ -84,18 +81,18 @@ export default class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute( - SubscribeEndpoint.PATH, SubscribeEndpoint.build({ - channel_ids: [ channel_id ], + const subscribe_endpoint = new NavigationEndpoint({ + subscribeEndpoint: { + channelIds: [ channel_id ], params: 'EgIIAhgA' - }) - ); + } + }); - return action; + return subscribe_endpoint.call(this.#actions); } /** - * Unsubscribes from a given channel. + * Unsubscribes from the given channel. * @param channel_id - The channel ID */ async unsubscribe(channel_id: string): Promise { @@ -104,14 +101,14 @@ export default class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute( - UnsubscribeEndpoint.PATH, UnsubscribeEndpoint.build({ - channel_ids: [ channel_id ], + const unsubscribe_endpoint = new NavigationEndpoint({ + unsubscribeEndpoint: { + channelIds: [ channel_id ], params: 'CgIIAhgA' - }) - ); + } + }); - return action; + return unsubscribe_endpoint.call(this.#actions); } /** @@ -135,33 +132,29 @@ export default class InteractionManager { const params = encodeURIComponent(u8ToBase64(writer.finish())); - const action = await this.#actions.execute( - CreateCommentEndpoint.PATH, CreateCommentEndpoint.build({ - comment_text: text, - create_comment_params: params - }) - ); + const create_comment_endpoint = new NavigationEndpoint({ + createCommentEndpoint: { + commentText: text, + createCommentParams: params + } + }); - return action; + return create_comment_endpoint.call(this.#actions); } /** - * Translates a given text using YouTube's comment translate feature. - * + * Translates a given text using YouTube's comment translation feature. + * @param text - The text to translate * @param target_language - an ISO language code * @param args - optional arguments */ async translate(text: string, target_language: string, args: { video_id?: string; comment_id?: string; } = {}) { throwIfMissing({ text, target_language }); - const target_action = ProtoUtils.encodeCommentActionParams(22, { text, target_language, ...args }); - - const response = await this.#actions.execute( - PerformCommentActionEndpoint.PATH, PerformCommentActionEndpoint.build({ - actions: [ target_action ] - }) - ); + const action = ProtoUtils.encodeCommentActionParams(22, { text, target_language, ...args }); + const perform_comment_action_endpoint = new NavigationEndpoint({ performCommentActionEndpoint: { action } }); + const response = await perform_comment_action_endpoint.call(this.#actions); const mutation = response.data.frameworkUpdates.entityBatchUpdate.mutations[0].payload.commentEntityPayload; return { @@ -202,14 +195,8 @@ export default class InteractionManager { }); const params = encodeURIComponent(u8ToBase64(writer.finish())); - - const action = await this.#actions.execute( - ModifyChannelPreferenceEndpoint.PATH, ModifyChannelPreferenceEndpoint.build({ - client: 'WEB', - params - }) - ); - - return action; + + const modify_channel_notification_preference_endpoint = new NavigationEndpoint({ modifyChannelNotificationPreferenceEndpoint: { params } }); + return modify_channel_notification_preference_endpoint.call(this.#actions); } } \ No newline at end of file diff --git a/src/core/managers/PlaylistManager.ts b/src/core/managers/PlaylistManager.ts index c2be4eaa3..5247e2b9d 100644 --- a/src/core/managers/PlaylistManager.ts +++ b/src/core/managers/PlaylistManager.ts @@ -1,15 +1,12 @@ import { InnertubeError, throwIfMissing } from '../../utils/Utils.js'; -import { EditPlaylistEndpoint } from '../endpoints/browse/index.js'; -import { BrowseEndpoint } from '../endpoints/index.js'; -import { CreateEndpoint, DeleteEndpoint } from '../endpoints/playlist/index.js'; import Playlist from '../../parser/youtube/Playlist.js'; import type { Actions } from '../index.js'; import type { Feed } from '../mixins/index.js'; -import type { EditPlaylistEndpointOptions } from '../../types/index.js'; +import NavigationEndpoint from '../../parser/classes/NavigationEndpoint.js'; export default class PlaylistManager { - #actions: Actions; + readonly #actions: Actions; constructor(actions: Actions) { this.#actions = actions; @@ -26,12 +23,14 @@ export default class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const response = await this.#actions.execute( - CreateEndpoint.PATH, CreateEndpoint.build({ - ids: video_ids, - title - }) - ); + const create_playlist_endpoint = new NavigationEndpoint({ + createPlaylistServiceEndpoint: { + title, + videoIds: video_ids + } + }); + + const response = await create_playlist_endpoint.call(this.#actions); return { success: response.success, @@ -51,11 +50,13 @@ export default class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const response = await this.#actions.execute( - DeleteEndpoint.PATH, DeleteEndpoint.build({ - playlist_id - }) - ); + const delete_playlist_endpoint = new NavigationEndpoint({ + deletePlaylistServiceEndpoint: { + sourcePlaylistId: playlist_id + } + }); + + const response = await delete_playlist_endpoint.call(this.#actions); return { playlist_id, @@ -76,15 +77,17 @@ export default class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const response = await this.#actions.execute( - EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build({ + const playlist_edit_endpoint = new NavigationEndpoint({ + playlistEditEndpoint: { + playlistId: playlist_id, actions: video_ids.map((id) => ({ action: 'ACTION_ADD_VIDEO', - added_video_id: id - })), - playlist_id - }) - ); + addedVideoId: id + })) + } + }); + + const response = await playlist_edit_endpoint.call(this.#actions); return { playlist_id, @@ -104,45 +107,40 @@ export default class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const info = await this.#actions.execute( - BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: `VL${playlist_id}` }), parse: true } - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: `VL${playlist_id}` } }); + const browse_response = await browse_endpoint.call(this.#actions, { parse: true }); - const playlist = new Playlist(this.#actions, info, true); + const playlist = new Playlist(this.#actions, browse_response, true); - if (playlist.info.is_editable === false) + if (!playlist.info.is_editable) throw new InnertubeError('This playlist cannot be edited.', playlist_id); - const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] }; + const payload = { playlistId: playlist_id, actions: [] as Record[] }; + + const getSetVideoIds = async (pl: Feed): Promise => { + const key_id = use_set_video_ids ? 'set_video_id' : 'id'; + const videos = pl.videos.filter((video) => video_ids.includes(video.key(key_id).string())); - const getSetVideoIds = async (pl: Feed, set_video_ids: string[] = []): Promise => { - const videos = pl.videos.filter((video) => video_ids.includes(video.key('id').string())); videos.forEach((video) => - set_video_ids.push(video.key('set_video_id').string()) + payload.actions.push({ + action: 'ACTION_REMOVE_VIDEO', + setVideoId: video.key('set_video_id').string() + }) ); if (payload.actions.length < video_ids.length) { const next = await pl.getContinuation(); - return getSetVideoIds(next, set_video_ids); + return getSetVideoIds(next); } - - return set_video_ids; }; - const set_video_ids = use_set_video_ids ? video_ids : await getSetVideoIds(playlist); - set_video_ids.forEach((set_video_id: string) => - payload.actions.push({ - action: 'ACTION_REMOVE_VIDEO', - set_video_id - }) - ); + await getSetVideoIds(playlist); if (!payload.actions.length) throw new InnertubeError('Given video ids were not found in this playlist.', video_ids); - const response = await this.#actions.execute( - EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload) - ); + const playlist_edit_endpoint = new NavigationEndpoint({ playlistEditEndpoint: payload }); + const response = await playlist_edit_endpoint.call(this.#actions); return { playlist_id, @@ -162,16 +160,15 @@ export default class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const info = await this.#actions.execute( - BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: `VL${playlist_id}` }), parse: true } - ); + const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: `VL${playlist_id}` } }); + const browse_response = await browse_endpoint.call(this.#actions, { parse: true }); - const playlist = new Playlist(this.#actions, info, true); + const playlist = new Playlist(this.#actions, browse_response, true); if (!playlist.info.is_editable) throw new InnertubeError('This playlist cannot be edited.', playlist_id); - const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] }; + const payload = { playlistId: playlist_id, actions: [] as Record[] }; let set_video_id_0: string | undefined, set_video_id_1: string | undefined; @@ -192,13 +189,12 @@ export default class PlaylistManager { payload.actions.push({ action: 'ACTION_MOVE_VIDEO_AFTER', - set_video_id: set_video_id_0, - moved_set_video_id_predecessor: set_video_id_1 + setVideoId: set_video_id_0, + movedSetVideoIdPredecessor: set_video_id_1 }); - const response = await this.#actions.execute( - EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload) - ); + const playlist_edit_endpoint = new NavigationEndpoint({ playlistEditEndpoint: payload }); + const response = await playlist_edit_endpoint.call(this.#actions); return { playlist_id, @@ -217,16 +213,15 @@ export default class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] }; + const payload = { playlist_id, actions: [] as Record[] }; payload.actions.push({ action: 'ACTION_SET_PLAYLIST_NAME', - playlist_name: name + playlistName: name }); - const response = await this.#actions.execute( - EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload) - ); + const playlist_edit_endpoint = new NavigationEndpoint({ playlistEditEndpoint: payload }); + const response = await playlist_edit_endpoint.call(this.#actions); return { playlist_id, @@ -245,16 +240,15 @@ export default class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] }; + const payload = { playlistId: playlist_id, actions: [] as Record[] }; payload.actions.push({ action: 'ACTION_SET_PLAYLIST_DESCRIPTION', - playlist_description: description + playlistDescription: description }); - const response = await this.#actions.execute( - EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload) - ); + const playlist_edit_endpoint = new NavigationEndpoint({ playlistEditEndpoint: payload }); + const response = await playlist_edit_endpoint.call(this.#actions); return { playlist_id, diff --git a/src/parser/classes/ContentMetadataView.ts b/src/parser/classes/ContentMetadataView.ts index 054156de3..78dea9cac 100644 --- a/src/parser/classes/ContentMetadataView.ts +++ b/src/parser/classes/ContentMetadataView.ts @@ -18,7 +18,7 @@ export default class ContentMetadataView extends YTNode { super(); this.metadata_rows = data.metadataRows.map((row: RawNode) => ({ metadata_parts: row.metadataParts?.map((part: RawNode) => ({ - text: Text.fromAttributed(part.text) + text: Text.fromAttributed(part.text || {}) })) })); this.delimiter = data.delimiter; diff --git a/src/parser/classes/CreatePlaylistDialogFormView.ts b/src/parser/classes/CreatePlaylistDialogFormView.ts new file mode 100644 index 000000000..8cf0b3488 --- /dev/null +++ b/src/parser/classes/CreatePlaylistDialogFormView.ts @@ -0,0 +1,25 @@ +import { YTNode } from '../helpers.js'; +import { Parser, type RawNode } from '../index.js'; +import DropdownView from './DropdownView.js'; +import TextFieldView from './TextFieldView.js'; + +export default class CreatePlaylistDialogFormView extends YTNode { + static type = 'CreatePlaylistDialogFormView'; + + public playlist_title: TextFieldView | null; + public playlist_visibility: DropdownView | null; + public disable_playlist_collaborate: boolean; + public create_playlist_params_collaboration_enabled: string; + public create_playlist_params_collaboration_disabled: string; + public video_ids: string[]; + + constructor(data: RawNode) { + super(); + this.playlist_title = Parser.parseItem(data.playlistTitle, TextFieldView); + this.playlist_visibility = Parser.parseItem(data.playlistVisibility, DropdownView); + this.disable_playlist_collaborate = !!data.disablePlaylistCollaborate; + this.create_playlist_params_collaboration_enabled = data.createPlaylistParamsCollaborationEnabled; + this.create_playlist_params_collaboration_disabled = data.createPlaylistParamsCollaborationDisabled; + this.video_ids = data.videoIds; + } +} \ No newline at end of file diff --git a/src/parser/classes/DialogHeaderView.ts b/src/parser/classes/DialogHeaderView.ts new file mode 100644 index 000000000..75ee67081 --- /dev/null +++ b/src/parser/classes/DialogHeaderView.ts @@ -0,0 +1,14 @@ +import { YTNode } from '../helpers.js'; +import { type RawNode } from '../index.js'; +import { Text } from '../misc.js'; + +export default class DialogHeaderView extends YTNode { + static type = 'DialogHeaderView'; + + public headline: Text; + + constructor(data: RawNode) { + super(); + this.headline = Text.fromAttributed(data.headline); + } +} \ No newline at end of file diff --git a/src/parser/classes/DialogView.ts b/src/parser/classes/DialogView.ts new file mode 100644 index 000000000..164e5d8c9 --- /dev/null +++ b/src/parser/classes/DialogView.ts @@ -0,0 +1,20 @@ +import { YTNode } from '../helpers.js'; +import { Parser, type RawNode } from '../index.js'; +import DialogHeaderView from './DialogHeaderView.js'; +import FormFooterView from './FormFooterView.js'; +import CreatePlaylistDialogFormView from './CreatePlaylistDialogFormView.js'; + +export default class DialogView extends YTNode { + static type = 'DialogView'; + + public header: DialogHeaderView | null; + public footer: FormFooterView | null; + public custom_content: CreatePlaylistDialogFormView | null; + + constructor (data: RawNode) { + super(); + this.header = Parser.parseItem(data.header, DialogHeaderView); + this.footer = Parser.parseItem(data.footer, FormFooterView); + this.custom_content = Parser.parseItem(data.customContent, CreatePlaylistDialogFormView); + } +} \ No newline at end of file diff --git a/src/parser/classes/DropdownView.ts b/src/parser/classes/DropdownView.ts new file mode 100644 index 000000000..c0e84a2eb --- /dev/null +++ b/src/parser/classes/DropdownView.ts @@ -0,0 +1,46 @@ +import { YTNode } from '../helpers.js'; +import { type RawNode } from '../index.js'; +import { Text, Thumbnail } from '../misc.js'; +import NavigationEndpoint from './NavigationEndpoint.js'; + +export type Option = { + title: Text; + subtitle: Text; + leading_image: Thumbnail; + value: { + privacy_status_value?: string; + }; + on_tap: NavigationEndpoint; + is_selected: boolean; +}; + +export default class DropdownView extends YTNode { + static type = 'DropdownView'; + + public label: Text; + public placeholder_text: Text; + public disabled: boolean; + public options?: Option[]; + public dropdown_type: string; + public id: string; + + constructor(data: RawNode) { + super(); + this.label = new Text(data.label); + this.placeholder_text = new Text(data.placeholderText); + this.disabled = !!data.disabled; + this.dropdown_type = data.type; + this.id = data.id; + + if (Reflect.has(data, 'options')) { + this.options = data.options.map((option: RawNode) => ({ + title: new Text(option.title), + subtitle: new Text(option.subtitle), + leading_image: Thumbnail.fromResponse(option.leadingImage), + value: { privacy_status_value: option.value?.privacyStatusValue }, + on_tap: new NavigationEndpoint(option.onTap), + is_selected: !!option.isSelected + })); + } + } +} \ No newline at end of file diff --git a/src/parser/classes/FormFooterView.ts b/src/parser/classes/FormFooterView.ts new file mode 100644 index 000000000..f21b931db --- /dev/null +++ b/src/parser/classes/FormFooterView.ts @@ -0,0 +1,18 @@ +import { YTNode } from '../helpers.js'; +import { Parser, type RawNode } from '../index.js'; +import PanelFooterView from './PanelFooterView.js'; + +export default class FormFooterView extends YTNode { + static type = 'FormFooterView'; + + public panel_footer: PanelFooterView | null; + public form_id: string; + public container_type: string; + + constructor(data: RawNode) { + super(); + this.panel_footer = Parser.parseItem(data.panelFooter, PanelFooterView); + this.form_id = data.formId; + this.container_type = data.containerType; + } +} \ No newline at end of file diff --git a/src/parser/classes/NavigationEndpoint.ts b/src/parser/classes/NavigationEndpoint.ts index b20646982..65a1e1f59 100644 --- a/src/parser/classes/NavigationEndpoint.ts +++ b/src/parser/classes/NavigationEndpoint.ts @@ -1,11 +1,12 @@ -import type Actions from '../../core/Actions.js'; -import type { ApiResponse } from '../../core/Actions.js'; import { YTNode } from '../helpers.js'; -import { Parser, type RawNode } from '../index.js'; -import type { IParsedResponse } from '../types/ParsedResponse.js'; +import { Parser, type IEndpoint, type RawNode } from '../index.js'; +import OpenPopupAction from './actions/OpenPopupAction.js'; import CreatePlaylistDialog from './CreatePlaylistDialog.js'; + +import type Actions from '../../core/Actions.js'; import type ModalWithTitleAndButton from './ModalWithTitleAndButton.js'; -import OpenPopupAction from './actions/OpenPopupAction.js'; +import type { ApiResponse } from '../../core/Actions.js'; +import type { IParsedResponse } from '../types/index.js'; export type Metadata = { url?: string; @@ -24,12 +25,23 @@ export default class NavigationEndpoint extends YTNode { public open_popup?: OpenPopupAction | null; public next_endpoint?: NavigationEndpoint; public metadata: Metadata; + public command?: YTNode | YTNode & IEndpoint; + public commands?: NavigationEndpoint[]; constructor(data: RawNode) { super(); + if (data) { + if (data.serialCommand || data.parallelCommand) { + const raw_command = data.serialCommand || data.parallelCommand; + this.commands = raw_command.commands.map((command: RawNode) => new NavigationEndpoint(command)); + } - if (data && (data.innertubeCommand || data.command)) - data = data.innertubeCommand || data.command; + if (data.innertubeCommand || data.command || data.performOnceCommand) { + data = data.innertubeCommand || data.command || data.performOnceCommand; + } + } + + this.command = Parser.parseCommand(data); if (Reflect.has(data || {}, 'openPopupAction')) this.open_popup = new OpenPopupAction(data.openPopupAction); @@ -87,6 +99,7 @@ export default class NavigationEndpoint extends YTNode { /** * Sometimes InnerTube does not return an API url, in that case the library should set it based on the name of the payload object. + * @deprecated This should be removed in the future. */ getPath(name: string) { switch (name) { @@ -108,9 +121,16 @@ export default class NavigationEndpoint extends YTNode { call(actions: Actions, args?: { [key: string]: any; parse?: false }): Promise; call(actions: Actions, args?: { [key: string]: any; parse?: boolean }): Promise { if (!actions) - throw new Error('An active caller must be provided'); + throw new Error('An API caller must be provided'); + + if (this.command) { + const command = this.command as (YTNode & IEndpoint); + return actions.execute(command.getApiPath(), { ...command.buildRequest(), ...args }); + } + if (!this.metadata.api_url) - throw new Error('Expected an api_url, but none was found, this is a bug.'); + throw new Error('Expected an api_url, but none was found.'); + return actions.execute(this.metadata.api_url, { ...this.payload, ...args }); } diff --git a/src/parser/classes/PanelFooterView.ts b/src/parser/classes/PanelFooterView.ts new file mode 100644 index 000000000..fbdb58707 --- /dev/null +++ b/src/parser/classes/PanelFooterView.ts @@ -0,0 +1,18 @@ +import { YTNode } from '../helpers.js'; +import { Parser, type RawNode } from '../index.js'; +import ButtonView from './ButtonView.js'; + +export default class PanelFooterView extends YTNode { + static type = 'PanelFooterView'; + + public primary_button: ButtonView | null; + public secondary_button: ButtonView | null; + public should_hide_divider: boolean; + + constructor(data: RawNode) { + super(); + this.primary_button = Parser.parseItem(data.primaryButton, ButtonView); + this.secondary_button = Parser.parseItem(data.secondaryButton, ButtonView); + this.should_hide_divider = !!data.shouldHideDivider; + } +} \ No newline at end of file diff --git a/src/parser/classes/PlaylistSidebarPrimaryInfo.ts b/src/parser/classes/PlaylistSidebarPrimaryInfo.ts index 2623bab76..1277d964d 100644 --- a/src/parser/classes/PlaylistSidebarPrimaryInfo.ts +++ b/src/parser/classes/PlaylistSidebarPrimaryInfo.ts @@ -6,12 +6,12 @@ import Text from './misc/Text.js'; export default class PlaylistSidebarPrimaryInfo extends YTNode { static type = 'PlaylistSidebarPrimaryInfo'; - stats: Text[]; - thumbnail_renderer: YTNode; - title: Text; - menu: YTNode; - endpoint: NavigationEndpoint; - description: Text; + public stats: Text[]; + public thumbnail_renderer: YTNode; + public title: Text; + public menu: YTNode; + public endpoint: NavigationEndpoint; + public description: Text; constructor(data: RawNode) { super(); diff --git a/src/parser/classes/PremiereTrailerBadge.ts b/src/parser/classes/PremiereTrailerBadge.ts new file mode 100644 index 000000000..17648997f --- /dev/null +++ b/src/parser/classes/PremiereTrailerBadge.ts @@ -0,0 +1,14 @@ +import { YTNode } from '../helpers.js'; +import { type RawNode } from '../index.js'; +import { Text } from '../misc.js'; + +export default class PremiereTrailerBadge extends YTNode { + static type = 'PremiereTrailerBadge'; + + public label: Text; + + constructor(data: RawNode) { + super(); + this.label = new Text(data.label); + } +} \ No newline at end of file diff --git a/src/parser/classes/SharedPost.ts b/src/parser/classes/SharedPost.ts index d3c8027d3..c34a7cfc0 100644 --- a/src/parser/classes/SharedPost.ts +++ b/src/parser/classes/SharedPost.ts @@ -1,6 +1,6 @@ import { YTNode } from '../helpers.js'; import type { RawNode } from '../index.js'; -import * as Parser from '../parser.js'; +import { Parser } from '../index.js'; import BackstagePost from './BackstagePost.js'; import Button from './Button.js'; import Menu from './menus/Menu.js'; @@ -8,19 +8,20 @@ import Author from './misc/Author.js'; import Text from './misc/Text.js'; import Thumbnail from './misc/Thumbnail.js'; import NavigationEndpoint from './NavigationEndpoint.js'; +import Post from './Post.js'; export default class SharedPost extends YTNode { static type = 'SharedPost'; - thumbnail: Thumbnail[]; - content: Text; - published: Text; - menu: Menu | null; - original_post: BackstagePost | null; - id: string; - endpoint: NavigationEndpoint; - expand_button: Button | null; - author: Author; + public thumbnail: Thumbnail[]; + public content: Text; + public published: Text; + public menu: Menu | null; + public original_post: BackstagePost | Post | null; + public id: string; + public endpoint: NavigationEndpoint; + public expand_button: Button | null; + public author: Author; constructor(data: RawNode) { super(); @@ -28,7 +29,7 @@ export default class SharedPost extends YTNode { this.content = new Text(data.content); this.published = new Text(data.publishedTimeText); this.menu = Parser.parseItem(data.actionMenu, Menu); - this.original_post = Parser.parseItem(data.originalPost, BackstagePost); + this.original_post = Parser.parseItem(data.originalPost, [ BackstagePost, Post ]); this.id = data.postId; this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.expand_button = Parser.parseItem(data.expandButton, Button); diff --git a/src/parser/classes/TextFieldView.ts b/src/parser/classes/TextFieldView.ts new file mode 100644 index 000000000..88bf0be97 --- /dev/null +++ b/src/parser/classes/TextFieldView.ts @@ -0,0 +1,62 @@ +import { YTNode } from '../helpers.js'; +import { type RawNode } from '../index.js'; + +export type DisplayProperties = { + isMultiline: boolean; + disableNewLines: boolean; +}; + +export type ContentProperties = { + labelText: string; + placeholderText: string; + maxCharacterCount: number; +}; + +export type InitialState = { + isFocused: boolean; +}; + +export type FormFieldMetadata = { + formId: string; + fieldId: string; +}; + +export default class TextFieldView extends YTNode { + static type = 'TextFieldView'; + + public display_properties?: DisplayProperties; + public content_properties?: ContentProperties; + public initial_state?: InitialState; + public form_field_metadata?: FormFieldMetadata; + + constructor(data: RawNode) { + super(); + if (Reflect.has(data, 'displayProperties')) { + this.display_properties = { + isMultiline: !!data.displayProperties.isMultiline, + disableNewLines: !!data.displayProperties.disableNewLines + }; + } + + if (Reflect.has(data, 'contentProperties')) { + this.content_properties = { + labelText: data.contentProperties.labelText, + placeholderText: data.contentProperties.placeholderText, + maxCharacterCount: data.contentProperties.maxCharacterCount + }; + } + + if (Reflect.has(data, 'initialState')) { + this.initial_state = { + isFocused: !!data.initialState.isFocused + }; + } + + if (Reflect.has(data, 'formFieldMetadata')) { + this.form_field_metadata = { + formId: data.formFieldMetadata.formId, + fieldId: data.formFieldMetadata.fieldId + }; + } + } +} \ No newline at end of file diff --git a/src/parser/classes/UnifiedSharePanel.ts b/src/parser/classes/UnifiedSharePanel.ts index 2ce5668b2..2bfebb8f7 100644 --- a/src/parser/classes/UnifiedSharePanel.ts +++ b/src/parser/classes/UnifiedSharePanel.ts @@ -18,20 +18,26 @@ export default class UnifiedSharePanel extends YTNode { public third_party_network_section?: ThirdPartyNetworkSection; public header: SharePanelHeader | null; public share_panel_version: number; + public show_loading_spinner?: boolean; constructor(data: RawNode) { super(); - const contents = data.contents.find((content: RawNode) => content.thirdPartyNetworkSection); + if (data.contents) { + const contents = data.contents.find((content: RawNode) => content.thirdPartyNetworkSection); - if (contents) { - this.third_party_network_section = { - share_target_container: Parser.parseItem(contents.thirdPartyNetworkSection.shareTargetContainer, ThirdPartyShareTargetSection), - copy_link_container: Parser.parseItem(contents.thirdPartyNetworkSection.copyLinkContainer, CopyLink), - start_at_container: Parser.parseItem(contents.thirdPartyNetworkSection.startAtContainer, StartAt) - }; + if (contents) { + this.third_party_network_section = { + share_target_container: Parser.parseItem(contents.thirdPartyNetworkSection.shareTargetContainer, ThirdPartyShareTargetSection), + copy_link_container: Parser.parseItem(contents.thirdPartyNetworkSection.copyLinkContainer, CopyLink), + start_at_container: Parser.parseItem(contents.thirdPartyNetworkSection.startAtContainer, StartAt) + }; + } } this.header = Parser.parseItem(data.header, SharePanelHeader); this.share_panel_version = data.sharePanelVersion; + + if (Reflect.has(data, 'showLoadingSpinner')) + this.show_loading_spinner = data.showLoadingSpinner; } } \ No newline at end of file diff --git a/src/parser/classes/actions/SendFeedbackAction.ts b/src/parser/classes/actions/SendFeedbackAction.ts new file mode 100644 index 000000000..29f4af007 --- /dev/null +++ b/src/parser/classes/actions/SendFeedbackAction.ts @@ -0,0 +1,13 @@ +import { YTNode } from '../../helpers.js'; +import type { RawNode } from '../../types/index.js'; + +export default class SendFeedbackAction extends YTNode { + static type = 'SendFeedbackAction'; + + public bucket: string; + + constructor(data: RawNode) { + super(); + this.bucket = data.bucket; + } +} \ No newline at end of file diff --git a/src/parser/classes/commands/AddToPlaylistCommand.ts b/src/parser/classes/commands/AddToPlaylistCommand.ts new file mode 100644 index 000000000..3d86276a7 --- /dev/null +++ b/src/parser/classes/commands/AddToPlaylistCommand.ts @@ -0,0 +1,22 @@ +import { YTNode } from '../../helpers.js'; +import { type RawNode } from '../../index.js'; +import NavigationEndpoint from '../NavigationEndpoint.js'; + +export default class AddToPlaylistCommand extends YTNode { + static type = 'AddToPlaylistCommand'; + + public open_miniplayer: boolean; + public video_id: string; + public list_type: string; + public endpoint: NavigationEndpoint; + public video_ids: string[]; + + constructor(data: RawNode) { + super(); + this.open_miniplayer = data.openMiniplayer; + this.video_id = data.videoId; + this.list_type = data.listType; + this.endpoint = new NavigationEndpoint(data.onCreateListCommand); + this.video_ids = data.videoIds; + } +} \ No newline at end of file diff --git a/src/parser/classes/commands/CommandExecutorCommand.ts b/src/parser/classes/commands/CommandExecutorCommand.ts new file mode 100644 index 000000000..fc33e86ec --- /dev/null +++ b/src/parser/classes/commands/CommandExecutorCommand.ts @@ -0,0 +1,14 @@ +import type { ObservedArray } from '../../helpers.js'; +import { YTNode } from '../../helpers.js'; +import { Parser, type RawNode } from '../../index.js'; + +export default class CommandExecutorCommand extends YTNode { + static type = 'CommandExecutorCommand'; + + public commands: ObservedArray; + + constructor(data: RawNode) { + super(); + this.commands = Parser.parseCommands(data.commands); + } +} \ No newline at end of file diff --git a/src/parser/classes/commands/ContinuationCommand.ts b/src/parser/classes/commands/ContinuationCommand.ts new file mode 100644 index 000000000..61d4617b0 --- /dev/null +++ b/src/parser/classes/commands/ContinuationCommand.ts @@ -0,0 +1,58 @@ +import { YTNode } from '../../helpers.js'; +import type { ContinuationRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class ContinuationCommand extends YTNode implements IEndpoint { + static type = 'ContinuationCommand'; + + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + switch (this.#data.request) { + case 'CONTINUATION_REQUEST_TYPE_WATCH_NEXT': + return 'next'; + case 'CONTINUATION_REQUEST_TYPE_BROWSE': + return 'browse'; + case 'CONTINUATION_REQUEST_TYPE_SEARCH': + return 'search'; + case 'CONTINUATION_REQUEST_TYPE_ACCOUNTS_LIST': + return 'account/accounts_list'; + case 'CONTINUATION_REQUEST_TYPE_COMMENTS_NOTIFICATION_MENU': + return 'notification/get_notification_menu'; + case 'CONTINUATION_REQUEST_TYPE_COMMENT_REPLIES': + return 'comment/get_comment_replies'; + case 'CONTINUATION_REQUEST_TYPE_REEL_WATCH_SEQUENCE': + return 'reel/reel_watch_sequence'; + case 'CONTINUATION_REQUEST_TYPE_GET_PANEL': + return 'get_panel'; + default: + return ''; + } + } + + public buildRequest(): ContinuationRequest { + const request: ContinuationRequest = {}; + + if (this.#data.formData) + request.formData = this.#data.formData; + + if (this.#data.token) + request.continuation = this.#data.token; + + if (this.#data.request === 'CONTINUATION_REQUEST_TYPE_COMMENTS_NOTIFICATION_MENU') { + request.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_COMMENTS'; + if (this.#data.token) { + request.fetchCommentsParams = { + continuation: this.#data.token + }; + delete request.continuation; + } + } + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/commands/GetKidsBlocklistPickerCommand.ts b/src/parser/classes/commands/GetKidsBlocklistPickerCommand.ts new file mode 100644 index 000000000..afdca79fd --- /dev/null +++ b/src/parser/classes/commands/GetKidsBlocklistPickerCommand.ts @@ -0,0 +1,27 @@ +import { YTNode } from '../../helpers.js'; +import type { GetKidsBlocklistPickerRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class GetKidsBlocklistPickerCommand extends YTNode implements IEndpoint { + static type = 'GetKidsBlocklistPickerCommand'; + + #API_PATH = 'kids/get_kids_blocklist_picker'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): GetKidsBlocklistPickerRequest { + const request: GetKidsBlocklistPickerRequest = {}; + + if (this.#data.blockedForKidsContent) + request.blockedForKidsContent = this.#data.blockedForKidsContent; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/commands/ShowDialogCommand.ts b/src/parser/classes/commands/ShowDialogCommand.ts new file mode 100644 index 000000000..af5b55f14 --- /dev/null +++ b/src/parser/classes/commands/ShowDialogCommand.ts @@ -0,0 +1,15 @@ +import { YTNode } from '../../helpers.js'; +import { Parser, type RawNode } from '../../index.js'; + +export default class ShowDialogCommand extends YTNode { + static type = 'ShowDialogCommand'; + + public inline_content: YTNode | null; + public remove_default_padding: boolean; + + constructor(data: RawNode) { + super(); + this.inline_content = Parser.parseItem(data.panelLoadingStrategy?.inlineContent); + this.remove_default_padding = !!data.removeDefaultPadding; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/AddToPlaylistEndpoint.ts b/src/parser/classes/endpoints/AddToPlaylistEndpoint.ts new file mode 100644 index 000000000..c64849777 --- /dev/null +++ b/src/parser/classes/endpoints/AddToPlaylistEndpoint.ts @@ -0,0 +1,10 @@ +import type { RawNode } from '../../index.js'; +import AddToPlaylistServiceEndpoint from './AddToPlaylistServiceEndpoint.js'; + +export default class AddToPlaylistEndpoint extends AddToPlaylistServiceEndpoint { + static type = 'AddToPlaylistEndpoint'; + + constructor (data: RawNode) { + super(data); + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/AddToPlaylistServiceEndpoint.ts b/src/parser/classes/endpoints/AddToPlaylistServiceEndpoint.ts new file mode 100644 index 000000000..07359d23f --- /dev/null +++ b/src/parser/classes/endpoints/AddToPlaylistServiceEndpoint.ts @@ -0,0 +1,34 @@ +import { YTNode } from '../../helpers.js'; +import type { AddToPlaylistServiceRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class AddToPlaylistServiceEndpoint extends YTNode implements IEndpoint { + static type = 'AddToPlaylistServiceEndpoint'; + + #API_PATH = 'playlist/get_add_to_playlist'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): AddToPlaylistServiceRequest { + const request: AddToPlaylistServiceRequest = {}; + + request.videoIds = this.#data.videoIds ? this.#data.videoIds : [ this.#data.videoId ]; + + if (this.#data.playlistId) + request.playlistId = this.#data.playlistId; + + if (this.#data.params) + request.params = this.#data.params; + + request.excludeWatchLater = !!this.#data.excludeWatchLater; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/BrowseEndpoint.ts b/src/parser/classes/endpoints/BrowseEndpoint.ts new file mode 100644 index 000000000..f6920fb19 --- /dev/null +++ b/src/parser/classes/endpoints/BrowseEndpoint.ts @@ -0,0 +1,55 @@ +import { YTNode } from '../../helpers.js'; +import type { BrowseRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class BrowseEndpoint extends YTNode implements IEndpoint { + static type = 'BrowseEndpoint'; + + #API_PATH = 'browse'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): BrowseRequest { + const request: BrowseRequest = {}; + + if (this.#data.browseId) + request.browseId = this.#data.browseId; + + if (this.#data.params) + request.params = this.#data.params; + + if (this.#data.query) + request.query = this.#data.query; + + if (this.#data.browseId === 'FEsubscriptions') { + request.subscriptionSettingsState = this.#data.subscriptionSettingsState || 'MY_SUBS_SETTINGS_STATE_LAYOUT_FORMAT_LIST'; + } + + if (this.#data.browseId === 'SPaccount_playback') { + request.formData = this.#data.formData || { + accountSettingsFormData: { + flagCaptionsDefaultOff: false, + flagAutoCaptionsDefaultOn: false, + flagDisableInlinePreview: false, + flagAudioDescriptionDefaultOn: false + } + }; + } + + if (this.#data.browseId === 'FEwhat_to_watch') { + if (this.#data.browseRequestSupportedMetadata) + request.browseRequestSupportedMetadata = this.#data.browseRequestSupportedMetadata; + if (this.#data.inlineSettingStatus) + request.inlineSettingStatus = this.#data.inlineSettingStatus; + } + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/CreateCommentEndpoint.ts b/src/parser/classes/endpoints/CreateCommentEndpoint.ts new file mode 100644 index 000000000..40fbb91ab --- /dev/null +++ b/src/parser/classes/endpoints/CreateCommentEndpoint.ts @@ -0,0 +1,47 @@ +import { YTNode } from '../../helpers.js'; +import type { CreateCommentRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class CreateCommentEndpoint extends YTNode implements IEndpoint { + static type = 'CreateCommentEndpoint'; + + #API_PATH = 'comment/create_comment'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): CreateCommentRequest { + const request: CreateCommentRequest = {}; + + if (this.#data.createCommentParams) + request.createCommentParams = this.#data.createCommentParams; + + if (this.#data.commentText) + request.commentText = this.#data.commentText; + + if (this.#data.attachedVideoId) + request.videoAttachment = { videoId: this.#data.attachedVideoId }; + else if (this.#data.pollOptions) + request.pollAttachment = { choices: this.#data.pollOptions }; + else if (this.#data.imageBlobId) + request.imageAttachment = { encryptedBlobId: this.#data.imageBlobId }; + else if (this.#data.sharedPostId) + request.sharedPostAttachment = { postId: this.#data.sharedPostId }; + + if (this.#data.accessRestrictions && typeof this.#data.accessRestrictions === 'number') { + const restriction = this.#data.accessRestrictions === 1 ? 'RESTRICTION_TYPE_EVERYONE' : 'RESTRICTION_TYPE_SPONSORS_ONLY'; + request.accessRestrictions = { restriction }; + } + + if (this.#data.botguardResponse) + request.botguardResponse = this.#data.botguardResponse; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/CreatePlaylistServiceEndpoint.ts b/src/parser/classes/endpoints/CreatePlaylistServiceEndpoint.ts new file mode 100644 index 000000000..0adc02f88 --- /dev/null +++ b/src/parser/classes/endpoints/CreatePlaylistServiceEndpoint.ts @@ -0,0 +1,42 @@ +import { YTNode } from '../../helpers.js'; +import type { CreatePlaylistServiceRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class CreatePlaylistServiceEndpoint extends YTNode implements IEndpoint { + static type = 'CreatePlaylistServiceEndpoint'; + + #API_PATH = 'playlist/create'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): CreatePlaylistServiceRequest { + const request: CreatePlaylistServiceRequest = {}; + + if (this.#data.title) + request.title = this.#data.title; + + if (this.#data.privacyStatus) + request.privacyStatus = this.#data.privacyStatus; + + if (this.#data.description) + request.description = this.#data.description; + + if (this.#data.videoIds) + request.videoIds = this.#data.videoIds; + + if (this.#data.params) + request.params = this.#data.params; + + if (this.#data.sourcePlaylistId) + request.sourcePlaylistId = this.#data.sourcePlaylistId; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/DeletePlaylistEndpoint.ts b/src/parser/classes/endpoints/DeletePlaylistEndpoint.ts new file mode 100644 index 000000000..8173f190f --- /dev/null +++ b/src/parser/classes/endpoints/DeletePlaylistEndpoint.ts @@ -0,0 +1,27 @@ +import { YTNode } from '../../helpers.js'; +import type { DeletePlaylistServiceRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class DeletePlaylistEndpoint extends YTNode implements IEndpoint { + static type = 'DeletePlaylistEndpoint'; + + #API_PATH = 'playlist/delete'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): DeletePlaylistServiceRequest { + const request: DeletePlaylistServiceRequest = {}; + + if (this.#data.playlistId) + request.playlistId = this.#data.sourcePlaylistId; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/FeedbackEndpoint.ts b/src/parser/classes/endpoints/FeedbackEndpoint.ts new file mode 100644 index 000000000..8215b87cb --- /dev/null +++ b/src/parser/classes/endpoints/FeedbackEndpoint.ts @@ -0,0 +1,33 @@ +import { YTNode } from '../../helpers.js'; +import type { FeedbackRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class FeedbackEndpoint extends YTNode implements IEndpoint { + static type = 'FeedbackEndpoint'; + + #API_PATH = 'feedback'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): FeedbackRequest { + const request: FeedbackRequest = {}; + + if (this.#data.feedbackToken) + request.feedbackTokens = [ this.#data.feedbackToken ]; + + if (this.#data.cpn) + request.feedbackContext = { cpn: this.#data.cpn }; + + request.isFeedbackTokenUnencrypted = !!this.#data.isFeedbackTokenUnencrypted; + request.shouldMerge = !!this.#data.shouldMerge; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/GetAccountsListInnertubeEndpoint.ts b/src/parser/classes/endpoints/GetAccountsListInnertubeEndpoint.ts new file mode 100644 index 000000000..032487d9b --- /dev/null +++ b/src/parser/classes/endpoints/GetAccountsListInnertubeEndpoint.ts @@ -0,0 +1,54 @@ +import { YTNode } from '../../helpers.js'; +import type { GetAccountsListInnertubeRequest, IEndpoint, RawNode } from '../../index.js'; + +export default class GetAccountsListInnertubeEndpoint extends YTNode implements IEndpoint { + static type = 'GetAccountsListInnertubeEndpoint'; + + #API_PATH = 'account/accounts_list'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): GetAccountsListInnertubeRequest { + const request: GetAccountsListInnertubeRequest = {}; + + if (this.#data.requestType) { + request.requestType = this.#data.requestType; + if (this.#data.requestType === 'ACCOUNTS_LIST_REQUEST_TYPE_CHANNEL_SWITCHER' || this.#data.requestType === 'ACCOUNTS_LIST_REQUEST_TYPE_IDENTITY_PROMPT') { + if (this.#data.nextUrl) + request.nextNavendpoint = { + urlEndpoint: { + url: this.#data.nextUrl + } + }; + } + } + + if (this.#data.channelSwitcherQuery) + request.channelSwitcherQuery = this.#data.channelSwitcherQuery; + + if (this.#data.triggerChannelCreation) + request.triggerChannelCreation = this.#data.triggerChannelCreation; + + if (this.#data.contentOwnerConfig && this.#data.contentOwnerConfig.externalContentOwnerId) + request.contentOwnerConfig = this.#data.contentOwnerConfig; + + if (this.#data.obfuscatedSelectedGaiaId) + request.obfuscatedSelectedGaiaId = this.#data.obfuscatedSelectedGaiaId; + + if (this.#data.selectedSerializedDelegationContext) + request.selectedSerializedDelegationContext = this.#data.selectedSerializedDelegationContext; + + if (this.#data.callCircumstance) + request.callCircumstance = this.#data.callCircumstance; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/LikeEndpoint.ts b/src/parser/classes/endpoints/LikeEndpoint.ts new file mode 100644 index 000000000..97701954b --- /dev/null +++ b/src/parser/classes/endpoints/LikeEndpoint.ts @@ -0,0 +1,48 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, LikeRequest, RawNode } from '../../index.js'; + +export default class LikeEndpoint extends YTNode implements IEndpoint { + static type = 'LikeEndpoint'; + + #LIKE_API_PATH = 'like/like'; + #DISLIKE_API_PATH = 'like/dislike'; + #REMOVE_LIKE_API_PATH = 'like/removelike'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#data.status === 'DISLIKE' ? + this.#DISLIKE_API_PATH : this.#data.status === 'INDIFFERENT' ? + this.#REMOVE_LIKE_API_PATH : this.#LIKE_API_PATH; + } + + public buildRequest(): LikeRequest { + const request: LikeRequest = {}; + + if (this.#data.target) + request.target = this.#data.target; + + const params = this.getParams(); + if (params) + request.params = params; + + return request; + } + + public getParams(): string | undefined { + switch (this.#data.status) { + case 'LIKE': + return this.#data.likeParams; + case 'DISLIKE': + return this.#data.dislikeParams; + case 'INDIFFERENT': + return this.#data.removeLikeParams; + default: + return undefined; + } + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/LiveChatItemContextMenuEndpoint.ts b/src/parser/classes/endpoints/LiveChatItemContextMenuEndpoint.ts new file mode 100644 index 000000000..6ccfd0c31 --- /dev/null +++ b/src/parser/classes/endpoints/LiveChatItemContextMenuEndpoint.ts @@ -0,0 +1,27 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, RawNode, LiveChatItemContextMenuRequest } from '../../index.js'; + +export default class LiveChatItemContextMenuEndpoint extends YTNode implements IEndpoint { + static type = 'LiveChatItemContextMenuEndpoint'; + + #API_PATH = 'live_chat/get_item_context_menu'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): LiveChatItemContextMenuRequest { + const request: LiveChatItemContextMenuRequest = {}; + + if (this.#data.params) + request.params = this.#data.params; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/ModifyChannelNotificationPreferenceEndpoint.ts b/src/parser/classes/endpoints/ModifyChannelNotificationPreferenceEndpoint.ts new file mode 100644 index 000000000..fd22bbce3 --- /dev/null +++ b/src/parser/classes/endpoints/ModifyChannelNotificationPreferenceEndpoint.ts @@ -0,0 +1,30 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, ModifyChannelNotificationPreferenceRequest, RawNode } from '../../index.js'; + +export default class ModifyChannelNotificationPreferenceEndpoint extends YTNode implements IEndpoint { + static type = 'ModifyChannelNotificationPreferenceEndpoint'; + + #API_PATH = 'notification/modify_channel_preference'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): ModifyChannelNotificationPreferenceRequest { + const request: ModifyChannelNotificationPreferenceRequest = {}; + + if (this.#data.params) + request.params = this.#data.params; + + if (this.#data.secondaryParams) + request.secondaryParams = this.#data.secondaryParams; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/PerformCommentActionEndpoint.ts b/src/parser/classes/endpoints/PerformCommentActionEndpoint.ts new file mode 100644 index 000000000..385f89474 --- /dev/null +++ b/src/parser/classes/endpoints/PerformCommentActionEndpoint.ts @@ -0,0 +1,30 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, PerformCommentActionRequest, RawNode } from '../../index.js'; + +export default class PerformCommentActionEndpoint extends YTNode implements IEndpoint { + static type = 'PerformCommentActionEndpoint'; + + #API_PATH = 'comment/perform_comment_action'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): PerformCommentActionRequest { + const request: PerformCommentActionRequest = {}; + + if (this.#data.actions) + request.actions = this.#data.actions; + + if (this.#data.action) + request.actions = [ this.#data.action ]; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/PlaylistEditEndpoint.ts b/src/parser/classes/endpoints/PlaylistEditEndpoint.ts new file mode 100644 index 000000000..f6dbfdbb0 --- /dev/null +++ b/src/parser/classes/endpoints/PlaylistEditEndpoint.ts @@ -0,0 +1,33 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, PlaylistEditRequest, RawNode } from '../../index.js'; + +export default class PlaylistEditEndpoint extends YTNode implements IEndpoint { + static type = 'PlaylistEditEndpoint'; + + #API_PATH = 'browse/edit_playlist'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): PlaylistEditRequest { + const request: PlaylistEditRequest = {}; + + if (this.#data.actions) + request.actions = this.#data.actions; + + if (this.#data.playlistId) + request.playlistId = this.#data.playlistId; + + if (this.#data.params) + request.params = this.#data.params; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/PrefetchWatchCommand.ts b/src/parser/classes/endpoints/PrefetchWatchCommand.ts new file mode 100644 index 000000000..ad87f95bb --- /dev/null +++ b/src/parser/classes/endpoints/PrefetchWatchCommand.ts @@ -0,0 +1,10 @@ +import type { RawNode } from '../../index.js'; +import WatchEndpoint from './WatchEndpoint.js'; + +export default class PrefetchWatchCommand extends WatchEndpoint { + static type = 'PrefetchWatchCommand'; + + constructor(data: RawNode) { + super(data); + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/ReelWatchEndpoint.ts b/src/parser/classes/endpoints/ReelWatchEndpoint.ts new file mode 100644 index 000000000..d03be46a0 --- /dev/null +++ b/src/parser/classes/endpoints/ReelWatchEndpoint.ts @@ -0,0 +1,49 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, RawNode, ReelWatchRequest } from '../../index.js'; + +export default class ReelWatchEndpoint extends YTNode implements IEndpoint { + static type = 'ReelWatchEndpoint'; + + #API_PATH = 'reel/reel_item_watch'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): ReelWatchRequest { + const request: ReelWatchRequest = {}; + + if (this.#data.videoId) { + request.playerRequest = { + videoId: this.#data.videoId + }; + } + + if (request.playerRequest) { + if (this.#data.playerParams) + request.playerRequest.params = this.#data.playerParams; + + if (this.#data.racyCheckOk) + request.playerRequest.racyCheckOk = !!this.#data.racyCheckOk; + + if (this.#data.contentCheckOk) + request.playerRequest.contentCheckOk = !!this.#data.contentCheckOk; + } + + if (this.#data.params) + request.params = this.#data.params; + + if (this.#data.inputType) + request.inputType = this.#data.inputType; + + request.disablePlayerResponse = !!this.#data.disablePlayerResponse; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/SearchEndpoint.ts b/src/parser/classes/endpoints/SearchEndpoint.ts new file mode 100644 index 000000000..2e548a825 --- /dev/null +++ b/src/parser/classes/endpoints/SearchEndpoint.ts @@ -0,0 +1,36 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, RawNode, SearchRequest } from '../../index.js'; + +export default class SearchEndpoint extends YTNode implements IEndpoint { + static type = 'SearchEndpoint'; + + #API_PATH = 'search'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): SearchRequest { + const request: SearchRequest = {}; + + if (this.#data.query) + request.query = this.#data.query; + + if (this.#data.params) + request.params = this.#data.params; + + if (this.#data.webSearchboxStatsUrl) + request.webSearchboxStatsUrl = this.#data.webSearchboxStatsUrl; + + if (this.#data.suggestStats) + request.suggestStats = this.#data.suggestStats; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/ShareEndpoint.ts b/src/parser/classes/endpoints/ShareEndpoint.ts new file mode 100644 index 000000000..ab0e53314 --- /dev/null +++ b/src/parser/classes/endpoints/ShareEndpoint.ts @@ -0,0 +1,10 @@ +import type { RawNode } from '../../index.js'; +import ShareEntityServiceEndpoint from './ShareEntityServiceEndpoint.js'; + +export default class ShareEndpoint extends ShareEntityServiceEndpoint { + static type = 'ShareEndpoint'; + + constructor(data: RawNode) { + super(data); + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/ShareEntityEndpoint.ts b/src/parser/classes/endpoints/ShareEntityEndpoint.ts new file mode 100644 index 000000000..95d2236b3 --- /dev/null +++ b/src/parser/classes/endpoints/ShareEntityEndpoint.ts @@ -0,0 +1,10 @@ +import type { RawNode } from '../../index.js'; +import ShareEntityServiceEndpoint from './ShareEntityServiceEndpoint.js'; + +export default class ShareEntityEndpoint extends ShareEntityServiceEndpoint { + static type = 'ShareEntityEndpoint'; + + constructor(data: RawNode) { + super(data); + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/ShareEntityServiceEndpoint.ts b/src/parser/classes/endpoints/ShareEntityServiceEndpoint.ts new file mode 100644 index 000000000..d7e02d400 --- /dev/null +++ b/src/parser/classes/endpoints/ShareEntityServiceEndpoint.ts @@ -0,0 +1,30 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, RawNode, ShareEntityServiceRequest } from '../../index.js'; + +export default class ShareEntityServiceEndpoint extends YTNode implements IEndpoint { + static type = 'ShareEntityServiceEndpoint'; + + #API_PATH = 'share/get_share_panel'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): ShareEntityServiceRequest { + const request: ShareEntityServiceRequest = {}; + + if (this.#data.serializedShareEntity) + request.serializedSharedEntity = this.#data.serializedShareEntity; + + if (this.#data.clientParams) + request.clientParams = this.#data.clientParams; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/SignalServiceEndpoint.ts b/src/parser/classes/endpoints/SignalServiceEndpoint.ts new file mode 100644 index 000000000..7cc635607 --- /dev/null +++ b/src/parser/classes/endpoints/SignalServiceEndpoint.ts @@ -0,0 +1,20 @@ +import { type ObservedArray, YTNode } from '../../helpers.js'; +import { Parser, type RawNode } from '../../index.js'; + +export default class SignalServiceEndpoint extends YTNode { + static type = 'SignalServiceEndpoint'; + + public actions?: ObservedArray; + public signal?: string; + + constructor(data: RawNode) { + super(); + if (Array.isArray(data.actions)) { + this.actions = Parser.parseArray(data.actions.map((action: RawNode) => { + delete action.clickTrackingParams; + return action; + })); + } + this.signal = data.signal; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/SubscribeEndpoint.ts b/src/parser/classes/endpoints/SubscribeEndpoint.ts new file mode 100644 index 000000000..ff3f6ea54 --- /dev/null +++ b/src/parser/classes/endpoints/SubscribeEndpoint.ts @@ -0,0 +1,39 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, RawNode, SubscribeRequest } from '../../index.js'; + +export default class SubscribeEndpoint extends YTNode implements IEndpoint { + static type = 'SubscribeEndpoint'; + + #API_PATH = 'subscription/subscribe'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): SubscribeRequest { + const request: SubscribeRequest = {}; + + if (this.#data.channelIds) + request.channelIds = this.#data.channelIds; + + if (this.#data.siloName) + request.siloName = this.#data.siloName; + + if (this.#data.params) + request.params = this.#data.params; + + if (this.#data.botguardResponse) + request.botguardResponse = this.#data.botguardResponse; + + if (this.#data.feature) + request.clientFeature = this.#data.feature; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/UnsubscribeEndpoint.ts b/src/parser/classes/endpoints/UnsubscribeEndpoint.ts new file mode 100644 index 000000000..2caaa1656 --- /dev/null +++ b/src/parser/classes/endpoints/UnsubscribeEndpoint.ts @@ -0,0 +1,33 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, RawNode, UnsubscribeRequest } from '../../index.js'; + +export default class UnsubscribeEndpoint extends YTNode implements IEndpoint { + static type = 'UnsubscribeEndpoint'; + + #API_PATH = 'subscription/unsubscribe'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string{ + return this.#API_PATH; + } + + public buildRequest(): UnsubscribeRequest { + const request: UnsubscribeRequest = {}; + + if (this.#data.channelIds) + request.channelIds = this.#data.channelIds; + + if (this.#data.siloName) + request.siloName = this.#data.siloName; + + if (this.#data.params) + request.params = this.#data.params; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/WatchEndpoint.ts b/src/parser/classes/endpoints/WatchEndpoint.ts new file mode 100644 index 000000000..eea74e385 --- /dev/null +++ b/src/parser/classes/endpoints/WatchEndpoint.ts @@ -0,0 +1,45 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, RawNode, WatchRequest } from '../../index.js'; + +export default class WatchEndpoint extends YTNode implements IEndpoint { + static type = 'WatchEndpoint'; + + #API_PATH = 'player'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): WatchRequest { + const request: WatchRequest = {}; + + if (this.#data.videoId) + request.videoId = this.#data.videoId; + + if (this.#data.playlistId) + request.playlistId = this.#data.playlistId; + + if (this.#data.index !== undefined || this.#data.playlistIndex !== undefined) + request.playlistIndex = this.#data.index || this.#data.playlistIndex; + + if (this.#data.playerParams || this.#data.params) + request.params = this.#data.playerParams || this.#data.params; + + if (this.#data.startTimeSeconds) + request.startTimeSecs = this.#data.startTimeSeconds; + + if (this.#data.overrideMutedAtStart) + request.overrideMutedAtStart = this.#data.overrideMutedAtStart; + + request.racyCheckOk = !!this.#data.racyCheckOk; + request.contentCheckOk = !!this.#data.contentCheckOk; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/endpoints/WatchNextEndpoint.ts b/src/parser/classes/endpoints/WatchNextEndpoint.ts new file mode 100644 index 000000000..79b8b80a0 --- /dev/null +++ b/src/parser/classes/endpoints/WatchNextEndpoint.ts @@ -0,0 +1,39 @@ +import { YTNode } from '../../helpers.js'; +import type { IEndpoint, RawNode, WatchNextRequest } from '../../index.js'; + +export default class WatchNextEndpoint extends YTNode implements IEndpoint { + static type = 'WatchNextEndpoint'; + + #API_PATH = 'next'; + #data: RawNode; + + constructor(data: RawNode) { + super(); + this.#data = data; + } + + public getApiPath(): string { + return this.#API_PATH; + } + + public buildRequest(): WatchNextRequest { + const request: WatchNextRequest = {}; + + if (this.#data.videoId) + request.videoId = this.#data.videoId; + + if (this.#data.playlistId) + request.playlistId = this.#data.playlistId; + + if (this.#data.index !== undefined || this.#data.playlistIndex !== undefined) + request.playlistIndex = this.#data.index || this.#data.playlistIndex; + + if (this.#data.playerParams || this.#data.params) + request.params = this.#data.playerParams || this.#data.params; + + request.racyCheckOk = !!this.#data.racyCheckOk; + request.contentCheckOk = !!this.#data.contentCheckOk; + + return request; + } +} \ No newline at end of file diff --git a/src/parser/classes/menus/Menu.ts b/src/parser/classes/menus/Menu.ts index a4a555303..08dd7c2a3 100644 --- a/src/parser/classes/menus/Menu.ts +++ b/src/parser/classes/menus/Menu.ts @@ -6,20 +6,22 @@ import Button from '../Button.js'; import ButtonView from '../ButtonView.js'; import SegmentedLikeDislikeButtonView from '../SegmentedLikeDislikeButtonView.js'; import MenuFlexibleItem from './MenuFlexibleItem.js'; +import LikeButton from '../LikeButton.js'; +import ToggleButton from '../ToggleButton.js'; export default class Menu extends YTNode { static type = 'Menu'; public items: ObservedArray; public flexible_items: ObservedArray; - public top_level_buttons: ObservedArray