From 1d62e469a9ff958de55657b31e2558b04aace4a6 Mon Sep 17 00:00:00 2001 From: LuanRT Date: Sat, 2 Jul 2022 19:55:33 -0300 Subject: [PATCH] refactor: rewrite Comments Section logic (#88) * feat: add core comments section classes * chore: update type declarations * chore: fix linter warnings * style: fix linter * chore: update tests * chore(tests): fix typo * chore(tests): fix typo x2 * fix(tests): `getReplies()` method is only present in `CommentThread` and not `Comment` * chore(tests): fix comment id path * chore(tests): remove outdated code * chore(tests): fix results path * chore: enforce code style * chore: update type declarations * docs: add examples and documentation * chore(docs): fix paths * chore(docs): fix more paths * chore(docs): fix `Comments.js` path * chore(docs): fix typo * chore(docs): mention example file * chore(examples): fix imports * chore(examples): fix typo --- .eslintrc.yml | 32 +- examples/comments/Comment.md | 34 ++ examples/comments/CommentThread.md | 35 ++ examples/comments/README.md | 47 ++ examples/comments/index.js | 33 ++ examples/livechat/index.js | 2 +- lib/Innertube.js | 93 ++-- lib/core/AccountManager.js | 98 ++-- lib/core/Actions.js | 421 ++++++++++-------- lib/core/Feed.js | 76 ++-- lib/core/FilterableFeed.js | 16 +- lib/core/InteractionManager.js | 46 +- lib/core/Music.js | 56 +-- lib/core/OAuth.js | 32 +- lib/core/Player.js | 32 +- lib/core/PlaylistManager.js | 36 +- lib/core/SessionBuilder.js | 64 +-- lib/core/TabbedFeed.js | 10 +- lib/deciphers/NToken.js | 124 +++--- lib/deciphers/Signature.js | 72 +-- .../classes/AnalyticsMainAppKeyMetrics.js | 8 +- lib/parser/contents/classes/AnalyticsVideo.js | 4 +- .../classes/AnalyticsVodCarouselCard.js | 4 +- lib/parser/contents/classes/Author.js | 12 +- lib/parser/contents/classes/BackstageImage.js | 2 + .../contents/classes/BackstagePostThread.js | 2 + .../contents/classes/BrowseFeedActions.js | 2 +- lib/parser/contents/classes/Button.js | 26 +- lib/parser/contents/classes/C4TabbedHeader.js | 7 +- .../contents/classes/CallToActionButton.js | 2 +- lib/parser/contents/classes/Card.js | 6 +- lib/parser/contents/classes/CardCollection.js | 2 +- lib/parser/contents/classes/Channel.js | 2 + .../classes/ChannelFeaturedContent.js | 2 +- .../contents/classes/ChannelHeaderLinks.js | 10 +- .../contents/classes/ChannelMetadata.js | 2 + .../classes/ChannelThumbnailWithLink.js | 2 +- .../contents/classes/ChannelVideoPlayer.js | 2 + lib/parser/contents/classes/ChildVideo.js | 2 +- lib/parser/contents/classes/ChipCloud.js | 2 +- lib/parser/contents/classes/ChipCloudChip.js | 4 +- .../contents/classes/CollageHeroImage.js | 2 + lib/parser/contents/classes/Comment.js | 136 ++++++ .../contents/classes/CommentActionButtons.js | 13 - .../contents/classes/CommentReplyDialog.js | 19 + lib/parser/contents/classes/CommentThread.js | 74 +++ .../classes/CommentsEntryPointHeader.js | 2 +- lib/parser/contents/classes/CommentsHeader.js | 27 ++ lib/parser/contents/classes/CompactLink.js | 2 +- lib/parser/contents/classes/CompactMix.js | 2 +- .../contents/classes/CompactPlaylist.js | 2 +- lib/parser/contents/classes/CompactVideo.js | 6 +- .../contents/classes/ContinuationItem.js | 7 +- .../contents/classes/CtaGoToCreatorStudio.js | 6 +- .../contents/classes/DataModelSection.js | 8 +- lib/parser/contents/classes/DidYouMean.js | 2 +- lib/parser/contents/classes/DownloadButton.js | 2 +- lib/parser/contents/classes/Element.js | 4 +- .../contents/classes/EmergencyOnebox.js | 2 +- lib/parser/contents/classes/EmojiRun.js | 4 +- .../contents/classes/EndScreenPlaylist.js | 4 +- lib/parser/contents/classes/EndScreenVideo.js | 4 +- lib/parser/contents/classes/Endscreen.js | 2 +- .../contents/classes/EndscreenElement.js | 53 +-- lib/parser/contents/classes/ExpandableTab.js | 4 +- .../contents/classes/ExpandedShelfContents.js | 4 +- .../contents/classes/FeedFilterChipBar.js | 2 + lib/parser/contents/classes/Format.js | 4 +- lib/parser/contents/classes/Grid.js | 4 +- lib/parser/contents/classes/GridPlaylist.js | 10 +- lib/parser/contents/classes/GridVideo.js | 6 +- .../contents/classes/HorizontalCardList.js | 2 +- lib/parser/contents/classes/HorizontalList.js | 4 +- lib/parser/contents/classes/ItemSection.js | 6 +- lib/parser/contents/classes/LikeButton.js | 10 +- lib/parser/contents/classes/LiveChat.js | 8 +- .../contents/classes/LiveChatAuthorBadge.js | 2 +- lib/parser/contents/classes/LiveChatHeader.js | 2 +- .../contents/classes/LiveChatItemList.js | 2 +- .../contents/classes/LiveChatMessageInput.js | 2 +- .../contents/classes/LiveChatParticipant.js | 2 +- .../classes/LiveChatParticipantsList.js | 2 +- lib/parser/contents/classes/Menu.js | 4 +- .../contents/classes/MenuNavigationItem.js | 2 +- .../contents/classes/MenuServiceItem.js | 2 +- .../classes/MenuServiceItemDownload.js | 2 +- .../contents/classes/MerchandiseItem.js | 2 +- .../contents/classes/MerchandiseShelf.js | 4 +- lib/parser/contents/classes/Message.js | 4 +- lib/parser/contents/classes/MetadataBadge.js | 10 +- lib/parser/contents/classes/MetadataRow.js | 2 +- .../contents/classes/MetadataRowContainer.js | 2 +- .../contents/classes/MetadataRowHeader.js | 2 +- .../contents/classes/MicroformatData.js | 2 + lib/parser/contents/classes/Mix.js | 2 +- lib/parser/contents/classes/Movie.js | 10 +- .../contents/classes/MovingThumbnail.js | 2 +- .../contents/classes/MusicCarouselShelf.js | 6 +- .../classes/MusicCarouselShelfBasicHeader.js | 14 +- .../contents/classes/MusicDescriptionShelf.js | 14 +- .../contents/classes/MusicDetailHeader.js | 20 +- lib/parser/contents/classes/MusicHeader.js | 2 +- .../contents/classes/MusicImmersiveHeader.js | 6 +- .../contents/classes/MusicInlineBadge.js | 4 +- .../classes/MusicItemThumbnailOverlay.js | 2 +- .../contents/classes/MusicNavigationButton.js | 2 +- .../contents/classes/MusicPlayButton.js | 16 +- .../contents/classes/MusicPlaylistShelf.js | 6 +- lib/parser/contents/classes/MusicQueue.js | 2 +- .../classes/MusicResponsiveListItem.js | 80 ++-- .../MusicResponsiveListItemFixedColumn.js | 2 +- .../MusicResponsiveListItemFlexColumn.js | 2 +- lib/parser/contents/classes/MusicShelf.js | 16 +- lib/parser/contents/classes/MusicThumbnail.js | 2 +- .../contents/classes/MusicTwoRowItem.js | 48 +- .../contents/classes/NavigatableText.js | 12 +- .../contents/classes/NavigationEndpoint.js | 151 ++++--- .../classes/PlayerAnnotationsExpanded.js | 4 +- .../classes/PlayerCaptionsTracklist.js | 6 +- .../contents/classes/PlayerErrorMessage.js | 2 +- .../classes/PlayerLiveStoryboardSpec.js | 2 +- .../contents/classes/PlayerMicroformat.js | 6 +- lib/parser/contents/classes/PlayerOverlay.js | 2 +- .../contents/classes/PlayerOverlayAutoplay.js | 2 +- .../contents/classes/PlayerStoryboardSpec.js | 10 +- lib/parser/contents/classes/Playlist.js | 8 +- lib/parser/contents/classes/PlaylistAuthor.js | 2 +- lib/parser/contents/classes/PlaylistHeader.js | 2 +- lib/parser/contents/classes/PlaylistPanel.js | 2 +- .../contents/classes/PlaylistPanelVideo.js | 16 +- .../contents/classes/PlaylistSidebar.js | 4 +- .../classes/PlaylistSidebarPrimaryInfo.js | 2 +- lib/parser/contents/classes/PlaylistVideo.js | 6 +- .../contents/classes/PlaylistVideoList.js | 2 +- .../classes/PlaylistVideoThumbnail.js | 2 + lib/parser/contents/classes/Poll.js | 4 +- lib/parser/contents/classes/Post.js | 2 +- lib/parser/contents/classes/ProfileColumn.js | 4 +- .../contents/classes/ProfileColumnStats.js | 4 +- .../classes/ProfileColumnStatsEntry.js | 2 +- .../contents/classes/ProfileColumnUserInfo.js | 2 +- lib/parser/contents/classes/ReelItem.js | 2 + lib/parser/contents/classes/ReelShelf.js | 4 +- .../contents/classes/RelatedChipCloud.js | 2 +- lib/parser/contents/classes/RichGrid.js | 2 + lib/parser/contents/classes/RichItem.js | 2 + lib/parser/contents/classes/SearchBox.js | 2 +- .../classes/SecondarySearchContainer.js | 2 +- lib/parser/contents/classes/SectionList.js | 8 +- lib/parser/contents/classes/Shelf.js | 16 +- .../contents/classes/ShowingResultsFor.js | 2 +- .../contents/classes/SimpleCardTeaser.js | 2 +- .../classes/SingleActionEmergencySupport.js | 2 +- .../classes/SingleColumnBrowseResults.js | 4 +- .../SingleColumnMusicWatchNextResults.js | 2 +- .../contents/classes/SingleHeroImage.js | 2 +- .../contents/classes/SortFilterSubMenu.js | 4 +- lib/parser/contents/classes/SubFeedOption.js | 2 +- .../contents/classes/SubFeedSelector.js | 2 +- .../contents/classes/SubscribeButton.js | 4 +- .../SubscriptionNotificationToggleButton.js | 8 +- lib/parser/contents/classes/Tab.js | 2 +- lib/parser/contents/classes/Tabbed.js | 2 +- .../contents/classes/TabbedSearchResults.js | 6 +- lib/parser/contents/classes/Text.js | 6 +- lib/parser/contents/classes/TextHeader.js | 2 +- lib/parser/contents/classes/Thumbnail.js | 6 +- .../classes/ThumbnailOverlayBottomPanel.js | 2 +- .../classes/ThumbnailOverlayEndorsement.js | 2 +- .../classes/ThumbnailOverlayHoverText.js | 2 +- .../ThumbnailOverlayInlineUnplayable.js | 2 +- .../classes/ThumbnailOverlayLoadingPreview.js | 2 +- .../classes/ThumbnailOverlayNowPlaying.js | 2 +- .../classes/ThumbnailOverlayPinking.js | 2 +- .../classes/ThumbnailOverlayPlaybackStatus.js | 2 +- .../classes/ThumbnailOverlayResumePlayback.js | 2 +- .../classes/ThumbnailOverlaySidePanel.js | 2 +- .../classes/ThumbnailOverlayTimeStatus.js | 2 +- .../classes/ThumbnailOverlayToggleButton.js | 12 +- lib/parser/contents/classes/ToggleButton.js | 33 +- .../contents/classes/ToggleMenuServiceItem.js | 2 +- lib/parser/contents/classes/Tooltip.js | 8 +- .../classes/TwoColumnBrowseResults.js | 2 +- .../classes/TwoColumnSearchResults.js | 2 +- .../classes/TwoColumnWatchNextResults.js | 2 +- .../contents/classes/UniversalWatchCard.js | 2 +- lib/parser/contents/classes/VerticalList.js | 4 +- .../contents/classes/VerticalWatchCardList.js | 2 +- lib/parser/contents/classes/Video.js | 31 +- lib/parser/contents/classes/VideoDetails.js | 74 +-- .../contents/classes/VideoInfoCardContent.js | 2 +- lib/parser/contents/classes/VideoOwner.js | 6 +- .../contents/classes/VideoPrimaryInfo.js | 2 +- .../contents/classes/VideoSecondaryInfo.js | 2 +- .../contents/classes/WatchCardCompactVideo.js | 6 +- .../contents/classes/WatchCardHeroVideo.js | 2 +- .../contents/classes/WatchCardRichHeader.js | 4 +- .../classes/WatchCardSectionSequence.js | 2 +- .../classes/WatchNextTabbedResults.js | 2 +- .../classes/comments/AuthorCommentBadge.js | 25 ++ .../classes/comments/CommentActionButtons.js | 15 + .../classes/comments/CommentReplies.js | 15 + .../classes/comments/CommentSimplebox.js | 19 + .../classes/livechat/AddChatItemAction.js | 2 +- .../livechat/AddLiveChatTickerItemAction.js | 2 +- .../classes/livechat/LiveChatActionPanel.js | 2 +- .../livechat/MarkChatItemAsDeletedAction.js | 2 +- .../MarkChatItemsByAuthorAsDeletedAction.js | 2 +- .../classes/livechat/ReplayChatItemAction.js | 4 +- .../livechat/ShowLiveChatTooltipCommand.js | 2 +- .../classes/livechat/UpdateDateTextAction.js | 2 +- .../livechat/UpdateDescriptionAction.js | 2 +- .../livechat/UpdateLiveChatPollAction.js | 2 +- .../classes/livechat/UpdateTitleAction.js | 2 +- .../livechat/UpdateToggleButtonTextAction.js | 2 +- .../livechat/UpdateViewershipAction.js | 4 +- .../classes/livechat/items/LiveChatBanner.js | 2 +- .../livechat/items/LiveChatBannerHeader.js | 2 +- .../livechat/items/LiveChatBannerPoll.js | 6 +- .../livechat/items/LiveChatMembershipItem.js | 6 +- .../livechat/items/LiveChatPaidMessage.js | 12 +- .../livechat/items/LiveChatPlaceholderItem.js | 2 +- .../livechat/items/LiveChatTextMessage.js | 14 +- .../items/LiveChatTickerPaidMessageItem.js | 12 +- .../items/LiveChatTickerSponsorItem.js | 14 +- .../items/LiveChatViewerEngagementMessage.js | 6 +- .../contents/classes/livechat/items/Poll.js | 6 +- .../classes/livechat/items/PollHeader.js | 2 +- lib/parser/contents/index.js | 160 +++---- lib/parser/index.js | 324 +++++++------- lib/parser/youtube/Analytics.js | 8 +- lib/parser/youtube/Channel.js | 16 +- lib/parser/youtube/Comments.js | 77 ++++ lib/parser/youtube/History.js | 8 +- lib/parser/youtube/Library.js | 36 +- lib/parser/youtube/LiveChat.js | 12 +- lib/parser/youtube/Playlist.js | 10 +- lib/parser/youtube/Search.js | 32 +- lib/parser/youtube/VideoInfo.js | 138 +++--- lib/parser/youtube/others/ChannelMetadata.js | 2 +- lib/parser/youtube/others/CommentThread.js | 10 +- lib/parser/youtube/others/GridPlaylistItem.js | 6 +- lib/parser/youtube/others/GridVideoItem.js | 6 +- lib/parser/youtube/others/NotificationItem.js | 6 +- lib/parser/youtube/others/PlaylistItem.js | 6 +- lib/parser/youtube/others/ShelfRenderer.js | 20 +- lib/parser/youtube/others/VideoItem.js | 10 +- .../youtube/search/SearchSuggestionItem.js | 2 +- lib/parser/youtube/search/VideoResultItem.js | 6 +- lib/parser/ytmusic/Album.js | 10 +- lib/parser/ytmusic/Artist.js | 12 +- lib/parser/ytmusic/Explore.js | 8 +- lib/parser/ytmusic/HomeFeed.js | 10 +- lib/parser/ytmusic/Library.js | 6 +- lib/parser/ytmusic/Search.js | 64 +-- lib/parser/ytmusic/others/PlaylistItem.js | 8 +- lib/parser/ytmusic/search/AlbumResultItem.js | 4 +- lib/parser/ytmusic/search/ArtistResultItem.js | 2 +- .../search/MusicSearchSuggestionItem.js | 18 +- .../ytmusic/search/PlaylistResultItem.js | 4 +- lib/parser/ytmusic/search/SongResultItem.js | 4 +- lib/parser/ytmusic/search/TopResultItem.js | 10 +- lib/parser/ytmusic/search/VideoResultItem.js | 8 +- lib/proto/index.js | 138 +++--- lib/utils/Constants.js | 4 +- lib/utils/Request.js | 34 +- lib/utils/Utils.js | 63 ++- test/main.test.js | 28 +- typings/lib/Innertube.d.ts | 9 +- typings/lib/core/Actions.d.ts | 6 + .../lib/parser/contents/classes/Button.d.ts | 2 +- .../lib/parser/contents/classes/Comment.d.ts | 69 +++ .../contents/classes/CommentReplyDialog.d.ts | 12 + .../contents/classes/CommentThread.d.ts | 26 ++ .../contents/classes/CommentsHeader.d.ts | 12 + .../contents/classes/ContinuationItem.d.ts | 1 + .../contents/classes/EndscreenElement.d.ts | 6 +- .../classes/MusicResponsiveListItem.d.ts | 14 +- .../contents/classes/NavigationEndpoint.d.ts | 8 + .../parser/contents/classes/SectionList.d.ts | 1 + .../classes/comments/AuthorCommentBadge.d.ts | 10 + .../contents/classes/comments/Comment.d.ts | 26 ++ .../comments/CommentActionButtons.d.ts | 8 + .../classes/comments/CommentReplies.d.ts | 8 + .../classes/comments/CommentSimplebox.d.ts | 12 + .../livechat/items/LiveChatPaidSticker.d.ts | 19 + typings/lib/parser/contents/index.d.ts | 4 +- typings/lib/parser/index.d.ts | 2 +- typings/lib/parser/youtube/Comments.d.ts | 25 ++ typings/lib/parser/youtube/VideoInfo.d.ts | 3 +- typings/lib/proto/index.d.ts | 2 +- 291 files changed, 2774 insertions(+), 1831 deletions(-) create mode 100644 examples/comments/Comment.md create mode 100644 examples/comments/CommentThread.md create mode 100644 examples/comments/README.md create mode 100644 examples/comments/index.js create mode 100644 lib/parser/contents/classes/Comment.js delete mode 100644 lib/parser/contents/classes/CommentActionButtons.js create mode 100644 lib/parser/contents/classes/CommentReplyDialog.js create mode 100644 lib/parser/contents/classes/CommentThread.js create mode 100644 lib/parser/contents/classes/CommentsHeader.js create mode 100644 lib/parser/contents/classes/comments/AuthorCommentBadge.js create mode 100644 lib/parser/contents/classes/comments/CommentActionButtons.js create mode 100644 lib/parser/contents/classes/comments/CommentReplies.js create mode 100644 lib/parser/contents/classes/comments/CommentSimplebox.js create mode 100644 lib/parser/youtube/Comments.js create mode 100644 typings/lib/parser/contents/classes/Comment.d.ts create mode 100644 typings/lib/parser/contents/classes/CommentReplyDialog.d.ts create mode 100644 typings/lib/parser/contents/classes/CommentThread.d.ts create mode 100644 typings/lib/parser/contents/classes/CommentsHeader.d.ts create mode 100644 typings/lib/parser/contents/classes/comments/AuthorCommentBadge.d.ts create mode 100644 typings/lib/parser/contents/classes/comments/Comment.d.ts create mode 100644 typings/lib/parser/contents/classes/comments/CommentActionButtons.d.ts create mode 100644 typings/lib/parser/contents/classes/comments/CommentReplies.d.ts create mode 100644 typings/lib/parser/contents/classes/comments/CommentSimplebox.d.ts create mode 100644 typings/lib/parser/contents/classes/livechat/items/LiveChatPaidSticker.d.ts create mode 100644 typings/lib/parser/youtube/Comments.d.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index c6e6843d..87e5b253 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -53,4 +53,34 @@ rules: no-useless-call: error no-useless-concat: error no-useless-escape: error - no-useless-return: error \ No newline at end of file + no-useless-return: error + no-else-return: error + no-lonely-if: error + no-undef-init: error + no-unneeded-ternary: error + no-var: error + no-multi-spaces: error + no-multiple-empty-lines: ["error", { "max": 2, "maxEOF": 0 }] + no-tabs: error + no-trailing-spaces: error + + brace-style: error + new-parens: error + space-infix-ops: error + template-curly-spacing: error + wrap-regex: error + capitalized-comments: error + prefer-template: error + + keyword-spacing: ["error", { "before": true } ] + strict: ["error", "global"] + array-bracket-spacing: ["error", "always"] + arrow-parens: ["error", "always"] + comma-dangle: ["error", "never"] + comma-spacing: ["error", { "before": false, "after": true }] + computed-property-spacing: ["error", "never"] + func-call-spacing: ["error", "never"] + indent: ["error", 2, { "SwitchCase": 1 }] + key-spacing: ["error", { "beforeColon": false }] + semi: ["error", "always"] + operator-assignment: ["error", "always"] \ No newline at end of file diff --git a/examples/comments/Comment.md b/examples/comments/Comment.md new file mode 100644 index 00000000..9448318f --- /dev/null +++ b/examples/comments/Comment.md @@ -0,0 +1,34 @@ +## Comment +Contains information about a single comment. A [`Comment`](../../lib/parser/contents/classes/Comment.js) can be a top-level comment or a reply to a top-level comment. + +## API + +* Comment + * [.like](#like) ⇒ `function` + * [.dislike](#dislike) ⇒ `function` + * [.reply](#comment) ⇒ `function` + * [.translate](#translate) ⇒ `function` + + +### like() +Likes the comment. + +**Returns:** `Promise.<{ success: boolean, status_code: number, data: object }>` + + +### dislike() +Dislikes the comment. + +**Returns:** `Promise.<{ success: boolean, status_code: number, data: object }>` + + +### reply(text) +Creates a reply to the comment. **Note:** To create a top-level comment, use the [`Comments#comment(text)`](./README.md#comment) method. + +**Returns:** `Promise.<{ success: boolean, status_code: number, data: object }>` + + +### translate(target_language) +Translates the comment to the given language. + +**Returns:** `Promise.<{ success: boolean, status_code: number, data: object, content: string }>` diff --git a/examples/comments/CommentThread.md b/examples/comments/CommentThread.md new file mode 100644 index 00000000..96091728 --- /dev/null +++ b/examples/comments/CommentThread.md @@ -0,0 +1,35 @@ +# CommentThread + +A `CommentThread` represents a top-level comment and its replies. + +## API + +* CommentThread + * [.comment](#comment) ⇒ `Comment` + * [.replies](#replies) ⇒ `Comment[]` + * [.getReplies](#getreplies) ⇒ `function` + * [.getContinuation](#getcontinuation) ⇒ `function` + + +### comment +The top-level comment. **Note:** More about `Comment` [here](./Comment.md). + +**Type:** [`Comment`](../../lib/parser/contents/classes/Comment.js) + + +### replies +An array of replies to the top-level comment. (not populated until [`getReplies()`](#getreplies) is called). + +**Type:** [`Comment[]`](../../lib/parser/contents/classes/Comment.js) + + +### getReplies() +Retrieves replies to the top-level comment and attaches a [`replies`](#replies) array to the original `CommentThread` object and returns it. + +**Returns:** [`Promise.`](../../lib/parser/contents/classes/CommentThread.js) + + +### getContinuation() +Retrieves next batch of replies and adds them to the [`replies`](#replies) array. **Note:** [`getReplies()`](#getreplies) must be called before using this. + +**Returns:** [`Promise.`](../../lib/parser/contents/classes/CommentThread.js) \ No newline at end of file diff --git a/examples/comments/README.md b/examples/comments/README.md new file mode 100644 index 00000000..c8517249 --- /dev/null +++ b/examples/comments/README.md @@ -0,0 +1,47 @@ +## Comments +YouTube.js has full support for comments, including comment actions such as liking, disliking, replying etc. + +## Usage +Get a [`Comments`](../../lib/parser/youtube/Comments.js) instance: + +```js +const comments = await session.getComments(VIDEO_ID); +``` + +## API +* Comments + * [.contents](#commentthread) ⇒ `CommentThread[]` + * [.comment](#comment) ⇒ `function` + * [.getContinuation](#getc) ⇒ `function` + * [.page](#page) ⇒ `getter` + + +### contents +A list of comment threads. **Note:** More about comment threads [**here**](./CommentThread.md). + +**Type:** [`CommentThread[]`](../../lib/parser/contents/classes/CommentThread.js) + + +### comment(text) +Posts a top-level comment. + +| Param | Type | Description | +| --- | --- | --- | +| text | `string` | Comment content | + +**Returns:** `Promise.` + + +### getContinuation() +Retrieves next batch of comment threads. + +**Returns:** [`Promise.`](../../lib/parser/youtube/Comments.js) + + +### page +Returns original InnerTube response (sanitized). + +**Returns:** `Promise.` + +## Example +See [`index.js`]('./index.js'). \ No newline at end of file diff --git a/examples/comments/index.js b/examples/comments/index.js new file mode 100644 index 00000000..f90433f7 --- /dev/null +++ b/examples/comments/index.js @@ -0,0 +1,33 @@ +import Innertube from 'youtubei.js'; + +const session = await new Innertube(); + +const comments = await session.getComments('a-rqu-hjobc'); + +console.info(`This video has ${comments.header.comments_count.toString()} comments.`); + +for (const thread of comments.contents) { + const comment = thread.comment; + + console.info( + `${comment.author.name} • ${comment.published}\n`, + `${comment.content.toString()}`, '\n', + `Likes: ${comment.vote_count.short_text}`, '\n' + ); + + if (comment.reply_count > 0) { + console.info('Replies:', '\n'); + + const comment_thread = await thread.getReplies(); + + for (const reply of comment_thread.replies) { + console.info( + `> ${reply.author.name} • ${reply.published}\n`, + `${reply.content.toString()}`, '\n', + `Likes: ${reply.vote_count.short_text}`, '\n' + ); + } + } + + console.log('\n'); +} \ No newline at end of file diff --git a/examples/livechat/index.js b/examples/livechat/index.js index 2c60f6e7..feceb4e8 100644 --- a/examples/livechat/index.js +++ b/examples/livechat/index.js @@ -1,4 +1,4 @@ -import Innertube from '../..'; +import Innertube from 'youtubei.js'; const session = await new Innertube(); diff --git a/lib/Innertube.js b/lib/Innertube.js index dc4581c4..fef7d96a 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -16,6 +16,7 @@ const Channel = require('./parser/youtube/Channel'); const Playlist = require('./parser/youtube/Playlist'); const Library = require('./parser/youtube/Library'); const History = require('./parser/youtube/History'); +const Comments = require('./parser/youtube/Comments'); const YTMusic = require('./core/Music'); const FilterableFeed = require('./core/FilterableFeed'); @@ -31,7 +32,7 @@ const { PassThrough } = require('stream'); class Innertube { #axios; #player; - + /** * @example * ```js @@ -50,21 +51,21 @@ class Innertube { this.config = config || {}; return this.#init(); } - + async #init() { const session = await new SessionBuilder(this.config).build(); this.key = session.key; this.version = session.api_version; this.context = session.context; - + this.logged_in = false; this.player_url = session.player.url; this.sts = session.player.sts; - + this.#axios = session.axios; this.#player = session.player; - + /** * @fires Innertube#auth - fired when signing in to an account. * @fires Innertube#update-credentials - fired when the access token is no longer valid. @@ -72,21 +73,21 @@ class Innertube { */ this.ev = new EventEmitter(); this.oauth = new OAuth(this.ev, session.axios); - + if (this.config.cookie) { this.auth_apisid = Utils.getStringBetweenStrings(this.config.cookie, 'PAPISID=', ';'); this.auth_apisid = Utils.generateSidAuth(this.auth_apisid); } - + this.request = new Request(this); this.actions = new Actions(this); - + this.account = new AccountManager(this.actions); this.playlist = new PlaylistManager(this.actions); this.interact = new InteractionManager(this.actions); - + this.music = new YTMusic(this); - + return this; } @@ -131,13 +132,16 @@ class Innertube { */ async signOut() { if (!this.logged_in) throw new Utils.InnertubeError('You are not signed in'); + const response = await this.oauth.revokeAccessToken(); + if (response.success) { - this.logged_in = false; - } + this.logged_in = false; + } + return response; } - + /** * Retrieves video info. * @@ -147,14 +151,14 @@ class Innertube { async getInfo(video_id) { Utils.throwIfMissing({ video_id }); const cpn = Utils.generateRandomString(16); - + const initial_info = this.actions.getVideoInfo(video_id, cpn); const continuation = this.actions.next({ video_id }); - + const response = await Promise.all([ initial_info, continuation ]); return new VideoInfo(response, this.actions, this.#player, cpn); } - + /** * Retrieves basic video info. * @@ -164,32 +168,32 @@ class Innertube { async getBasicInfo(video_id) { Utils.throwIfMissing({ video_id }); const cpn = Utils.generateRandomString(16); - + const response = await this.actions.getVideoInfo(video_id, cpn); return new VideoInfo([ response, {} ], this.actions, this.#player, cpn); } - + /** - * Searches a given query. - * + * Searches a given query. + * * @param {string} query - search query. * @param {object} [filters] - search filters. * @param {string} [filters.upload_date] - filter videos by upload date, can be: any | last_hour | today | this_week | this_month | this_year * @param {string} [filters.type] - filter results by type, can be: any | video | channel | playlist | movie - * @param {string} [filters.duration] - filter videos by duration, can be: any | short | medium | long + * @param {string} [filters.duration] - filter videos by duration, can be: any | short | medium | long * @param {string} [filters.sort_by] - filter video results by order, can be: relevance | rating | upload_date | view_count * @returns {Promise.} */ async search(query, filters = {}) { Utils.throwIfMissing({ query }); - + const response = await this.actions.search({ query, filters }); return new Search(this.actions, response.data); } - + /** * Retrieves search suggestions for a given query. - * + * * @param {string} query - the search query. * @param {object} [options] - search options. * @param {string} [options.client='YOUTUBE'] - client used to retrieve search suggestions, can be: `YOUTUBE` or `YTMUSIC`. @@ -197,7 +201,7 @@ class Innertube { */ async getSearchSuggestions(query, options = { client: 'YOUTUBE' }) { Utils.throwIfMissing({ query }); - + const response = await this.actions.getSearchSuggestions(options.client, query); if (options.client === 'YTMUSIC' && !response.data.contents) return []; @@ -214,23 +218,18 @@ class Innertube { * * @param {string} video_id - the video id. * @param {string} [sort_by] - can be: `TOP_COMMENTS` or `NEWEST_FIRST`. - * @returns {Promise.<{ page_count: number, comment_count: number, items: object[] }>} + * @returns {Promise.} */ async getComments(video_id, sort_by) { Utils.throwIfMissing({ video_id }); - + const payload = Proto.encodeCommentsSectionParams(video_id, { sort_by: sort_by || 'TOP_COMMENTS' }); const response = await this.actions.next({ ctoken: payload }); - const comments = new OldParser(this, response.data, { - video_id, - client: 'YOUTUBE', - data_type: 'COMMENTS' - }).parse(); - return comments; + return new Comments(this.actions, response.data); } /** @@ -242,7 +241,7 @@ class Innertube { const response = await this.actions.browse('FEwhat_to_watch'); return new FilterableFeed(this.actions, response.data); } - + /** * Returns the account's library. * @@ -252,11 +251,11 @@ class Innertube { const response = await this.actions.browse('FElibrary'); return new Library(response.data, this.actions); } - + /** * Retrieves watch history. * Which can also be achieved through {@link getLibrary()}. - * + * * @returns {Promise.} */ async getHistory() { @@ -266,7 +265,7 @@ class Innertube { /** * Retrieves trending content. - * + * * @returns {Promise} */ async getTrending() { @@ -281,7 +280,7 @@ class Innertube { */ async getSubscriptionsFeed() { const response = await this.actions.browse('FEsubscriptions'); - + const subsfeed = new OldParser(this, response, { client: 'YOUTUBE', data_type: 'SUBSFEED' @@ -289,7 +288,7 @@ class Innertube { return subsfeed; } - + /** * Retrieves contents for a given channel. (WIP) * @@ -301,7 +300,7 @@ class Innertube { const response = await this.actions.browse(id); return new Channel(this.actions, response.data); } - + /** * Retrieves notifications. * @@ -309,7 +308,7 @@ class Innertube { */ async getNotifications() { const response = await this.actions.notifications('get_notification_menu'); - + const notifications = new OldParser(this, response.data, { client: 'YOUTUBE', data_type: 'NOTIFICATIONS' @@ -330,7 +329,7 @@ class Innertube { /** * Retrieves the contents of a given playlist. - * + * * @param {string} playlist_id - the id of the playlist. * @returns {Promise.} */ @@ -343,7 +342,7 @@ class Innertube { /** * An alternative to {@link download}. * Returns deciphered streaming data. - * + * * @param {string} video_id - video id * @param {object} options - download options. * @param {string} options.quality - video quality; 360p, 720p, 1080p, etc... @@ -358,7 +357,7 @@ class Innertube { /** * Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}. - * + * * @param {string} video_id - video id * @param {object} options - download options. * @param {string} [options.quality] - video quality; 360p, 720p, 1080p, etc... @@ -384,11 +383,11 @@ class Innertube { getPlayer() { return this.#player; - } - + } + /** @readonly */ get axios() { - return this.#axios; + return this.#axios; } } diff --git a/lib/core/AccountManager.js b/lib/core/AccountManager.js index 93450f4f..27eef650 100644 --- a/lib/core/AccountManager.js +++ b/lib/core/AccountManager.js @@ -8,19 +8,19 @@ const Proto = require('../proto'); /** @namespace */ class AccountManager { #actions; - + /** * @param {import('./Actions')} actions */ constructor (actions) { this.#actions = actions; - + /** - * API response. + * API response. * * @typedef {{ success: boolean, status_code: number, data: object }} Response */ - + /** @namespace */ this.channel = { /** @@ -30,7 +30,7 @@ class AccountManager { * @returns {Promise.} */ editName: (new_name) => this.#actions.channel('channel/edit_name', { new_name }), - + /** * Edits channel description. * @@ -38,22 +38,22 @@ class AccountManager { * @returns {Promise.} */ editDescription: (new_description) => this.#actions.channel('channel/edit_description', { new_description }), - + /** * Retrieves basic channel analytics. * * @borrows getAnalytics as getBasicAnalytics */ getBasicAnalytics: () => this.getAnalytics() - } - + }; + /** @namespace */ this.settings = { notifications: { /** * Notify about activity from the channels you're subscribed to. * - * @param {boolean} option - ON | OFF + * @param {boolean} option - ON | OFF * @returns {Promise.} */ setSubscriptions: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'SPaccount_notifications', option), @@ -61,7 +61,7 @@ class AccountManager { /** * Recommended content notifications. * - * @param {boolean} option - ON | OFF + * @param {boolean} option - ON | OFF * @returns {Promise.} */ setRecommendedVideos: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'SPaccount_notifications', option), @@ -69,7 +69,7 @@ class AccountManager { /** * Notify about activity on your channel. * - * @param {boolean} option - ON | OFF + * @param {boolean} option - ON | OFF * @returns {Promise.} */ setChannelActivity: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'SPaccount_notifications', option), @@ -77,7 +77,7 @@ class AccountManager { /** * Notify about replies to your comments. * - * @param {boolean} option - ON | OFF + * @param {boolean} option - ON | OFF * @returns {Promise.} */ setCommentReplies: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'SPaccount_notifications', option), @@ -85,7 +85,7 @@ class AccountManager { /** * Notify when others mention your channel. * - * @param {boolean} option - ON | OFF + * @param {boolean} option - ON | OFF * @returns {Promise.} */ setMentions: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'SPaccount_notifications', option), @@ -93,7 +93,7 @@ class AccountManager { /** * Notify when others share your content on their channels. * - * @param {boolean} option - ON | OFF + * @param {boolean} option - ON | OFF * @returns {Promise.} */ setSharedContent: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'SPaccount_notifications', option) @@ -102,7 +102,7 @@ class AccountManager { /** * If set to true, your subscriptions won't be visible to others. * - * @param {boolean} option - ON | OFF + * @param {boolean} option - ON | OFF * @returns {Promise.} */ setSubscriptionsPrivate: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'SPaccount_privacy', option), @@ -110,44 +110,44 @@ class AccountManager { /** * If set to true, saved playlists won't appear on your channel. * - * @param {boolean} option - ON | OFF + * @param {boolean} option - ON | OFF * @returns {Promise.} */ setSavedPlaylistsPrivate: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.PLAYLISTS_PRIVACY, 'SPaccount_privacy', option) } - } + }; } - + /** * Internal method to perform changes on an account's settings. * * @private - * @param {string} setting_id - * @param {string} type - * @param {string} new_value + * @param {string} setting_id + * @param {string} type + * @param {string} new_value * @returns {Promise.} */ async #setSetting(setting_id, type, new_value) { Utils.throwIfMissing({ setting_id, type, new_value }); - + const values = { ON: true, OFF: false }; - - if (!values.hasOwnProperty(new_value)) + + if (!values.hasOwnProperty(new_value)) throw new Utils.InnertubeError('Invalid option', { option: new_value, available_options: Object.keys(values) }); - + const response = await this.#actions.browse(type); - - const contents = (() => { - switch (type.trim()) { - case 'SPaccount_notifications': - return Utils.findNode(response.data, 'contents', 'Your preferences', 13, false).options; - case 'SPaccount_privacy': - return Utils.findNode(response.data, 'contents', 'settingsSwitchRenderer', 13, false).options; - default: - // this is just for maximum compatibility, this is most definitely a bad way to handle this - throw new TypeError('undefined is not a function'); - } - })() + + const contents = (() => { + switch (type.trim()) { + case 'SPaccount_notifications': + return Utils.findNode(response.data, 'contents', 'Your preferences', 13, false).options; + case 'SPaccount_privacy': + return Utils.findNode(response.data, 'contents', 'settingsSwitchRenderer', 13, false).options; + default: + // This is just for maximum compatibility, this is most definitely a bad way to handle this + throw new TypeError('undefined is not a function'); + } + })(); const option = contents.find((option) => option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemIdForClient == setting_id); @@ -159,7 +159,7 @@ class AccountManager { return set_setting; } - + /** * Retrieves channel info. * @@ -167,19 +167,19 @@ class AccountManager { */ async getInfo() { const response = await this.#actions.account('account/accounts_list', { client: 'ANDROID' }); - + const account_item_section_renderer = Utils.findNode(response.data, 'contents', 'accountItem', 8, false); const profile = account_item_section_renderer.accountItem.serviceEndpoint.signInEndpoint.directSigninUserProfile; - + const name = profile.accountName; const email = profile.email; const photo = profile.accountPhoto.thumbnails; const subscriber_count = account_item_section_renderer.accountItem.accountByline.runs.map((run) => run.text).join(''); const channel_id = response.data.contents[0].accountSectionListRenderer.footers[0].accountChannelRenderer.navigationEndpoint.browseEndpoint.browseId; - + return { name, email, channel_id, subscriber_count, photo }; } - + /** * Retrieves time watched statistics. * @@ -187,22 +187,22 @@ class AccountManager { */ async getTimeWatched() { const response = await this.#actions.browse('SPtime_watched', { client: 'ANDROID' }); - + const rows = Utils.findNode(response.data, 'contents', 'statRowRenderer', 11, false); - + const stats = rows.map((row) => { const renderer = row.statRowRenderer; if (renderer) { return { title: renderer.title.runs.map((run) => run.text).join(''), time: renderer.contents.runs.map((run) => run.text).join('') - } + }; } }).filter((stat) => stat); - + return stats; } - + /** * Retrieves basic channel analytics. * @@ -210,10 +210,10 @@ class AccountManager { */ async getAnalytics() { const info = await this.getInfo(); - + const params = Proto.encodeChannelAnalyticsParams(info.channel_id); const response = await this.#actions.browse('FEanalytics_screen', { params, client: 'ANDROID' }); - + return new Analytics(response.data); } } diff --git a/lib/core/Actions.js b/lib/core/Actions.js index c3a0838a..de1a2346 100644 --- a/lib/core/Actions.js +++ b/lib/core/Actions.js @@ -9,7 +9,7 @@ const Constants = require('../utils/Constants'); class Actions { #session; #request; - + /** * @param {import('../Innertube')} session */ @@ -17,20 +17,20 @@ class Actions { this.#session = session; this.#request = session.request; } - + /** * API response. * * @typedef {{ success: boolean, status_code: number, data: object }} Response */ - + /** * Covers `/browse` endpoint, mostly used to access - * YouTube's sections such as the home feed, etc + * YouTube's sections such as the home feed, etc * and sometimes to retrieve continuations. - * + * * @param {string} id - browseId or a continuation token - * @param {object} args - additional arguments + * @param {object} args - additional arguments * @param {string} [args.params] * @param {boolean} [args.is_ytm] * @param {boolean} [args.is_ctoken] @@ -40,30 +40,31 @@ class Actions { async browse(id, args = {}) { if (this.#needsLogin(id) && !this.#session.logged_in) throw new Utils.InnertubeError('You are not signed in'); - + const data = {}; - - if (args.params) data.params = args.params; - - if (args.is_ctoken) { - data.continuation = id - } else { - data.browseId = id; - } - - if (args.client) { - data.client = args.client; - } - + + if (args.params) + data.params = args.params; + + if (args.is_ctoken) { + data.continuation = id; + } else { + data.browseId = id; + } + + if (args.client) { + data.client = args.client; + } + const response = await this.#request.post('/browse', data); - + return response; } - + /** * Covers endpoints used to perform direct interactions * on YouTube. - * + * * @param {string} action * @param {object} args * @param {string} [args.video_id] @@ -75,23 +76,23 @@ class Actions { async engage(action, args = {}) { if (!this.#session.logged_in && !args.hasOwnProperty('text')) throw new Utils.InnertubeError('You are not signed in'); - + const data = {}; - + switch (action) { case 'like/like': case 'like/dislike': case 'like/removelike': data.target = {}; - data.videoId = args.video_id; - + data.target.videoId = args.video_id; + if (args.params) { - data.params = args.params; - } + data.params = args.params; + } break; case 'subscription/subscribe': case 'subscription/unsubscribe': - data.channelIds = [args.channel_id]; + data.channelIds = [ args.channel_id ]; data.params = action === 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA'; break; case 'comment/create_comment': @@ -103,33 +104,32 @@ class Actions { data.commentText = args.text; break; case 'comment/perform_comment_action': - const target_action = (() => { - switch (args.comment_action) { - case 'like': - return Proto.encodeCommentActionParams(5, args); - case 'dislike': - return Proto.encodeCommentActionParams(4, args); - case 'translate': - return Proto.encodeCommentActionParams(22, args); - default: - // this is just for maximum compatibility, this is most definitely a bad way to handle this - throw new TypeError('undefined is not a function'); - } - })() - + const target_action = (() => { + switch (args.comment_action) { + case 'like': + return Proto.encodeCommentActionParams(5, args); + case 'dislike': + return Proto.encodeCommentActionParams(4, args); + case 'translate': + return Proto.encodeCommentActionParams(22, args); + default: + break; + } + })(); + data.actions = [ target_action ]; break; default: throw new Utils.InnertubeError('Action not implemented', action); } - + const response = await this.#request.post(`/${action}`, data); return response; } - + /** * Covers endpoints related to account management. - * + * * @param {string} action * @param {object} args * @param {string} [args.new_value] @@ -137,14 +137,18 @@ class Actions { * @returns {Promise.} */ async account(action, args = {}) { - if (!this.#session.logged_in) + if (!this.#session.logged_in) throw new Utils.InnertubeError('You are not signed in'); - const data = { client: args.client }; - + const data = { + client: args.client + }; + switch (action) { case 'account/set_setting': - data.newValue = { boolValue: args.new_value }; + data.newValue = { + boolValue: args.new_value + }; data.settingItemId = args.setting_item_id; break; case 'account/accounts_list': @@ -152,14 +156,14 @@ class Actions { default: throw new Utils.InnertubeError('Action not implemented', action); } - + const response = await this.#request.post(`/${action}`, data); return response; } - + /** * Endpoint used for search. - * + * * @param {object} args * @param {string} [args.query] * @param {object} [args.options] @@ -171,41 +175,37 @@ class Actions { * @returns {Promise.} */ async search(args = {}) { - const data = {}; - - if (args.query) { - data.query = args.query; - } - - if (args.ctoken) { - data.continuation = args.ctoken; - } - - if (args.params) { - data.params - } - - if (args.filters) { - if (args.client == 'YTMUSIC') { - data.filters = Proto.encodeMusicSearchFilters(args.filters); - } else { - data.filters = Proto.encodeSearchFilters(args.filters); - } + const data = { client: args.client }; + + if (args.query) { + data.query = args.query; } - - if (args.client) { - data.client - } - + + if (args.ctoken) { + data.continuation = args.ctoken; + } + + if (args.params) { + data.params; + } + + if (args.filters) { + if (args.client == 'YTMUSIC') { + data.filters = Proto.encodeMusicSearchFilters(args.filters); + } else { + data.filters = Proto.encodeSearchFilters(args.filters); + } + } + const response = await this.#request.post('/search', data); - + return response; } - - + + /** * Endpoint used fo Shorts' sound search. - * + * * @param {object} args * @param {string} args.query * @returns {Promise.} @@ -213,16 +213,16 @@ class Actions { async searchSound(args = {}) { const data = { query: args.query, - client: 'ANDROID', + client: 'ANDROID' }; - + const response = await this.#request.post('/sfv/search', data); return response; } - + /** * Channel management endpoints. - * + * * @param {string} action * @param {object} args * @param {string} [args.new_name] @@ -232,9 +232,11 @@ class Actions { async channel(action, args = {}) { if (!this.#session.logged_in) throw new Utils.InnertubeError('You are not signed in'); - - const data = { client: args.client || 'ANDROID' }; - + + const data = { + client: args.client || 'ANDROID' + }; + switch (action) { case 'channel/edit_name': data.givenName = args.new_name; @@ -247,15 +249,15 @@ class Actions { default: throw new Utils.InnertubeError('Action not implemented', action); } - + const response = await this.#request.post(`/${action}`, data); - + return response; } - + /** * Covers endpoints used for playlist management. - * + * * @param {string} action * @param {object} args * @param {string} [args.title] @@ -265,11 +267,11 @@ class Actions { * @returns {Promise.} */ async playlist(action, args = {}) { - if (!this.#session.logged_in) + if (!this.#session.logged_in) throw new Utils.InnertubeError('You are not signed in'); - + const data = {}; - + switch (action) { case 'playlist/create': data.title = args.title; @@ -281,35 +283,34 @@ class Actions { case 'browse/edit_playlist': data.playlistId = args.playlist_id; data.actions = args.ids.map((id) => { - switch (args.action) { - case 'ACTION_ADD_VIDEO': - return { - action: args.action, - addedVideoId: id - } - case 'ACTION_REMOVE_VIDEO': - return { - action: args.action, - setVideoId: id - } - default: - // this is just for maximum compatibility, this is most definitely a bad way to handle this - throw new TypeError('undefined is not a function'); - } - }); + switch (args.action) { + case 'ACTION_ADD_VIDEO': + return { + action: args.action, + addedVideoId: id + }; + case 'ACTION_REMOVE_VIDEO': + return { + action: args.action, + setVideoId: id + }; + default: + break; + } + }); break; default: throw new Utils.InnertubeError('Action not implemented', action); } - + const response = await this.#request.post(`/${action}`, data); - + return response; } - + /** * Covers endpoints used for notifications management. - * + * * @param {string} action * @param {object} args * @param {string} [args.pref] @@ -318,14 +319,18 @@ class Actions { * @returns {Promise.} */ async notifications(action, args = {}) { - if (!this.#session.logged_in) + if (!this.#session.logged_in) throw new Utils.InnertubeError('You are not signed in'); - + const data = {}; - + switch (action) { case 'modify_channel_preference': - const pref_types = { PERSONALIZED: 1, ALL: 2, NONE: 3 }; + const pref_types = { + PERSONALIZED: 1, + ALL: 2, + NONE: 3 + }; data.params = Proto.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()]); break; case 'get_notification_menu': @@ -340,15 +345,15 @@ class Actions { default: throw new Utils.InnertubeError('Action not implemented', action); } - + const response = await this.#request.post(`/notification/${action}`, data); - + return response; } - + /** * Covers livechat endpoints. - * + * * @param {string} action * @param {object} args * @param {string} [args.text] @@ -360,7 +365,7 @@ class Actions { */ async livechat(action, args = {}) { const data = { client: args.client }; - + switch (action) { case 'live_chat/get_live_chat': case 'live_chat/get_live_chat_replay': @@ -370,11 +375,13 @@ class Actions { data.params = Proto.encodeMessageParams(args.channel_id, args.video_id); data.clientMessageId = Uuid.v4(); data.richMessage = { - textSegments: [{ text: args.text }] - } + textSegments: [ { + text: args.text + } ] + }; break; case 'live_chat/get_item_context_menu': - // note: this is currently broken due to a recent refactor + // Note: this is currently broken due to a recent refactor break; case 'live_chat/moderate': data.params = args.params; @@ -386,15 +393,15 @@ class Actions { default: throw new Utils.InnertubeError('Action not implemented', action); } - + const response = await this.#request.post(`/${action}`, data); - + return response; } - + /** * Endpoint used to retrieve video thumbnails. - * + * * @param {object} args * @param {string} args.video_id * @returns {Promise.} @@ -404,22 +411,22 @@ class Actions { client: 'ANDROID', videoId: args.video_id }; - + const response = await this.#request.post('/thumbnails', data); return response; } - + /** * Place Autocomplete endpoint, found it in the APK but * doesn't seem to be used anywhere on YouTube (maybe for ads?). - * + * * Ex: * ```js * const places = await session.actions.geo('place_autocomplete', { input: 'San diego cafe' }); * console.info(places.data); * ``` - * + * * @param {string} action * @param {object} args * @param {string} args.input @@ -428,20 +435,20 @@ class Actions { async geo(action, args = {}) { if (!this.#session.logged_in) throw new Utils.InnertubeError('You are not signed in'); - + const data = { input: args.input, client: 'ANDROID' }; - + const response = await this.#request.post(`/geo/${action}`, data); return response; } - + /** * Covers endpoints used to report content. - * + * * @param {string} action * @param {object} args * @param {object} [args.action] @@ -451,9 +458,9 @@ class Actions { async flag(action, args) { if (!this.#session.logged_in) throw new Utils.InnertubeError('You are not signed in'); - + const data = {}; - + switch (action) { case 'flag/flag': data.action = args.action; @@ -464,15 +471,15 @@ class Actions { default: throw new Utils.InnertubeError('Action not implemented', action); } - + const response = await this.#request.post(`/${action}`, data); - + return response; } - + /** * Covers specific YouTube Music endpoints. - * + * * @param {string} action * @param {object} args * @param {string} [args.input] @@ -483,15 +490,15 @@ class Actions { input: args.input || '', client: 'YTMUSIC' }; - + const response = await this.#request.post(`/music/${action}`, data); - + return response; } - + /** * Mostly used for pagination and specific operations. - * + * * @param {object} args * @param {string} [args.video_id] * @param {string} [args.ctoken] @@ -499,28 +506,24 @@ class Actions { * @returns {Promise.} */ async next(args = {}) { - const data = {}; - + const data = { client: args.client }; + if (args.ctoken) { - data.continuation - } - + data.continuation = args.ctoken; + } + if (args.video_id) { - data.videoId - } - - if (args.client) { - data.client - } - + data.videoId = args.video_id; + } + const response = await this.#request.post('/next', data); - + return response; } - + /** * Used to retrieve video info. - * + * * @param {string} id * @param {string} [cpn] * @param {string} [client] @@ -533,7 +536,7 @@ class Actions { vis: 0, splay: false, referer: 'https://www.youtube.com', - currentUrl: '/watch?v=' + id, + currentUrl: `/watch?v=${id}`, autonavState: 'STATE_OFF', signatureTimestamp: this.#session.sts, autoCaptionsDefaultOn: false, @@ -546,35 +549,35 @@ class Actions { }, videoId: id }; - - if (client) { - data.client = client - } - - if (cpn) { - data.cpn = cpn - } - + + if (client) { + data.client = client; + } + + if (cpn) { + data.cpn = cpn; + } + const response = await this.#request.post('/player', data); - + return response.data; } - + /** * Covers search suggestion endpoints. - * + * * @param {string} client * @param {string} query * @returns {Promise.} */ async getSearchSuggestions(client, query) { - if (!['YOUTUBE', 'YTMUSIC'].includes(client)) + if (![ 'YOUTUBE', 'YTMUSIC' ].includes(client)) throw new Utils.InnertubeError('Invalid client', client); - + const response = await ({ - YOUTUBE: () => this.#request({ + YOUTUBE: () => this.#request({ url: 'search', - baseURL: Constants.URLS.YT_SUGGESTIONS, + baseURL: Constants.URLS.YT_SUGGESTIONS, params: { q: query, ds: 'yt', @@ -585,15 +588,17 @@ class Actions { hl: this.#session.context.client.hl } }), - YTMUSIC: () => this.music('get_search_suggestions', { input: query }) + YTMUSIC: () => this.music('get_search_suggestions', { + input: query + }) }[client])(); - + return response; } - + /** * Endpoint used to retrieve user mention suggestions. - * + * * @param {object} args * @param {string} args.input * @returns {Promise.} @@ -601,21 +606,51 @@ class Actions { async getUserMentionSuggestions(args = {}) { if (!this.#session.logged_in) throw new Utils.InnertubeError('You are not signed in'); - + const data = { input: args.input, client: 'ANDROID' }; - + const response = await this.#request.post('get_user_mention_suggestions', data); - + return response; } - + + /** + * Executes an API call. + * @param {string} action - endpoint + * @param {object} args - call arguments + */ + async execute(action, args) { + const data = { ...args }; + + if (Reflect.has(data, 'request')) + delete data.request; + + if (Reflect.has(data, 'clientActions')) + delete data.clientActions; + + if (Reflect.has(data, 'action')) { + data.actions = [ data.action ]; + delete data.action; + } + + if (Reflect.has(data, 'token')) { + data.continuation = data.token; + delete data.token; + } + + return this.#request.post(action, data); + } + #needsLogin(id) { return [ - 'FElibrary', 'FEhistory', 'FEsubscriptions', - 'SPaccount_notifications', 'SPaccount_privacy', + 'FElibrary', + 'FEhistory', + 'FEsubscriptions', + 'SPaccount_notifications', + 'SPaccount_privacy', 'SPtime_watched' ].includes(id); } diff --git a/lib/core/Feed.js b/lib/core/Feed.js index edea1474..571c87cd 100644 --- a/lib/core/Feed.js +++ b/lib/core/Feed.js @@ -7,15 +7,15 @@ const { InnertubeError } = require('../utils/Utils'); class Feed { #page; - + /** @type {import('../parser/contents/classes/ContinuationItem')[]} */ #continuation; - + /** @type {import('../core/Actions')} */ #actions; - + #memo; - + constructor(actions, data, already_parsed = false) { if (data.on_response_received_actions || data.on_response_received_endpoints || already_parsed) { this.#page = data; @@ -24,19 +24,19 @@ class Feed { } this.#memo = - + this.#page.on_response_received_commands ? - this.#page.on_response_received_commands_memo: - - this.#page.on_response_received_endpoints ? - this.#page.on_response_received_endpoints_memo: - - this.#page.contents ? - this.#page.contents_memo: - - this.#page.on_response_received_actions ? - this.#page.on_response_received_actions_memo: []; - + this.#page.on_response_received_commands_memo : + + this.#page.on_response_received_endpoints ? + this.#page.on_response_received_endpoints_memo : + + this.#page.contents ? + this.#page.contents_memo : + + this.#page.on_response_received_actions ? + this.#page.on_response_received_actions_memo : []; + this.#actions = actions; } @@ -53,7 +53,7 @@ class Feed { const playlist_videos = memo.get('PlaylistVideo') || []; const playlist_panel_videos = memo.get('PlaylistPanelVideo') || []; const watch_card_compact_videos = memo.get('WatchCardCompactVideo') || []; - + return [ ...videos, ...grid_videos, @@ -73,7 +73,7 @@ class Feed { static getPlaylistsFromMemo(memo) { const playlists = memo.get('Playlist') || []; const grid_playlists = memo.get('GridPlaylist') || []; - return [...playlists, ...grid_playlists]; + return [ ...playlists, ...grid_playlists ]; } /** @@ -100,9 +100,9 @@ class Feed { get channels() { const channels = this.#memo.get('Channel') || []; const grid_channels = this.#memo.get('GridChannel') || []; - return [...channels, ...grid_channels]; + return [ ...channels, ...grid_channels ]; } - + /** * Get all playlists in the feed * @@ -111,11 +111,11 @@ class Feed { get playlists() { return Feed.getPlaylistsFromMemo(this.#memo); } - + get memo() { return this.#memo; } - + /** * Returns contents from the page. * @@ -125,32 +125,32 @@ class Feed { const tab_content = this.#memo.get('Tab')?.[0]?.content; const reload_continuation_items = this.#memo.get('reloadContinuationItemsCommand')?.[0]; const append_continuation_items = this.#memo.get('appendContinuationItemsAction')?.[0]; - + return tab_content || reload_continuation_items || append_continuation_items; } - - /** - * Returns all segments/sections from the page. - * - * @returns {import('../parser/contents/Shelf')[] | import('../parser/contents/RichShelf')[] | import('../parser/contents/ReelShelf')[]} - */ + + /** + * Returns all segments/sections from the page. + * + * @returns {import('../parser/contents/Shelf')[] | import('../parser/contents/RichShelf')[] | import('../parser/contents/ReelShelf')[]} + */ get shelves() { const shelf = this.#page.contents_memo.get('Shelf') || []; const rich_shelf = this.#page.contents_memo.get('RichShelf') || []; const reel_shelf = this.#page.contents_memo.get('ReelShelf') || []; - + return [ ...shelf, ...rich_shelf, ...reel_shelf ]; } - + /** * Finds shelf by title. * * @param {string} title */ getShelf(title) { - return this.shelves.find(shelf => shelf.title.toString() === title); + return this.shelves.find((shelf) => shelf.title.toString() === title); } - + /** * Returns secondary contents from the page. * @@ -159,18 +159,18 @@ class Feed { get secondary_contents() { return this.page.contents?.secondary_contents; } - + get actions() { return this.#actions; } - + /** * Get the original page data */ get page() { return this.#page; } - + /** * Checks if the feed has continuation. * @@ -179,7 +179,7 @@ class Feed { get has_continuation() { return (this.#memo.get('ContinuationItem') || []).length > 0; } - + /** * Retrieves continuation data as it is. * @@ -204,7 +204,7 @@ class Feed { return null; } - + /** * Retrieves next batch of contents and returns a new {@link Feed} object. * diff --git a/lib/core/FilterableFeed.js b/lib/core/FilterableFeed.js index 3fd0b91d..c6e0f4fb 100644 --- a/lib/core/FilterableFeed.js +++ b/lib/core/FilterableFeed.js @@ -8,9 +8,9 @@ class FilterableFeed extends Feed { * @type {import('../parser/contents/ChipCloudChip')[]} */ #chips; - + constructor(actions, data, already_parsed = false) { - super(actions, data, already_parsed) + super(actions, data, already_parsed); } /** @@ -32,9 +32,9 @@ class FilterableFeed extends Feed { } get filters() { - return this.filter_chips.map(chip => chip.text.toString()) || []; + return this.filter_chips.map((chip) => chip.text.toString()) || []; } - + /** * Applies given filter and returns a new {@link Feed} object. * @@ -43,23 +43,23 @@ class FilterableFeed extends Feed { */ async getFilteredFeed(filter) { let target_filter; - + if (typeof filter === 'string') { if (!this.filters.includes(filter)) throw new InnertubeError('Filter not found', { available_filters: this.filters }); - target_filter = this.filter_chips.find(chip => chip.text.toString() === filter); + target_filter = this.filter_chips.find((chip) => chip.text.toString() === filter); } else if (filter.type === 'ChipCloudChip') { target_filter = filter; } else { throw new InnertubeError('Invalid filter'); } - + if (target_filter.is_selected) return this; const response = await target_filter.endpoint.call(this.actions); - + return new Feed(this.actions, response, true); } } diff --git a/lib/core/InteractionManager.js b/lib/core/InteractionManager.js index 0c7f7925..225d61a3 100644 --- a/lib/core/InteractionManager.js +++ b/lib/core/InteractionManager.js @@ -5,20 +5,20 @@ const Utils = require('../utils/Utils'); /** @namespace */ class InteractionManager { #actions; - + /** * @param {import('../Actions')} actions */ constructor(actions) { this.#actions = actions; } - + /** * API response. * * @typedef {{ success: boolean, status_code: number, data: object }} Response */ - + /** * Likes a given video. * @@ -30,7 +30,7 @@ class InteractionManager { const action = await this.#actions.engage('like/like', { video_id }); return action; } - + /** * Dislikes a given video. * @@ -42,23 +42,23 @@ class InteractionManager { const action = await this.#actions.engage('like/dislike', { video_id }); return action; } - + /** * Removes a like/dislike. * - * @param {string} video_id - * @returns {Promise.} + * @param {string} video_id + * @returns {Promise.} */ async removeLike(video_id) { Utils.throwIfMissing({ video_id }); const action = await this.actions.engage('like/removelike', { video_id }); return action; } - + /** * Subscribes to a given channel. * - * @param {string} channel_id + * @param {string} channel_id * @returns {Promise.} */ async subscribe(channel_id) { @@ -66,11 +66,11 @@ class InteractionManager { const action = await this.#actions.engage('subscription/subscribe', { channel_id }); return action; } - + /** * Unsubscribes from a given channel. * - * @param {string} channel_id + * @param {string} channel_id * @returns {Promise.} */ async unsubscribe(channel_id) { @@ -78,12 +78,12 @@ class InteractionManager { const action = await this.#actions.engage('subscription/unsubscribe', { channel_id }); return action; } - + /** * Posts a comment on a given video. - * - * @param {string} video_id - * @param {string} text + * + * @param {string} video_id + * @param {string} text * @returns {Promise.} */ async comment(video_id, text) { @@ -91,7 +91,7 @@ class InteractionManager { const action = await this.#actions.engage('comment/create_comment', { video_id, text }); return action; } - + /** * Translates a given text using YouTube's comment translate feature. * @@ -104,7 +104,7 @@ class InteractionManager { */ async translate(text, target_language, args = {}) { Utils.throwIfMissing({ text, target_language }); - + const response = await await this.#actions.engage('comment/perform_comment_action', { video_id: args.video_id, comment_id: args.comment_id, @@ -112,24 +112,24 @@ class InteractionManager { comment_action: 'translate', text }); - + const translated_content = Utils.findNode(response.data, 'frameworkUpdates', 'content', 7, false); - + return { success: response.success, status_code: response.status_code, translated_content: translated_content.content, data: response.data - } + }; } - + /** * Changes notification preferences for a given channel. * Only works with channels you are subscribed to. * - * @param {string} channel_id + * @param {string} channel_id * @param {string} type - `PERSONALIZED` | `ALL` | `NONE` - * @returns {Promise.} + * @returns {Promise.} */ async setNotificationPreferences(channel_id, type) { Utils.throwIfMissing({ channel_id, type }); diff --git a/lib/core/Music.js b/lib/core/Music.js index e2cb14a8..77a8d331 100644 --- a/lib/core/Music.js +++ b/lib/core/Music.js @@ -14,7 +14,7 @@ const { InnertubeError, observe } = require('../utils/Utils'); class Music { #session; #actions; - + /** * @param {import('../Innertube')} session */ @@ -22,7 +22,7 @@ class Music { this.#session = session; this.#actions = session.actions; } - + /** * Searches on YouTube Music. * @@ -35,7 +35,7 @@ class Music { const response = await this.#actions.search({ query, filters, client: 'YTMUSIC' }); return new Search(response, this.#actions, { is_filtered: filters?.hasOwnProperty('type') && filters.type !== 'all' }); } - + /** * Retrieves the home feed. * @@ -45,27 +45,27 @@ class Music { const response = await this.#actions.browse('FEmusic_home', { client: 'YTMUSIC' }); return new HomeFeed(response, this.#actions); } - + /** * Retrieves the Explore feed. - * + * * @returns {Promise.} */ async getExplore() { const response = await this.#actions.browse('FEmusic_explore', { client: 'YTMUSIC' }); return new Explore(response, this.#actions); } - + /** * Retrieves the Library. - * + * * @returns {Promise.} */ async getLibrary() { const response = await this.#actions.browse('FEmusic_liked_albums', { client: 'YTMUSIC' }); return new Library(response, this.#actions); } - + /** * Retrieves artist's info & content. * @@ -77,10 +77,10 @@ class Music { const response = await this.#actions.browse(artist_id, { client: 'YTMUSIC' }); return new Artist(response, this.#actions); } - + /** * Retrieves album. - * + * * @param {string} album_id * @returns {Promise.} */ @@ -89,7 +89,7 @@ class Music { const response = await this.#actions.browse(album_id, { client: 'YTMUSIC' }); return new Album(response, this.#actions); } - + /** * Retrieves song lyrics. * @@ -97,26 +97,26 @@ class Music { */ async getLyrics(video_id) { const response = await this.#actions.next({ video_id, client: 'YTMUSIC' }); - + const data = Parser.parseResponse(response.data); const tab = data.contents.tabs.get({ title: 'Lyrics' }); - + const page = await tab.endpoint.call(this.#actions, 'YTMUSIC'); if (!page) throw new InnertubeError('Invalid video id'); - + if (page.contents.constructor.name === 'Message') throw new InnertubeError(page.contents.text, video_id); - + const description_shelf = page.contents.contents.get({ type: 'MusicDescriptionShelf' }); - + return { /** @type {string} */ text: description_shelf.description.toString(), /** @type {import('../parser/contents/classes/Text')} */ footer: description_shelf.footer - } + }; } - + /** * Retrieves up next. * @@ -124,13 +124,13 @@ class Music { */ async getUpNext(video_id) { const response = await this.#actions.next({ video_id, client: 'YTMUSIC' }); - + const data = Parser.parseResponse(response.data); const tab = data.contents.tabs.get({ title: 'Up next' }); - + const upnext_content = tab.content.content; if (!upnext_content) throw new InnertubeError('Invalid id', video_id); - + return { /** @type {string} */ id: upnext_content.playlist_id, @@ -140,9 +140,9 @@ class Music { is_editable: upnext_content.is_editable, /** @type {import('../parser/contents/classes/PlaylistPanelVideo')[]} */ contents: observe(upnext_content.contents) - } + }; } - + /** * Retrieves related content. * @@ -150,22 +150,22 @@ class Music { */ async getRelated(video_id) { const response = await this.#actions.next({ video_id, client: 'YTMUSIC' }); - + const data = Parser.parseResponse(response.data); const tab = data.contents.tabs.get({ title: 'Related' }); - + const page = await tab.endpoint.call(this.#actions, 'YTMUSIC'); if (!page) throw new InnertubeError('Invalid video id'); - + const shelves = page.contents.contents.findAll({ type: 'MusicCarouselShelf' }); const info = page.contents.contents.get({ type: 'MusicDescriptionShelf' }); - + return { /** @type {import('../parser/contents/classes/MusicCarouselShelf')[]} */ sections: shelves, /** @type {string} */ info: info?.description.toString() || '' - } + }; } } diff --git a/lib/core/OAuth.js b/lib/core/OAuth.js index 38c573b9..63ba6ffd 100644 --- a/lib/core/OAuth.js +++ b/lib/core/OAuth.js @@ -12,11 +12,11 @@ class OAuth { #oauth_code_url = `${Constants.URLS.YT_BASE}/o/oauth2/device/code`; #oauth_token_url = `${Constants.URLS.YT_BASE}/o/oauth2/token`; #oauth_revoke_url = `${Constants.URLS.YT_BASE}/o/oauth2/revoke`; - + #auth_info = {}; #polling_interval = 5; #ev = null; - + /** * @param {EventEmitter} ev * @param {AxiosInstance} axios @@ -25,7 +25,7 @@ class OAuth { this.#ev = ev; this.#axios = axios; } - + /** * Starts the auth flow in case no valid credentials are available. * @@ -121,7 +121,7 @@ class OAuth { const credentials = { access_token: response.data.access_token, refresh_token: response.data.refresh_token, - expires: expiration_date, + expires: expiration_date }; this.#auth_info = credentials; @@ -133,7 +133,7 @@ class OAuth { } }, 1000 * this.#polling_interval); } - + /** * Refreshes the access token if necessary. * @@ -144,7 +144,7 @@ class OAuth { await this.#refreshAccessToken(); } } - + /** * Gets a new access token using a refresh token. * @@ -157,12 +157,12 @@ class OAuth { client_id: identity.id, client_secret: identity.secret, refresh_token: this.#auth_info.refresh_token, - grant_type: 'refresh_token', + grant_type: 'refresh_token' }; const response = await this.#axios.post(this.#oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error); - if (response instanceof Error) + if (response instanceof Error) return this.#ev.emit('update-credentials', { error: 'Could not refresh access token.', status: 'FAILED' @@ -173,17 +173,17 @@ class OAuth { const credentials = { access_token: response.data.access_token, refresh_token: response.data.refresh_token || this.#auth_info.refresh_token, - expires: expiration_date, + expires: expiration_date }; this.#auth_info = credentials; - + this.#ev.emit('update-credentials', { credentials, status: 'SUCCESS' }); } - + /** * Revokes access token (note that the refresh token will also be revoked). * @@ -194,13 +194,13 @@ class OAuth { return { success: !(response instanceof Error), status_code: response.status || 0 - } + }; } /** * Gets client identity data. * - * @returns {Promise.<{ id: string, secret: string }>} + * @returns {Promise.<{ id: string, secret: string }>} */ async #getClientIdentity() { // This request is made to get the auth script url, hard-coding it isn't viable as it changes overtime. @@ -217,7 +217,7 @@ class OAuth { const client_identity = response.data.replace(/\n/g, '').match(Constants.OAUTH.REGEX.CLIENT_IDENTITY); return client_identity.groups; } - + /** * Returns the access token. * @@ -226,7 +226,7 @@ class OAuth { getAccessToken() { return this.#auth_info.access_token; } - + /** * Returns the refresh token. * @@ -235,7 +235,7 @@ class OAuth { getRefreshToken() { return this.#auth_info.refresh_token; } - + /** * Checks if the auth info format is valid. * diff --git a/lib/core/Player.js b/lib/core/Player.js index c6b40912..66780076 100644 --- a/lib/core/Player.js +++ b/lib/core/Player.js @@ -33,7 +33,7 @@ class Player { this.#player_id = id; this.#axios = axios; this.#cache_dir = `${os.tmpdir()}/yt-cache`; - this.#player_url = Constants.URLS.YT_BASE + '/s/player/' + this.#player_id + '/player_ias.vflset/en_US/base.js'; + this.#player_url = `${Constants.URLS.YT_BASE}/s/player/${this.#player_id}/player_ias.vflset/en_US/base.js`; this.#player_path = `${this.#cache_dir}/${this.#player_id}.bin`; } @@ -42,16 +42,16 @@ class Player { const buffer = (await Fs.promises.readFile(this.#player_path)).buffer; const view = new DataView(buffer); const version = view.getUint32(0, true); - + if (version == Player.LIBRARY_VERSION) { const sig_decipher_len = view.getUint32(8, true); const sig_decipher_buf = buffer.slice(12, 12 + sig_decipher_len); const ntoken_transform_buf = buffer.slice(12 + sig_decipher_len); - + this.#ntoken = NToken.fromArrayBuffer(ntoken_transform_buf); this.#signature = Signature.fromArrayBuffer(sig_decipher_buf); this.#signature_timestamp = view.getUint32(4, true); - + return this; } } @@ -59,14 +59,14 @@ class Player { const response = await this.#axios.get(this.#player_url, { headers: { 'content-type': 'text/javascript', 'user-agent': Utils.getRandomUserAgent('desktop').userAgent } }).catch((error) => error); - + if (response instanceof Error) throw new Utils.InnertubeError('Could not download js player', { player_id: this.#player_id }); this.#signature_timestamp = this.#extractSigTimestamp(response.data); - + const signature_decipher_sc = this.#extractSigDecipherSc(response.data); const ntoken_decipher_sc = this.#extractNTokenSc(response.data); - + this.#signature = Signature.fromSourceCode(signature_decipher_sc); this.#ntoken = NToken.fromSourceCode(ntoken_decipher_sc); @@ -77,26 +77,26 @@ class Player { const ntoken_buf = this.#ntoken.toArrayBuffer(); const sig_decipher_buf = this.#signature.toArrayBuffer(); const buffer = new ArrayBuffer(12 + sig_decipher_buf.byteLength + ntoken_buf.byteLength); - + const view = new DataView(buffer); view.setUint32(0, Player.LIBRARY_VERSION, true); view.setUint32(4, this.#signature_timestamp, true); view.setUint32(8, sig_decipher_buf.byteLength, true); - + new Uint8Array(buffer).set(new Uint8Array(sig_decipher_buf), 12); new Uint8Array(buffer).set(new Uint8Array(ntoken_buf), 12 + sig_decipher_buf.byteLength); // Cache the current player await Fs.promises.mkdir(this.#cache_dir, { recursive: true }); await Fs.promises.writeFile(this.#player_path, new Uint8Array(buffer)); - } finally { /* do nothing */ } + } finally { /* Do nothing */ } return this; } decipher(url, signature_cipher, cipher) { url = url || signature_cipher || cipher; - + Utils.throwIfMissing({ url }); const args = new URLSearchParams(url); @@ -107,7 +107,7 @@ class Player { if (signature_cipher || cipher) { const signature = this.#signature.decipher(url); args.get('sp') ? - url_components.searchParams.set(args.get('sp'), signature): + url_components.searchParams.set(args.get('sp'), signature) : url_components.searchParams.set('signature', signature); } @@ -138,16 +138,16 @@ class Player { get sts() { return this.#signature_timestamp; } - + static get LIBRARY_VERSION() { return 1; } - + /** * Extracts the signature timestamp from the player source code. * * @param {*} data - * @returns {number} + * @returns {number} */ #extractSigTimestamp(data) { return parseInt(Utils.getStringBetweenStrings(data, 'signatureTimestamp:', ',')); @@ -181,7 +181,7 @@ class Player { * @returns {boolean} */ isCached() { - // return false; + // Return false; return Fs.existsSync(this.#player_path); } } diff --git a/lib/core/PlaylistManager.js b/lib/core/PlaylistManager.js index 461f560f..8f1cd68b 100644 --- a/lib/core/PlaylistManager.js +++ b/lib/core/PlaylistManager.js @@ -5,20 +5,20 @@ const Utils = require('../utils/Utils'); /** @namespace */ class PlaylistManager { #actions; - + /** * @param {import('../Actions')} actions */ constructor (actions) { this.#actions = actions; } - + /** * API * * @typedef {{ success: boolean, status_code: number, playlist_id: string, data: object }} Response */ - + /** * Creates a playlist. * @@ -28,17 +28,17 @@ class PlaylistManager { */ async create(title, video_ids) { Utils.throwIfMissing({ title, video_ids }); - + const response = await this.#actions.playlist('playlist/create', { title, ids: video_ids }); - + return { success: response.success, status_code: response.status_code, playlist_id: response.data.playlistId, data: response.data - } + }; } - + /** * Deletes a given playlist. * @@ -47,27 +47,27 @@ class PlaylistManager { */ async delete(playlist_id) { Utils.throwIfMissing({ playlist_id }); - + const response = await this.#actions.playlist('playlist/delete', { playlist_id }); - + return { playlist_id, success: response.success, status_code: response.status_code, data: response.data - } + }; } - + /** * Adds videos to a given playlist. - * + * * @param {string} playlist_id * @param {Array.} video_ids * @returns {Promise.} */ async addVideos(playlist_id, video_ids) { Utils.throwIfMissing({ playlist_id, video_ids }); - + const response = await this.#actions.playlist('browse/edit_playlist', { ids: video_ids, action: 'ACTION_ADD_VIDEO', @@ -79,9 +79,9 @@ class PlaylistManager { success: response.success, status_code: response.status_code, data: response.data - } + }; } - + /** * Removes videos from a given playlist. * @@ -91,7 +91,7 @@ class PlaylistManager { */ async removeVideos(playlist_id, video_ids) { Utils.throwIfMissing({ playlist_id, video_ids }); - + const plinfo = await this.#actions.browse(`VL${playlist_id}`); const list = Utils.findNode(plinfo.data, 'contents', 'contents', 13, false); @@ -105,13 +105,13 @@ class PlaylistManager { action: 'ACTION_REMOVE_VIDEO', playlist_id }); - + return { success: response.success, status_code: response.status_code, playlist_id, data: response.data - } + }; } } diff --git a/lib/core/SessionBuilder.js b/lib/core/SessionBuilder.js index b3638cb3..8a66dea6 100644 --- a/lib/core/SessionBuilder.js +++ b/lib/core/SessionBuilder.js @@ -12,7 +12,7 @@ class SessionBuilder { /** @type {Axios} */ #axios; #config; - + #key; #client_name; #client_version; @@ -20,7 +20,7 @@ class SessionBuilder { #remote_host; #context; #player; - + /** * @param {object} config * @param {object} [config.proxy] @@ -35,27 +35,27 @@ class SessionBuilder { httpsAgent: this.#config.https_agent }); } - + async build() { const data = await Promise.all([ - this.#getYtConfig(), - this.#getPlayerId() + this.#getYtConfig(), + this.#getPlayerId() ]); - + const ytcfg = data[0][0][2]; - + this.#key = ytcfg[1]; this.#api_version = `v${ytcfg[0][0][6]}`; this.#client_name = Constants.CLIENTS.WEB.NAME; this.#client_version = ytcfg[0][0][16]; this.#remote_host = ytcfg[0][0][3]; this.#player = await new Player(data[1], this.#axios).init(); - + this.#context = this.#buildContext(); - + return this; } - + /** * Builds a valid context object. * @@ -63,12 +63,12 @@ class SessionBuilder { */ #buildContext() { const user_agent = new UserAgent({ deviceCategory: 'desktop' }); - + const id = Utils.generateRandomString(11); const timestamp = Math.floor(Date.now() / 1000); - + const visitor_data = Proto.encodeVisitorData(id, timestamp); - + const context = { client: { hl: 'en', @@ -84,29 +84,29 @@ class SessionBuilder { }, user: { lockedSafetyMode: false }, request: { useSsl: true } - } - + }; + return context; } - + /** - * Retrieves initial configuration such as keys, + * Retrieves initial configuration such as keys, * client data, etc. * * @returns {Promise.} */ async #getYtConfig() { const response = await this.axios.get(`${Constants.URLS.YT_BASE}/sw.js_data`).catch((err) => err); - - if (response instanceof Error) + + if (response instanceof Error) throw new Utils.InnertubeError('Could not retrieve configuration data', { - status_code: response?.response?.status || 0, - message: response.message + status_code: response?.response?.status || 0, + message: response.message }); - + return JSON.parse(response.data.replace(')]}\'', '')); } - + /** * Retrives the YouTube player id. * @@ -114,16 +114,16 @@ class SessionBuilder { */ async #getPlayerId() { const response = await this.axios.get(`${Constants.URLS.YT_BASE}/iframe_api`).catch((err) => err); - + if (response instanceof Error) - throw new Utils.InnertubeError('Could not retrieve js player id', { + throw new Utils.InnertubeError('Could not retrieve js player id', { status_code: response?.response?.status || 0, message: response.message }); - + return Utils.getStringBetweenStrings(response.data, 'player\\/', '\\/'); } - + /** @readonly */ get axios() { return this.#axios; @@ -133,27 +133,27 @@ class SessionBuilder { get key() { return this.#key; } - + /** @readonly */ get context() { return this.#context; } - + /** @readonly */ get api_version() { return this.#api_version; } - + /** @readonly */ get client_version() { return this.#client_version; } - + /** @readonly */ get client_name() { return this.#client_name; } - + /** @readonly */ get player() { return this.#player; diff --git a/lib/core/TabbedFeed.js b/lib/core/TabbedFeed.js index b529cf7d..4ad24445 100644 --- a/lib/core/TabbedFeed.js +++ b/lib/core/TabbedFeed.js @@ -9,7 +9,7 @@ class TabbedFeed extends Feed { */ #tabs; #actions; - + constructor (actions, data, already_parsed = false) { super(actions, data, already_parsed); this.#actions = actions; @@ -25,17 +25,17 @@ class TabbedFeed extends Feed { * @returns {Promise} */ async getTab(title) { - const tab = this.#tabs.find(tab => tab.title.toLowerCase() === title.toLowerCase()); + const tab = this.#tabs.find((tab) => tab.title.toLowerCase() === title.toLowerCase()); if (!tab) throw new InnertubeError(`Tab "${title}" not found`); - + if (tab.selected) return this; const response = await tab.endpoint.call(this.#actions); return new TabbedFeed(this.#actions, response, true); } - + get title() { - return this.page.contents_memo('Tab')?.find(tab => tab.selected)?.title.toString(); + return this.page.contents_memo('Tab')?.find((tab) => tab.selected)?.title.toString(); } } diff --git a/lib/deciphers/NToken.js b/lib/deciphers/NToken.js index 4a7634f4..7004a78f 100644 --- a/lib/deciphers/NToken.js +++ b/lib/deciphers/NToken.js @@ -15,7 +15,7 @@ const NTokenTransformOperation = exports.NTokenTransformOperation = { BASE64_DIA: 9, TRANSLATE_1: 10, TRANSLATE_2: 11 -} +}; const NTokenTransformOpType = exports.NTokenTransformOpType = { FUNC: 0, @@ -54,7 +54,7 @@ class NTokenTransforms { token_chars.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(token_chars[index]) + 64) % characters.length]); }); } - + static translate2(arr, token, characters) { let chars_length = characters.length; const token_chars = token.split(''); @@ -62,7 +62,7 @@ class NTokenTransforms { token_chars.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(token_chars[index]) + index + chars_length--) % characters.length]); }); } - + /** * Returns the requested base64 dialect, currently this is only used by 'translate2'. * @@ -73,7 +73,7 @@ class NTokenTransforms { const characters = is_reverse_base64 ? BASE64_DIALECT.REVERSE : BASE64_DIALECT.NORMAL; return characters; } - + /** * Swaps the first element with the one at the given index. * @@ -87,7 +87,7 @@ class NTokenTransforms { arr[0] = arr[index]; arr[index] = old_elem; } - + /** * Rotates elements of the array. * @@ -99,7 +99,7 @@ class NTokenTransforms { index = (index % arr.length + arr.length) % arr.length; arr.splice(-index).reverse().forEach((el) => arr.unshift(el)); } - + /** * Deletes one element at the given index. * @@ -111,14 +111,14 @@ class NTokenTransforms { index = (index % arr.length + arr.length) % arr.length; arr.splice(index, 1); } - + static reverse(arr) { arr.reverse(); } - + static push(arr, item) { if (Array.isArray(arr?.[0])) - arr.push([NTokenTransformOpType.LITERAL, item]); + arr.push([ NTokenTransformOpType.LITERAL, item ]); else arr.push(item); } @@ -126,18 +126,18 @@ class NTokenTransforms { exports.NTokenTransforms = NTokenTransforms; -const TRANSFORM_FUNCTIONS = [{ +const TRANSFORM_FUNCTIONS = [ { [NTokenTransformOperation.PUSH]: NTokenTransforms.push, [NTokenTransformOperation.SPLICE]: NTokenTransforms.splice, [NTokenTransformOperation.SWAP0_1]: NTokenTransforms.swap0, [NTokenTransformOperation.SWAP0_2]: NTokenTransforms.swap0, [NTokenTransformOperation.ROTATE_1]: NTokenTransforms.rotate, - [NTokenTransformOperation.ROTATE_2]: NTokenTransforms.rotate, + [NTokenTransformOperation.ROTATE_2]: NTokenTransforms.rotate, [NTokenTransformOperation.REVERSE_1]: NTokenTransforms.reverse, [NTokenTransformOperation.REVERSE_2]: NTokenTransforms.reverse, [NTokenTransformOperation.BASE64_DIA]: () => NTokenTransforms.getBase64Dia(false), - [NTokenTransformOperation.TRANSLATE_1]: (...args) => NTokenTransforms.translate1.apply(null, [...args, false]), - [NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2, + [NTokenTransformOperation.TRANSLATE_1]: (...args) => NTokenTransforms.translate1.apply(null, [ ...args, false ]), + [NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2 }, { [NTokenTransformOperation.PUSH]: NTokenTransforms.push, [NTokenTransformOperation.SPLICE]: NTokenTransforms.splice, @@ -148,50 +148,50 @@ const TRANSFORM_FUNCTIONS = [{ [NTokenTransformOperation.REVERSE_1]: NTokenTransforms.reverse, [NTokenTransformOperation.REVERSE_2]: NTokenTransforms.reverse, [NTokenTransformOperation.BASE64_DIA]: () => NTokenTransforms.getBase64Dia(true), - [NTokenTransformOperation.TRANSLATE_1]: (...args) => NTokenTransforms.translate1.apply(null, [...args, true]), - [NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2, -}]; + [NTokenTransformOperation.TRANSLATE_1]: (...args) => NTokenTransforms.translate1.apply(null, [ ...args, true ]), + [NTokenTransformOperation.TRANSLATE_2]: NTokenTransforms.translate2 +} ]; class NToken { constructor(transformer) { this.transformer = transformer; } - + static fromSourceCode(raw) { const transformation_data = NToken.getTransformationData(raw); - + const transformations = transformation_data.map((el) => { if (el != null && typeof el != 'number') { const is_reverse_base64 = el.includes('case 65:'); - let opcode = OP_LOOKUP[NToken.getFunc(el)?.[0]]; + const opcode = OP_LOOKUP[NToken.getFunc(el)?.[0]]; if (opcode) { el = [ NTokenTransformOpType.FUNC, opcode, 0 + is_reverse_base64 ]; } else if (el == 'b') { - el = [NTokenTransformOpType.N_ARR]; + el = [ NTokenTransformOpType.N_ARR ]; } else { - el = [NTokenTransformOpType.LITERAL, el ]; + el = [ NTokenTransformOpType.LITERAL, el ]; } } else if (el != null) { - el = [NTokenTransformOpType.LITERAL, el ]; + el = [ NTokenTransformOpType.LITERAL, el ]; } - + return el; }); - + // Fills all placeholders with the transformations array - const placeholder_indexes = [...raw.matchAll(NTOKEN_REGEX.PLACEHOLDERS)].map((item) => parseInt(item[1])); - placeholder_indexes.forEach((i) => transformations[i] = [NTokenTransformOpType.REF]); - + const placeholder_indexes = [ ...raw.matchAll(NTOKEN_REGEX.PLACEHOLDERS) ].map((item) => parseInt(item[1])); + placeholder_indexes.forEach((i) => transformations[i] = [ NTokenTransformOpType.REF ]); + // Parses and emulates calls to the functions of the transformations array - const function_calls = [...(raw.replace(/\n/g, '').match(/try\{(.*?)\}catch/s)[1]) - .matchAll(NTOKEN_REGEX.CALLS)].map((params) => [ - parseInt(params[1]), - params[2].split(',').map((param) => parseInt(param.match(/c\[(.*?)\]/)?.[1])) - ]); - + const function_calls = [ ...(raw.replace(/\n/g, '').match(/try\{(.*?)\}catch/s)[1]) + .matchAll(NTOKEN_REGEX.CALLS) ].map((params) => [ + parseInt(params[1]), + params[2].split(',').map((param) => parseInt(param.match(/c\[(.*?)\]/)?.[1])) + ]); + return new NToken([ transformations, function_calls ]); } @@ -207,26 +207,26 @@ class NToken { return transformer[0]; } } - + transform(n) { - let n_token = n.split(''); - + const n_token = n.split(''); + // We must copy since we will modify the array const transformer = this.getTransformerClone(); - + try { - transformer[1].forEach(([index, param_index]) => { + transformer[1].forEach(([ index, param_index ]) => { const base64_dia = (param_index[2] && this.evaluate(transformer[0][param_index[2]], n_token, transformer)()); this.evaluate(transformer[0][index], n_token, transformer)( param_index[0] !== undefined && - this.evaluate(transformer[0][param_index[0]], n_token, transformer), + this.evaluate(transformer[0][param_index[0]], n_token, transformer), param_index[1] !== undefined && this.evaluate(transformer[0][param_index[1]], n_token, transformer), base64_dia ); }); } catch (err) { - console.error(new Error('Could not transform n-token, download may be throttled.\nOriginal Token:'+ n + 'Error:\n' + err)); + console.error(new Error(`Could not transform n-token, download may be throttled.\nOriginal Token:${n}Error:\n${err}`)); return n; } @@ -235,8 +235,8 @@ class NToken { getTransformerClone() { return [ - [...this.transformer[0]], - [...this.transformer[1]] + [ ...this.transformer[0] ], + [ ...this.transformer[1] ] ]; } @@ -249,10 +249,10 @@ class NToken { // (8 bit N_ARG and REF) 2 bit op - 6 bit nonce // (40 bit LITERAL) 2 bit op - 6 bit nonce - 32 bit value // NTokenCall will be 8 bit for the index, 8 bit for the number of parameters, and 8 bit for each parameter - // we've got a 3 * 32 bit header to store the library version and the size of the two arrays + // We've got a 3 * 32 bit header to store the library version and the size of the two arrays let size = 4 * 3; - + for (const instruction of this.transformer[0]) { switch (instruction[0]) { case NTokenTransformOpType.FUNC: @@ -297,14 +297,14 @@ class NToken { view.setUint8(offset, instruction[1]); offset += 1; } - break; + break; case NTokenTransformOpType.N_ARR: case NTokenTransformOpType.REF: { const opcode = (instruction[0] << 6); view.setUint8(offset, opcode); offset += 1; } - break; + break; case NTokenTransformOpType.LITERAL: { const type = typeof instruction[1] === 'string' ? 1 : 0; const opcode = (instruction[0] << 6) | type; @@ -325,7 +325,7 @@ class NToken { } } } - break; + break; } } @@ -351,7 +351,7 @@ class NToken { const version = view.getUint32(offset, true); offset += 4; - + if (version !== NToken.LIBRARY_VERSION) throw new TypeError('Invalid library version'); @@ -366,26 +366,26 @@ class NToken { for (let i = 0; i < transformations_length; i++) { const opcode = view.getUint8(offset++); const op = opcode >> 6; - + switch (op) { case NTokenTransformOpType.FUNC: { const is_reverse_base64 = opcode & 0b00000001; const operation = view.getUint8(offset++); - transformations[i] = [op, operation, is_reverse_base64]; + transformations[i] = [ op, operation, is_reverse_base64 ]; } - break; + break; case NTokenTransformOpType.N_ARR: case NTokenTransformOpType.REF: - transformations[i] = [op]; + transformations[i] = [ op ]; break; case NTokenTransformOpType.LITERAL: { const type = opcode & 0b00000001; - + if (type === 0) { const literal = view.getInt32(offset, true); offset += 4; - - transformations[i] = [op, literal]; + + transformations[i] = [ op, literal ]; } else { const length = view.getUint32(offset, true); offset += 4; @@ -395,11 +395,11 @@ class NToken { for (let i = 0; i < length; i++) { literal[i] = view.getUint8(offset++); } - - transformations[i] = [op, new TextDecoder().decode(literal)]; + + transformations[i] = [ op, new TextDecoder().decode(literal) ]; } } - break; + break; default: throw new Error('Invalid opcode'); } @@ -410,17 +410,17 @@ class NToken { for (let i = 0; i < function_calls_length; i++) { const index = view.getUint8(offset++); const num_params = view.getUint8(offset++); - + const params = new Array(num_params); - + for (let j = 0; j < num_params; j++) { params[j] = view.getUint8(offset++); } - function_calls[i] = [index, params]; + function_calls[i] = [ index, params ]; } - return new NToken([transformations, function_calls]); + return new NToken([ transformations, function_calls ]); } static get LIBRARY_VERSION() { diff --git a/lib/deciphers/Signature.js b/lib/deciphers/Signature.js index 4af14a57..89b723a1 100644 --- a/lib/deciphers/Signature.js +++ b/lib/deciphers/Signature.js @@ -12,41 +12,41 @@ class Signature { constructor(action_sequence) { this.action_sequence = action_sequence; } - + static fromSourceCode(sig_decipher_sc) { let actions; - + const action_sequence = []; const functions = Signature.getFunctions(sig_decipher_sc); - + while ((actions = SIG_REGEX.ACTIONS.exec(sig_decipher_sc)) !== null) { const action = actions.groups; if (!action) continue; - + switch (action.name) { case functions[0]: - action_sequence.push([SignatureOperation.REVERSE, 0]); + action_sequence.push([ SignatureOperation.REVERSE, 0 ]); break; case functions[1]: - action_sequence.push([SignatureOperation.SPLICE, parseInt(action.param)]); + action_sequence.push([ SignatureOperation.SPLICE, parseInt(action.param) ]); break; case functions[2]: - action_sequence.push([SignatureOperation.SWAP, parseInt(action.param)]); + action_sequence.push([ SignatureOperation.SWAP, parseInt(action.param) ]); break; default: } } - + return new Signature(action_sequence); } - + decipher(url) { const args = new URLSearchParams(url); const signature = args.get('s')?.split(''); - + if (!signature) throw new TypeError('Invalid signature'); - + for (const action of this.action_sequence) { switch (action[0]) { case SignatureOperation.REVERSE: @@ -58,7 +58,7 @@ class Signature { case SignatureOperation.SWAP: { const index = action[1]; - let orig_arr = signature[0]; + const orig_arr = signature[0]; signature[0] = signature[index % signature.length]; signature[index % signature.length] = orig_arr; } @@ -67,53 +67,53 @@ class Signature { break; } } - + return signature.join(''); } - + toJSON() { - return [...this.action_sequence]; + return [ ...this.action_sequence ]; } - + toArrayBuffer() { // Array buffer encoding assumes that the index of the action is a short (16 bit unsigned) const buffer = new ArrayBuffer(4 + 4 + this.action_sequence.length * (1 + 2)); const view = new DataView(buffer); - + let offset = 0; - + view.setUint32(offset, Signature.LIBRARY_VERSION, true); offset += 4; - + view.setUint32(offset, this.action_sequence.length, true); offset += 4; - + for (let i = 0; i < this.action_sequence.length; i++) { view.setUint8(offset, this.action_sequence[i][0]); offset += 1; - + view.setUint16(offset, this.action_sequence[i][1], true); offset += 2; } - + return buffer; } - + static fromArrayBuffer(buffer) { const view = new DataView(buffer); let offset = 0; - + const version = view.getUint32(offset, true); offset += 4; - + if (version !== Signature.LIBRARY_VERSION) throw new TypeError('Invalid library version'); - + const action_sequence_length = view.getUint32(offset, true); offset += 4; - + const action_sequence = new Array(action_sequence_length); - + for (let i = 0; i < action_sequence_length; i++) { action_sequence[i] = [ view.getUint8(offset), @@ -121,21 +121,21 @@ class Signature { ]; offset += 3; } - + return new Signature(action_sequence); } - + /** * Extracts the functions used to modify the signature * and returns them in the correct order. - * + * * @param {string} sc * @returns {Array.} */ static getFunctions(sc) { let func; - let functions = []; - + const functions = []; + while ((func = SIG_REGEX.FUNCTIONS.exec(sc)) !== null) { if (func[0].includes('reverse')) { functions[0] = func[1]; @@ -145,13 +145,13 @@ class Signature { functions[2] = func[1]; } } - + return functions; } - + static get LIBRARY_VERSION() { return 1; } } - + exports.default = Signature; \ No newline at end of file diff --git a/lib/parser/contents/classes/AnalyticsMainAppKeyMetrics.js b/lib/parser/contents/classes/AnalyticsMainAppKeyMetrics.js index 4f4acb7d..747c005a 100644 --- a/lib/parser/contents/classes/AnalyticsMainAppKeyMetrics.js +++ b/lib/parser/contents/classes/AnalyticsMainAppKeyMetrics.js @@ -4,14 +4,14 @@ const DataModelSection = require('./DataModelSection'); class AnalyticsMainAppKeyMetrics { type = 'AnalyticsMainAppKeyMetrics'; - + constructor(data) { this.period = data.cardData.periodLabel; - + const metrics_data = data.cardData.sections[0].analyticsKeyMetricsData; - + this.sections = metrics_data.dataModel.sections.map((section) => new DataModelSection(section)); } } -module.exports = AnalyticsMainAppKeyMetrics; \ No newline at end of file +module.exports = AnalyticsMainAppKeyMetrics; \ No newline at end of file diff --git a/lib/parser/contents/classes/AnalyticsVideo.js b/lib/parser/contents/classes/AnalyticsVideo.js index 39cd3633..2f35810d 100644 --- a/lib/parser/contents/classes/AnalyticsVideo.js +++ b/lib/parser/contents/classes/AnalyticsVideo.js @@ -4,7 +4,7 @@ const Thumbnail = require('./Thumbnail'); class AnalyticsVideo { type = 'AnalyticsVideo'; - + constructor(data) { this.title = data.videoTitle; this.metadata = { @@ -13,7 +13,7 @@ class AnalyticsVideo { thumbnails: Thumbnail.fromResponse(data.thumbnailDetails), duration: data.formattedLength, is_short: data.isShort - } + }; } } diff --git a/lib/parser/contents/classes/AnalyticsVodCarouselCard.js b/lib/parser/contents/classes/AnalyticsVodCarouselCard.js index dab67b7f..2f86431b 100644 --- a/lib/parser/contents/classes/AnalyticsVodCarouselCard.js +++ b/lib/parser/contents/classes/AnalyticsVodCarouselCard.js @@ -4,11 +4,11 @@ const Video = require('./AnalyticsVideo'); class AnalyticsVodCarouselCard { type = 'AnalyticsVodCarouselCard'; - + constructor(data) { this.title = data.title; this.videos = data.videoCarouselData.videos.map((video) => new Video(video)); } } -module.exports = AnalyticsVodCarouselCard; \ No newline at end of file +module.exports = AnalyticsVodCarouselCard; \ No newline at end of file diff --git a/lib/parser/contents/classes/Author.js b/lib/parser/contents/classes/Author.js index e61307c9..4371b9e6 100644 --- a/lib/parser/contents/classes/Author.js +++ b/lib/parser/contents/classes/Author.js @@ -7,25 +7,25 @@ const Constants = require('../../../utils/Constants'); class Author { #nav_text; - + constructor(item, badges, thumbs) { this.#nav_text = new NavigatableText(item); - + this.id = this.#nav_text.runs?.[0].endpoint.browse?.id || this.#nav_text.endpoint?.browse?.id || 'N/A'; - + this.name = this.#nav_text.text || 'N/A'; this.thumbnails = thumbs ? Thumbnail.fromResponse(thumbs) : []; this.endpoint = this.#nav_text.runs?.[0].endpoint || this.#nav_text.endpoint; this.badges = Array.isArray(badges) ? Parser.parse(badges) : []; this.is_verified = this.badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED') || null; this.is_verified_artist = this.badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST') || null; - + this.url = this.#nav_text.runs?.[0].endpoint.browse && - `${Constants.URLS.YT_BASE}${this.#nav_text.runs[0].endpoint.browse?.base_url || '/u/' + this.#nav_text.runs[0].endpoint.browse?.id }` || - `${Constants.URLS.YT_BASE}${this.#nav_text.endpoint?.browse?.base_url || '/u/' + this.#nav_text.endpoint?.browse?.id }` || + `${Constants.URLS.YT_BASE}${this.#nav_text.runs[0].endpoint.browse?.base_url || `/u/${this.#nav_text.runs[0].endpoint.browse?.id}`}` || + `${Constants.URLS.YT_BASE}${this.#nav_text.endpoint?.browse?.base_url || `/u/${this.#nav_text.endpoint?.browse?.id}`}` || null; } diff --git a/lib/parser/contents/classes/BackstageImage.js b/lib/parser/contents/classes/BackstageImage.js index bfff689b..65866191 100644 --- a/lib/parser/contents/classes/BackstageImage.js +++ b/lib/parser/contents/classes/BackstageImage.js @@ -1,3 +1,5 @@ +'use strict'; + const Thumbnail = require('./Thumbnail'); class BackstageImage { diff --git a/lib/parser/contents/classes/BackstagePostThread.js b/lib/parser/contents/classes/BackstagePostThread.js index e0eebd53..d8dd161d 100644 --- a/lib/parser/contents/classes/BackstagePostThread.js +++ b/lib/parser/contents/classes/BackstagePostThread.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); class BackstagePostThread { diff --git a/lib/parser/contents/classes/BrowseFeedActions.js b/lib/parser/contents/classes/BrowseFeedActions.js index da0806ad..8f8cd624 100644 --- a/lib/parser/contents/classes/BrowseFeedActions.js +++ b/lib/parser/contents/classes/BrowseFeedActions.js @@ -4,7 +4,7 @@ const Parser = require('..'); class BrowseFeedActions { type = 'BrowseFeedActions'; - + constructor(data) { this.contents = Parser.parse(data.contents); } diff --git a/lib/parser/contents/classes/Button.js b/lib/parser/contents/classes/Button.js index 6ab3feba..56ca5066 100644 --- a/lib/parser/contents/classes/Button.js +++ b/lib/parser/contents/classes/Button.js @@ -5,23 +5,23 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class Button { type = 'Button'; - + constructor(data) { this.text = new Text(data.text).toString(); - + if (data.accessibility?.label) { - this.label = data.accessibility?.label - } - + this.label = data.accessibility?.label; + } + if (data.tooltip) { - this.tooltip = data.tooltip - } - - if (data.icon?.iconType) { - this.iconType = data.icon?.iconType - } - - this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint); + this.tooltip = data.tooltip; + } + + if (data.icon?.iconType) { + this.iconType = data.icon?.iconType; + } + + this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command); } } diff --git a/lib/parser/contents/classes/C4TabbedHeader.js b/lib/parser/contents/classes/C4TabbedHeader.js index ffa780ca..2c6b39d3 100644 --- a/lib/parser/contents/classes/C4TabbedHeader.js +++ b/lib/parser/contents/classes/C4TabbedHeader.js @@ -10,9 +10,10 @@ class C4TabbedHeader { constructor(data) { this.author = new Author({ - simpleText: data.title, - navigationEndpoint: data.navigationEndpoint + simpleText: data.title, + navigationEndpoint: data.navigationEndpoint }, data.badges, data.avatar); + this.banner = data.banner ? Thumbnail.fromResponse(data.banner) : []; this.tv_banner = data.tvBanner ? Thumbnail.fromResponse(data.tvBanner) : []; this.mobile_banner = data.mobileBanner ? Thumbnail.fromResponse(data.mobileBanner) : []; @@ -23,4 +24,4 @@ class C4TabbedHeader { } } -module.exports = C4TabbedHeader; +module.exports = C4TabbedHeader; \ No newline at end of file diff --git a/lib/parser/contents/classes/CallToActionButton.js b/lib/parser/contents/classes/CallToActionButton.js index 76b4a1d2..cf906ff9 100644 --- a/lib/parser/contents/classes/CallToActionButton.js +++ b/lib/parser/contents/classes/CallToActionButton.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class CallToActionButton { type = 'CallToActionButton'; - + constructor(data) { this.label = new Text(data.label); this.icon_type = data.icon.iconType; diff --git a/lib/parser/contents/classes/Card.js b/lib/parser/contents/classes/Card.js index cb69c562..1f0eea04 100644 --- a/lib/parser/contents/classes/Card.js +++ b/lib/parser/contents/classes/Card.js @@ -4,14 +4,14 @@ const Parser = require('..'); class Card { type = 'Card'; - + constructor(data) { this.teaser = Parser.parse(data.teaser); this.content = Parser.parse(data.content); - + this.card_id = data.cardId; this.feature = data.feature; - + this.cue_ranges = data.cueRanges.map((cr) => ({ start_card_active_ms: cr.startCardActiveMs, end_card_active_ms: cr.endCardActiveMs, diff --git a/lib/parser/contents/classes/CardCollection.js b/lib/parser/contents/classes/CardCollection.js index 11c7aec5..36eeaad9 100644 --- a/lib/parser/contents/classes/CardCollection.js +++ b/lib/parser/contents/classes/CardCollection.js @@ -5,7 +5,7 @@ const Text = require('./Text'); class CardCollection { type = 'CardCollection'; - + constructor(data) { this.cards = Parser.parse(data.cards); this.header = new Text(data.headerText); diff --git a/lib/parser/contents/classes/Channel.js b/lib/parser/contents/classes/Channel.js index bd90ac7f..52d4c155 100644 --- a/lib/parser/contents/classes/Channel.js +++ b/lib/parser/contents/classes/Channel.js @@ -1,3 +1,5 @@ +'use strict'; + const Author = require('./Author'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); diff --git a/lib/parser/contents/classes/ChannelFeaturedContent.js b/lib/parser/contents/classes/ChannelFeaturedContent.js index 34b80ea7..288951a8 100644 --- a/lib/parser/contents/classes/ChannelFeaturedContent.js +++ b/lib/parser/contents/classes/ChannelFeaturedContent.js @@ -5,7 +5,7 @@ const Text = require('./Text'); class ChannelFeaturedContent { type = 'ChannelFeaturedContent'; - + constructor(data) { this.title = new Text(data.title); this.items = Parser.parse(data.items); diff --git a/lib/parser/contents/classes/ChannelHeaderLinks.js b/lib/parser/contents/classes/ChannelHeaderLinks.js index 3a0b8fd6..2fb2375e 100644 --- a/lib/parser/contents/classes/ChannelHeaderLinks.js +++ b/lib/parser/contents/classes/ChannelHeaderLinks.js @@ -5,11 +5,11 @@ const Text = require('./Text'); const Thumbnail = require('./Thumbnail'); class HeaderLink { - constructor(data) { - this.endpoint = new NavigationEndpoint(data.navigationEndpoint); - this.icon = Thumbnail.fromResponse(data.icon); - this.title = new Text(data.title); - } + constructor(data) { + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + this.icon = Thumbnail.fromResponse(data.icon); + this.title = new Text(data.title); + } } class ChannelHeaderLinks { diff --git a/lib/parser/contents/classes/ChannelMetadata.js b/lib/parser/contents/classes/ChannelMetadata.js index 40d4d633..66d9b447 100644 --- a/lib/parser/contents/classes/ChannelMetadata.js +++ b/lib/parser/contents/classes/ChannelMetadata.js @@ -1,3 +1,5 @@ +'use strict'; + const Thumbnail = require('./Thumbnail'); class ChannelMetadata { diff --git a/lib/parser/contents/classes/ChannelThumbnailWithLink.js b/lib/parser/contents/classes/ChannelThumbnailWithLink.js index 784bcee3..00723de7 100644 --- a/lib/parser/contents/classes/ChannelThumbnailWithLink.js +++ b/lib/parser/contents/classes/ChannelThumbnailWithLink.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class ChannelThumbnailWithLink { type = 'ChannelThumbnailWithLink'; - + constructor(data) { this.thumbnails = Thumbnail.fromResponse(data.thumbnail); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); diff --git a/lib/parser/contents/classes/ChannelVideoPlayer.js b/lib/parser/contents/classes/ChannelVideoPlayer.js index 800e3639..de2ac864 100644 --- a/lib/parser/contents/classes/ChannelVideoPlayer.js +++ b/lib/parser/contents/classes/ChannelVideoPlayer.js @@ -1,3 +1,5 @@ +'use strict'; + const Text = require('./Text'); class ChannelVideoPlayer { diff --git a/lib/parser/contents/classes/ChildVideo.js b/lib/parser/contents/classes/ChildVideo.js index 73854838..811a3654 100644 --- a/lib/parser/contents/classes/ChildVideo.js +++ b/lib/parser/contents/classes/ChildVideo.js @@ -13,7 +13,7 @@ class ChildVideo { this.duration = { text: data.lengthText.simpleText, seconds: Utils.timeToSeconds(data.lengthText.simpleText) - } + }; this.endpoint = new NavigationEndpoint(data.navigationEndpoint); } } diff --git a/lib/parser/contents/classes/ChipCloud.js b/lib/parser/contents/classes/ChipCloud.js index 6a766ef4..37205364 100644 --- a/lib/parser/contents/classes/ChipCloud.js +++ b/lib/parser/contents/classes/ChipCloud.js @@ -4,7 +4,7 @@ const Parser = require('..'); class ChipCloud { type = 'ChipCloud'; - + constructor(data) { this.chips = Parser.parse(data.chips); this.next_button = Parser.parse(data.nextButton); diff --git a/lib/parser/contents/classes/ChipCloudChip.js b/lib/parser/contents/classes/ChipCloudChip.js index 564d80e1..6a7160aa 100644 --- a/lib/parser/contents/classes/ChipCloudChip.js +++ b/lib/parser/contents/classes/ChipCloudChip.js @@ -1,5 +1,7 @@ -const NavigationEndpoint = require('./NavigationEndpoint'); +'use strict'; + const Text = require('./Text'); +const NavigationEndpoint = require('./NavigationEndpoint'); class ChipCloudChip { type = 'ChipCloudChip'; diff --git a/lib/parser/contents/classes/CollageHeroImage.js b/lib/parser/contents/classes/CollageHeroImage.js index c76ca2c3..ee574ba5 100644 --- a/lib/parser/contents/classes/CollageHeroImage.js +++ b/lib/parser/contents/classes/CollageHeroImage.js @@ -1,3 +1,5 @@ +'use strict'; + const NavigationEndpoint = require('./NavigationEndpoint'); const Thumbnail = require('./Thumbnail'); diff --git a/lib/parser/contents/classes/Comment.js b/lib/parser/contents/classes/Comment.js new file mode 100644 index 00000000..d6ff8c4d --- /dev/null +++ b/lib/parser/contents/classes/Comment.js @@ -0,0 +1,136 @@ +'use strict'; + +const Parser = require('..'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); +const Author = require('./Author'); +const Proto = require('../../../proto'); +const { InnertubeError } = require('../../../utils/Utils'); + +class Comment { + type = 'Comment'; + + #actions; + + constructor(data) { + this.content = new Text(data.contentText); + this.published = new Text(data.publishedTimeText); + this.author_is_channel_owner = data.authorIsChannelOwner; + this.current_user_reply_thumbnail = Thumbnail.fromResponse(data.currentUserReplyThumbnail); + this.author_badge = Parser.parse(data.authorCommentBadge, 'comments'); + + this.author = new Author({ + ...data.authorText, + navigationEndpoint: data.authorEndpoint + }, this.author_badge ? [ { + metadataBadgeRenderer: this.author_badge?.orig_badge + } ] : null, data.authorThumbnail); + + this.action_menu = Parser.parse(data.actionMenu); + this.action_buttons = Parser.parse(data.actionButtons, 'comments'); + this.comment_id = data.commentId; + this.vote_status = data.voteStatus; + + this.vote_count = { + text: data.voteCount ? data.voteCount.accessibility.accessibilityData?.label.replace(/\D/g, '') : '0', + short_text: data.voteCount ? new Text(data.voteCount).toString() : '0' + }; + + this.reply_count = data.replyCount || 0; + this.is_liked = this.action_buttons.like_button.is_toggled; + this.is_disliked = this.action_buttons.dislike_button.is_toggled; + this.is_pinned = !!data.pinnedCommentBadge; + } + + /** + * API response. + * @typedef {{ success: boolean, status_code: number, data: object }} Response + */ + + /** + * Likes the comment. + * @returns {Promise.} + */ + async like() { + const button = this.action_buttons.like_button; + + if (button.is_toggled) + throw new InnertubeError('This comment is already liked', { comment_id: this.comment_id }); + + const response = await button.endpoint.callTest(this.#actions, { parse: false }); + + return response; + } + + /** + * Dislikes the comment. + * @returns {Promise.} + */ + async dislike() { + const button = this.action_buttons.dislike_button; + + if (button.is_toggled) + throw new InnertubeError('This comment is already disliked', { comment_id: this.comment_id }); + + const response = await button.endpoint.callTest(this.#actions, { parse: false }); + + return response; + } + + /** + * Creates a reply to the comment. + * @param {string} text + * @returns {Promise.} + */ + async reply(text) { + if (!this.action_buttons.reply_button) + throw new InnertubeError('Cannot reply to another reply. Try mentioning the user instead.', { comment_id: this.comment_id }); + + const button = this.action_buttons.reply_button; + const dialog_button = button.endpoint.dialog.reply_button; + + const payload = { + params: { + commentText: text + } + }; + + const response = await dialog_button.endpoint.callTest(this.#actions, payload); + + return response; + } + + /** + * Translates the comment to the given language. + * @param {string} target_language + */ + async translate(target_language) { + // Emojis must be removed otherwise InnerTube throws a 400 status code at us. + const text = this.content.toString().replace(/[^\p{L}\p{N}\p{P}\p{Z}]/gu, ''); + + const payload = { + text, + target_language, + comment_id: this.comment_id + }; + + const action = Proto.encodeCommentActionParams(22, payload); + const response = await this.#actions.execute('comment/perform_comment_action', { action, client: 'ANDROID' }); + + // TODO: maybe add these to Parser#parseResponse? + const mutations = response.data.frameworkUpdates.entityBatchUpdate.mutations; + const content = mutations[0].payload.commentEntityPayload.translatedContent.content; + + return { ...response, content }; + } + + /** + * @param {import('../../../../core/Actions')} actions + * @private + */ + setActions(actions) { + this.#actions = actions; + } +} + +module.exports = Comment; \ No newline at end of file diff --git a/lib/parser/contents/classes/CommentActionButtons.js b/lib/parser/contents/classes/CommentActionButtons.js deleted file mode 100644 index ca8bb32d..00000000 --- a/lib/parser/contents/classes/CommentActionButtons.js +++ /dev/null @@ -1,13 +0,0 @@ -const Parser = require('..'); - -class CommentActionButtons { - type = 'CommentActionButtons'; - - constructor(data) { - this.like = Parser.parse(data.likeButton); - this.reply = Parser.parse(data.replyButton); - this.dislike = Parser.parse(data.dislikeButton); - } -} - -module.exports = CommentActionButtons; \ No newline at end of file diff --git a/lib/parser/contents/classes/CommentReplyDialog.js b/lib/parser/contents/classes/CommentReplyDialog.js new file mode 100644 index 00000000..bbd75c55 --- /dev/null +++ b/lib/parser/contents/classes/CommentReplyDialog.js @@ -0,0 +1,19 @@ +'use strict'; + +const Parser = require('..'); +const Thumbnail = require('./Thumbnail'); +const Text = require('./Text'); + +class CommentReplyDialog { + type = 'CommentReplyDialog'; + + constructor(data) { + this.reply_button = Parser.parse(data.replyButton); + this.cancel_button = Parser.parse(data.cancelButton); + this.author_thumbnail = Thumbnail.fromResponse(data.authorThumbnail); + this.placeholder = new Text(data.placeholderText); + this.error_message = new Text(data.errorMessage); + } +} + +module.exports = CommentReplyDialog; \ No newline at end of file diff --git a/lib/parser/contents/classes/CommentThread.js b/lib/parser/contents/classes/CommentThread.js new file mode 100644 index 00000000..38f1a7f6 --- /dev/null +++ b/lib/parser/contents/classes/CommentThread.js @@ -0,0 +1,74 @@ +'use strict'; + +const Parser = require('..'); +const { InnertubeError } = require('../../../utils/Utils'); + +class CommentThread { + type = 'CommentThread'; + + #replies; + #actions; + #continuation; + + constructor(data) { + /** @type {import('./Comment')} */ + this.comment = Parser.parse(data.comment); + this.#replies = Parser.parse(data.replies, 'comments'); + /** @type {boolean} */ + this.is_moderated_elq_comment = data.isModeratedElqComment; + } + + /** + * Retrieves replies to this comment thread. + * @returns {Promise.} + */ + async getReplies() { + if (!this.#replies) + throw new InnertubeError('This comment has no replies.', { comment_id: this.comment.comment_id }); + + const continuation = this.#replies.contents.get({ type: 'ContinuationItem' }); + const response = await continuation.endpoint.callTest(this.#actions); + + this.replies = response.on_response_received_endpoints_memo.get('Comment').map((comment) => { + comment.setActions(this.#actions); + return comment; + }); + + this.#continuation = response.on_response_received_endpoints_memo.get('ContinuationItem')?.[0]; + + return this; + } + + /** + * Retrieves next batch of replies. + * @returns {Promise.} + */ + async getContinuation() { + if (!this.replies) + throw new InnertubeError('Continuation not available.'); + + if (!this.#continuation) + throw new InnertubeError('Continuation not found.'); + + const response = await this.#continuation.button.endpoint.callTest(this.#actions); + + this.replies = response.on_response_received_endpoints_memo.get('Comment').map((comment) => { + comment.setActions(this.#actions); + return comment; + }); + + this.#continuation = response.on_response_received_endpoints_memo.get('ContinuationItem')?.[0]; + + return this; + } + + /** + * @param {import('../../../core/Actions')} actions + * @private + */ + setActions(actions) { + this.#actions = actions; + } +} + +module.exports = CommentThread; \ No newline at end of file diff --git a/lib/parser/contents/classes/CommentsEntryPointHeader.js b/lib/parser/contents/classes/CommentsEntryPointHeader.js index afefcca8..93b30c5a 100644 --- a/lib/parser/contents/classes/CommentsEntryPointHeader.js +++ b/lib/parser/contents/classes/CommentsEntryPointHeader.js @@ -5,7 +5,7 @@ const Thumbnail = require('./Thumbnail'); class CommentsEntryPointHeader { type = 'CommentsEntryPointHeader'; - + constructor(data) { this.header = new Text(data.headerText); this.comment_count = new Text(data.commentCount); diff --git a/lib/parser/contents/classes/CommentsHeader.js b/lib/parser/contents/classes/CommentsHeader.js new file mode 100644 index 00000000..50c9eddc --- /dev/null +++ b/lib/parser/contents/classes/CommentsHeader.js @@ -0,0 +1,27 @@ +'use strict'; + +const Parser = require('..'); +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); + +class CommentsHeader { + type = 'CommentsHeader'; + + constructor(data) { + this.title = new Text(data.titleText); + this.count = new Text(data.countText); + this.comments_count = new Text(data.commentsCount); + this.create_renderer = Parser.parse(data.createRenderer, 'comments'); + this.sort_menu = Parser.parse(data.sortMenu); + + this.custom_emojis = data.customEmojis?.map((emoji) => ({ + emoji_id: emoji.emojiId, + shortcuts: emoji.shortcuts, + search_terms: emoji.searchTerms, + image: Thumbnail.fromResponse(emoji.image), + is_custom_emoji: emoji.isCustomEmoji + })) || null; + } +} + +module.exports = CommentsHeader; \ No newline at end of file diff --git a/lib/parser/contents/classes/CompactLink.js b/lib/parser/contents/classes/CompactLink.js index 5298b1dd..649ca1c3 100644 --- a/lib/parser/contents/classes/CompactLink.js +++ b/lib/parser/contents/classes/CompactLink.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class CompactLink { type = 'CompactLink'; - + constructor(data) { this.title = new Text(data.title).toString(); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); diff --git a/lib/parser/contents/classes/CompactMix.js b/lib/parser/contents/classes/CompactMix.js index cc02150e..b21a50c8 100644 --- a/lib/parser/contents/classes/CompactMix.js +++ b/lib/parser/contents/classes/CompactMix.js @@ -4,7 +4,7 @@ const Playlist = require('./Playlist'); class CompactMix extends Playlist { type = 'CompactMix'; - + constructor(data) { super(data); } diff --git a/lib/parser/contents/classes/CompactPlaylist.js b/lib/parser/contents/classes/CompactPlaylist.js index 6f2fb0e5..9f9a9649 100644 --- a/lib/parser/contents/classes/CompactPlaylist.js +++ b/lib/parser/contents/classes/CompactPlaylist.js @@ -4,7 +4,7 @@ const Playlist = require('./Playlist'); class CompactPlaylist extends Playlist { type = 'CompactPlaylist'; - + constructor(data) { super(data); } diff --git a/lib/parser/contents/classes/CompactVideo.js b/lib/parser/contents/classes/CompactVideo.js index e5e5aa02..a1abcc31 100644 --- a/lib/parser/contents/classes/CompactVideo.js +++ b/lib/parser/contents/classes/CompactVideo.js @@ -9,7 +9,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class CompactVideo { type = 'CompactVideo'; - + constructor(data) { this.id = data.videoId; this.thumbnails = Thumbnail.fromResponse(data.thumbnail) || null; @@ -20,12 +20,12 @@ class CompactVideo { this.view_count = new Text(data.viewCountText); this.short_view_count = new Text(data.shortViewCountText); this.published = new Text(data.publishedTimeText); - + this.duration = { text: new Text(data.lengthText).toString(), seconds: Utils.timeToSeconds(new Text(data.lengthText).toString()) }; - + this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.menu = Parser.parse(data.menu); diff --git a/lib/parser/contents/classes/ContinuationItem.js b/lib/parser/contents/classes/ContinuationItem.js index 9c7222de..6229e9ce 100644 --- a/lib/parser/contents/classes/ContinuationItem.js +++ b/lib/parser/contents/classes/ContinuationItem.js @@ -1,12 +1,17 @@ 'use strict'; +const Parser = require('..'); const NavigationEndpoint = require('./NavigationEndpoint'); class ContinuationItem { type = 'ContinuationItem'; - + constructor(data) { this.trigger = data.trigger; + + data.button && + (this.button = Parser.parse(data.button)); + this.endpoint = new NavigationEndpoint(data.continuationEndpoint); } } diff --git a/lib/parser/contents/classes/CtaGoToCreatorStudio.js b/lib/parser/contents/classes/CtaGoToCreatorStudio.js index 52b3690c..66395381 100644 --- a/lib/parser/contents/classes/CtaGoToCreatorStudio.js +++ b/lib/parser/contents/classes/CtaGoToCreatorStudio.js @@ -2,12 +2,12 @@ class CtaGoToCreatorStudio { type = 'CtaGoToCreatorStudio'; - + constructor(data) { this.title = data.buttonLabel; - this.use_new_specs = data.useNewSpecs; + this.use_new_specs = data.useNewSpecs; // Is this even useful? } } -module.exports = CtaGoToCreatorStudio; \ No newline at end of file +module.exports = CtaGoToCreatorStudio; \ No newline at end of file diff --git a/lib/parser/contents/classes/DataModelSection.js b/lib/parser/contents/classes/DataModelSection.js index ae6463a7..482bd207 100644 --- a/lib/parser/contents/classes/DataModelSection.js +++ b/lib/parser/contents/classes/DataModelSection.js @@ -2,21 +2,21 @@ class DataModelSection { type = 'DataModelSection'; - + constructor(data) { this.title = data.title; this.subtitle = data.subtitle; this.metric_value = data.metricValue; this.comparison_indicator = data.comparisonIndicator; - + this.series_configuration = { line_series: { lines_data: data.seriesConfiguration.lineSeries.linesData, domain_axis: data.seriesConfiguration.lineSeries.domainAxis, measure_axis: data.seriesConfiguration.lineSeries.measureAxis } - } + }; } } -module.exports = DataModelSection; \ No newline at end of file +module.exports = DataModelSection; \ No newline at end of file diff --git a/lib/parser/contents/classes/DidYouMean.js b/lib/parser/contents/classes/DidYouMean.js index b731cac6..4fee9a54 100644 --- a/lib/parser/contents/classes/DidYouMean.js +++ b/lib/parser/contents/classes/DidYouMean.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class DidYouMean { type = 'DidYouMean'; - + constructor(data) { this.corrected_query = new Text(data.correctedQuery); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); diff --git a/lib/parser/contents/classes/DownloadButton.js b/lib/parser/contents/classes/DownloadButton.js index 81ffc73f..c5c0d9ba 100644 --- a/lib/parser/contents/classes/DownloadButton.js +++ b/lib/parser/contents/classes/DownloadButton.js @@ -4,7 +4,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class DownloadButton { type = 'DownloadButton'; - + constructor(data) { this.style = data.style; this.size = data.size; diff --git a/lib/parser/contents/classes/Element.js b/lib/parser/contents/classes/Element.js index c1ddacbf..50f473c8 100644 --- a/lib/parser/contents/classes/Element.js +++ b/lib/parser/contents/classes/Element.js @@ -4,11 +4,11 @@ const Parser = require('..'); class Element { type = 'Element'; - + constructor(data) { const type = data.newElement.type.componentType; return Parser.parse(type.model); } } -module.exports = Element; \ No newline at end of file +module.exports = Element; \ No newline at end of file diff --git a/lib/parser/contents/classes/EmergencyOnebox.js b/lib/parser/contents/classes/EmergencyOnebox.js index 8f51d8e7..845e6899 100644 --- a/lib/parser/contents/classes/EmergencyOnebox.js +++ b/lib/parser/contents/classes/EmergencyOnebox.js @@ -5,7 +5,7 @@ const Parser = require('..'); class EmergencyOnebox { type = 'EmergencyOnebox'; - + constructor(data) { this.title = new Text(data.title); this.first_option = Parser.parse(data.firstOption); diff --git a/lib/parser/contents/classes/EmojiRun.js b/lib/parser/contents/classes/EmojiRun.js index 6f06e60e..cbe9d7cb 100644 --- a/lib/parser/contents/classes/EmojiRun.js +++ b/lib/parser/contents/classes/EmojiRun.js @@ -7,13 +7,13 @@ class EmojiRun { this.text = data.emoji?.emojiId || data.emoji?.shortcuts?.[0] || null; - + this.emoji = { emoji_id: data.emoji.emojiId, shortcuts: data.emoji.shortcuts, search_terms: data.emoji.searchTerms, image: Thumbnail.fromResponse(data.emoji.image) - } + }; } } diff --git a/lib/parser/contents/classes/EndScreenPlaylist.js b/lib/parser/contents/classes/EndScreenPlaylist.js index f93d2f20..764189df 100644 --- a/lib/parser/contents/classes/EndScreenPlaylist.js +++ b/lib/parser/contents/classes/EndScreenPlaylist.js @@ -6,11 +6,11 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class EndScreenPlaylist { type = 'EndScreenPlaylist'; - + constructor(data) { this.id = data.playlistId; this.title = new Text(data.title); - this.author = new Text(data.longBylineText); + this.author = new Text(data.longBylineText); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); this.video_count = new Text(data.videoCountText); diff --git a/lib/parser/contents/classes/EndScreenVideo.js b/lib/parser/contents/classes/EndScreenVideo.js index 6cc1cd56..70cc2aa8 100644 --- a/lib/parser/contents/classes/EndScreenVideo.js +++ b/lib/parser/contents/classes/EndScreenVideo.js @@ -19,9 +19,9 @@ class EndScreenVideo { this.short_view_count_text = new Text(data.shortViewCountText); this.badges = Parser.parse(data.badges); this.duration = { - text: new Text(data.lengthText).toString(), + text: new Text(data.lengthText).toString(), seconds: data.lengthInSeconds - } + }; } } diff --git a/lib/parser/contents/classes/Endscreen.js b/lib/parser/contents/classes/Endscreen.js index b30662de..3b41ff0f 100644 --- a/lib/parser/contents/classes/Endscreen.js +++ b/lib/parser/contents/classes/Endscreen.js @@ -4,7 +4,7 @@ const Parser = require('..'); class Endscreen { type = 'Endscreen'; - + constructor(data) { this.elements = Parser.parse(data.elements); this.start_ms = data.startMs; diff --git a/lib/parser/contents/classes/EndscreenElement.js b/lib/parser/contents/classes/EndscreenElement.js index 2f41ce83..9d3a884a 100644 --- a/lib/parser/contents/classes/EndscreenElement.js +++ b/lib/parser/contents/classes/EndscreenElement.js @@ -7,36 +7,37 @@ const Text = require('./Text'); class EndscreenElement { type = 'EndscreenElement'; - + constructor(data) { this.style = data.style; - - if (data.image) { - this.image = Thumbnail.fromResponse(data.image) - } - - if (data.icon) { - this.icon = Thumbnail.fromResponse(data.icon) - } - - if (data.metadata) { - this.metadata = new Text(data.metadata) - } - - if (data.callToAction) { - this.call_to_action = new Text(data.callToAction) - } - - if (data.hovercardButton) { - this.hovercard_button = Parser.parse(data.hovercardButton) - } - - if (data.isSubscribe) { - this.is_subscribe = data.isSubscribe - } - + this.title = new Text(data.title); this.endpoint = new NavigationEndpoint(data.endpoint); + + if (data.image) { + this.image = Thumbnail.fromResponse(data.image); + } + + if (data.icon) { + this.icon = Thumbnail.fromResponse(data.icon); + } + + if (data.metadata) { + this.metadata = new Text(data.metadata); + } + + if (data.callToAction) { + this.call_to_action = new Text(data.callToAction); + } + + if (data.hovercardButton) { + this.hovercard_button = Parser.parse(data.hovercardButton); + } + + if (data.isSubscribe) { + this.is_subscribe = data.isSubscribe; + } + this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays); this.left = data.left; this.width = data.width; diff --git a/lib/parser/contents/classes/ExpandableTab.js b/lib/parser/contents/classes/ExpandableTab.js index 35dc62ab..d49ceeb5 100644 --- a/lib/parser/contents/classes/ExpandableTab.js +++ b/lib/parser/contents/classes/ExpandableTab.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); const NavigationEndpoint = require('./NavigationEndpoint'); @@ -7,7 +9,7 @@ class ExpandableTab { constructor(data) { this.title = data.title; this.endpoint = new NavigationEndpoint(data.endpoint); - this.selected = data.selected; // if this.selected then we may have content else we do not + this.selected = data.selected; // If this.selected then we may have content else we do not this.content = data.content ? Parser.parse(data.content) : null; } } diff --git a/lib/parser/contents/classes/ExpandedShelfContents.js b/lib/parser/contents/classes/ExpandedShelfContents.js index fa3be02b..55ad86fb 100644 --- a/lib/parser/contents/classes/ExpandedShelfContents.js +++ b/lib/parser/contents/classes/ExpandedShelfContents.js @@ -4,11 +4,11 @@ const Parser = require('..'); class ExpandedShelfContents { type = 'ExpandedShelfContents'; - + constructor(data) { this.items = Parser.parse(data.items); } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/FeedFilterChipBar.js b/lib/parser/contents/classes/FeedFilterChipBar.js index 8581d554..39b1520e 100644 --- a/lib/parser/contents/classes/FeedFilterChipBar.js +++ b/lib/parser/contents/classes/FeedFilterChipBar.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); class FeedFilterChipBar { diff --git a/lib/parser/contents/classes/Format.js b/lib/parser/contents/classes/Format.js index c74980b5..5ccded2a 100644 --- a/lib/parser/contents/classes/Format.js +++ b/lib/parser/contents/classes/Format.js @@ -32,11 +32,11 @@ class Format { this.has_audio = !!data.audioBitrate || !!data.audioQuality; this.has_video = !!data.qualityLabel; } - + /** * Decipher the streaming url of the format. * - * @param {import('../../../core/Player')} player + * @param {import('../../../core/Player')} player * @returns {string} Deciphered URL for downloading */ decipher(player) { diff --git a/lib/parser/contents/classes/Grid.js b/lib/parser/contents/classes/Grid.js index 534cbfbf..1ea2a64d 100644 --- a/lib/parser/contents/classes/Grid.js +++ b/lib/parser/contents/classes/Grid.js @@ -4,14 +4,14 @@ const Parser = require('..'); class Grid { type = 'Grid'; - + constructor(data) { this.items = Parser.parse(data.items); this.is_collapsible = data.isCollapsible; this.visible_row_count = data.visibleRowCount; this.target_id = data.targetId; } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/GridPlaylist.js b/lib/parser/contents/classes/GridPlaylist.js index bdf472c6..126b7bb2 100644 --- a/lib/parser/contents/classes/GridPlaylist.js +++ b/lib/parser/contents/classes/GridPlaylist.js @@ -9,15 +9,15 @@ const NavigatableText = require('./NavigatableText'); class GridPlaylist { type = 'GridPlaylist'; - + constructor(data) { this.id = data.playlistId; this.title = new Text(data.title); - + if (data.shortBylineText) { - this.author = new PlaylistAuthor(data.shortBylineText, data.ownerBadges) - } - + this.author = new PlaylistAuthor(data.shortBylineText, data.ownerBadges); + } + this.badges = Parser.parse(data.ownerBadges); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.view_playlist = new NavigatableText(data.viewPlaylistText); diff --git a/lib/parser/contents/classes/GridVideo.js b/lib/parser/contents/classes/GridVideo.js index e93643e6..76a0d28d 100644 --- a/lib/parser/contents/classes/GridVideo.js +++ b/lib/parser/contents/classes/GridVideo.js @@ -8,10 +8,10 @@ const Author = require('./Author'); class GridVideo { type = 'GridVideo'; - + constructor(data) { - const length_alt = data.thumbnailOverlays.find(overlay => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; - + const length_alt = data.thumbnailOverlays.find((overlay) => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; + this.id = data.videoId; this.title = new Text(data.title); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); diff --git a/lib/parser/contents/classes/HorizontalCardList.js b/lib/parser/contents/classes/HorizontalCardList.js index 9ba90b6b..5ccf03bf 100644 --- a/lib/parser/contents/classes/HorizontalCardList.js +++ b/lib/parser/contents/classes/HorizontalCardList.js @@ -4,7 +4,7 @@ const Parser = require('..'); class HorizontalCardList { type = 'HorizontalCardList'; - + constructor(data) { this.cards = Parser.parse(data.cards); this.header = Parser.parse(data.header); diff --git a/lib/parser/contents/classes/HorizontalList.js b/lib/parser/contents/classes/HorizontalList.js index 3c5a812b..8df3af15 100644 --- a/lib/parser/contents/classes/HorizontalList.js +++ b/lib/parser/contents/classes/HorizontalList.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); class HorizontalList { @@ -7,7 +9,7 @@ class HorizontalList { this.visible_item_count = data.visibleItemCount; this.items = Parser.parse(data.items); } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/ItemSection.js b/lib/parser/contents/classes/ItemSection.js index 541d0a0e..f84d2037 100644 --- a/lib/parser/contents/classes/ItemSection.js +++ b/lib/parser/contents/classes/ItemSection.js @@ -4,12 +4,12 @@ const Parser = require('..'); class ItemSection { type = 'ItemSection'; - + constructor(data) { this.header = Parser.parse(data.header); this.contents = Parser.parse(data.contents); - - if (data.targetId || data.sectionIdentifier) { + + if (data.targetId || data.sectionIdentifier) { this.target_id = data?.target_id || data?.sectionIdentifier; } } diff --git a/lib/parser/contents/classes/LikeButton.js b/lib/parser/contents/classes/LikeButton.js index be6979af..0a6bfaad 100644 --- a/lib/parser/contents/classes/LikeButton.js +++ b/lib/parser/contents/classes/LikeButton.js @@ -4,18 +4,18 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class LikeButton { type = 'LikeButton'; - + constructor(data) { this.target = { video_id: data.target.videoId }; - + this.like_status = data.likeStatus; this.likes_allowed = data.likesAllowed; - + if (data.serviceEndpoints) { - this.endpoints = data.serviceEndpoints?.map((endpoint) => new NavigationEndpoint(endpoint)) - } + this.endpoints = data.serviceEndpoints?.map((endpoint) => new NavigationEndpoint(endpoint)); + } } } diff --git a/lib/parser/contents/classes/LiveChat.js b/lib/parser/contents/classes/LiveChat.js index 2f8c0a6c..9ccb64aa 100644 --- a/lib/parser/contents/classes/LiveChat.js +++ b/lib/parser/contents/classes/LiveChat.js @@ -5,13 +5,13 @@ const Text = require('./Text'); class LiveChat { type = 'LiveChat'; - + constructor(data) { this.header = Parser.parse(data.header); - + this.initial_display_state = data.initialDisplayState; this.continuation = data.continuations[0]?.reloadContinuationData?.continuation; - + this.client_messages = { reconnect_message: new Text(data.clientMessages.reconnectMessage), unable_to_reconnect_message: new Text(data.clientMessages.unableToReconnectMessage), @@ -19,7 +19,7 @@ class LiveChat { reconnected_message: new Text(data.clientMessages.reconnectedMessage), generic_error: new Text(data.clientMessages.genericError) }; - + this.is_replay = data.isReplay || false; } } diff --git a/lib/parser/contents/classes/LiveChatAuthorBadge.js b/lib/parser/contents/classes/LiveChatAuthorBadge.js index 952a8b22..d323f5f1 100644 --- a/lib/parser/contents/classes/LiveChatAuthorBadge.js +++ b/lib/parser/contents/classes/LiveChatAuthorBadge.js @@ -6,7 +6,7 @@ const Thumbnail = require('./Thumbnail'); class LiveChatAuthorBadge extends MetadataBadge { constructor(data) { super(data); - + this.custom_thumbnail = data.customThumbnail ? Thumbnail.fromResponse(data.customThumbnail) : null; } } diff --git a/lib/parser/contents/classes/LiveChatHeader.js b/lib/parser/contents/classes/LiveChatHeader.js index 8060705f..39a5b858 100644 --- a/lib/parser/contents/classes/LiveChatHeader.js +++ b/lib/parser/contents/classes/LiveChatHeader.js @@ -4,7 +4,7 @@ const Parser = require('..'); class LiveChatHeader { type = 'LiveChatHeader'; - + constructor(data) { this.overflow_menu = Parser.parse(data.overflowMenu); this.collapse_button = Parser.parse(data.collapseButton); diff --git a/lib/parser/contents/classes/LiveChatItemList.js b/lib/parser/contents/classes/LiveChatItemList.js index 6bc41b4e..409787c9 100644 --- a/lib/parser/contents/classes/LiveChatItemList.js +++ b/lib/parser/contents/classes/LiveChatItemList.js @@ -4,7 +4,7 @@ const Parser = require('..'); class LiveChatItemList { type = 'LiveChatItemList'; - + constructor(data) { this.max_items_to_display = data.maxItemsToDisplay; this.more_comments_below_button = Parser.parse(data.moreCommentsBelowButton); diff --git a/lib/parser/contents/classes/LiveChatMessageInput.js b/lib/parser/contents/classes/LiveChatMessageInput.js index 67afd9af..82b594c1 100644 --- a/lib/parser/contents/classes/LiveChatMessageInput.js +++ b/lib/parser/contents/classes/LiveChatMessageInput.js @@ -1,6 +1,6 @@ 'use strict'; -const Text = require('./Text') +const Text = require('./Text'); const Parser = require('..'); const Thumbnail = require('./Thumbnail'); diff --git a/lib/parser/contents/classes/LiveChatParticipant.js b/lib/parser/contents/classes/LiveChatParticipant.js index dd0f393a..1cf0c620 100644 --- a/lib/parser/contents/classes/LiveChatParticipant.js +++ b/lib/parser/contents/classes/LiveChatParticipant.js @@ -6,7 +6,7 @@ const Thumbnail = require('./Thumbnail'); class LiveChatParticipant { type = 'LiveChatParticipant'; - + constructor(data) { this.name = new Text(data.authorName); this.photo = Thumbnail.fromResponse(data.authorPhoto); diff --git a/lib/parser/contents/classes/LiveChatParticipantsList.js b/lib/parser/contents/classes/LiveChatParticipantsList.js index bcee2e73..02651677 100644 --- a/lib/parser/contents/classes/LiveChatParticipantsList.js +++ b/lib/parser/contents/classes/LiveChatParticipantsList.js @@ -5,7 +5,7 @@ const Text = require('./Text'); class LiveChatParticipantsList { type = 'LiveChatParticipantsList'; - + constructor(data) { this.title = new Text(data.title); this.participants = Parser.parse(data.participants); diff --git a/lib/parser/contents/classes/Menu.js b/lib/parser/contents/classes/Menu.js index 5f59a515..6081c61f 100644 --- a/lib/parser/contents/classes/Menu.js +++ b/lib/parser/contents/classes/Menu.js @@ -4,13 +4,13 @@ const Parser = require('..'); class Menu { type = 'Menu'; - + constructor(data) { this.items = Parser.parse(data.items) || []; this.top_level_buttons = Parser.parse(data.topLevelButtons) || []; this.label = data.accessibility?.accessibilityData?.label || null; } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/MenuNavigationItem.js b/lib/parser/contents/classes/MenuNavigationItem.js index 0be59e88..70e08580 100644 --- a/lib/parser/contents/classes/MenuNavigationItem.js +++ b/lib/parser/contents/classes/MenuNavigationItem.js @@ -4,7 +4,7 @@ const Button = require('./Button'); class MenuNavigationItem extends Button { type = 'MenuNavigationItem'; - + constructor(data) { super(data); } diff --git a/lib/parser/contents/classes/MenuServiceItem.js b/lib/parser/contents/classes/MenuServiceItem.js index 82069560..05c43b33 100644 --- a/lib/parser/contents/classes/MenuServiceItem.js +++ b/lib/parser/contents/classes/MenuServiceItem.js @@ -4,7 +4,7 @@ const Button = require('./Button'); class MenuServiceItem extends Button { type = 'MenuServiceItem'; - + constructor(data) { super(data); } diff --git a/lib/parser/contents/classes/MenuServiceItemDownload.js b/lib/parser/contents/classes/MenuServiceItemDownload.js index 17d57f68..ac66707e 100644 --- a/lib/parser/contents/classes/MenuServiceItemDownload.js +++ b/lib/parser/contents/classes/MenuServiceItemDownload.js @@ -4,7 +4,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class MenuServiceItemDownload { type = 'MenuServiceItemDownload'; - + constructor(data) { this.has_separator = data.hasSeparator; this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint); diff --git a/lib/parser/contents/classes/MerchandiseItem.js b/lib/parser/contents/classes/MerchandiseItem.js index 836af2a6..7f7f9a46 100644 --- a/lib/parser/contents/classes/MerchandiseItem.js +++ b/lib/parser/contents/classes/MerchandiseItem.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class MerchandiseItem { type = 'MerchandiseItem'; - + constructor(data) { this.title = data.title; this.description = data.description; diff --git a/lib/parser/contents/classes/MerchandiseShelf.js b/lib/parser/contents/classes/MerchandiseShelf.js index db156dcc..dcb6e316 100644 --- a/lib/parser/contents/classes/MerchandiseShelf.js +++ b/lib/parser/contents/classes/MerchandiseShelf.js @@ -4,13 +4,13 @@ const Parser = require('..'); class MerchandiseShelf { type = 'MerchandiseShelf'; - + constructor(data) { this.title = data.title; this.menu = Parser.parse(data.actionButton); this.items = Parser.parse(data.items); } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/Message.js b/lib/parser/contents/classes/Message.js index 72f56126..f29f3dcb 100644 --- a/lib/parser/contents/classes/Message.js +++ b/lib/parser/contents/classes/Message.js @@ -3,8 +3,8 @@ const Text = require('./Text'); class Message { - type = 'Message' - + type = 'Message'; + constructor(data) { this.text = new Text(data.text).toString(); } diff --git a/lib/parser/contents/classes/MetadataBadge.js b/lib/parser/contents/classes/MetadataBadge.js index 2069342b..f157d611 100644 --- a/lib/parser/contents/classes/MetadataBadge.js +++ b/lib/parser/contents/classes/MetadataBadge.js @@ -2,9 +2,13 @@ class MetadataBadge { constructor(data) { - this.icon_type = data.icon?.iconType || null; - this.style = data.style || null; - this.tooltip = data.tooltip || null; + data.icon && + (this.icon_type = data.icon.iconType); + + data.style && + (this.style = data.style); + + this.tooltip = data.tooltip || data.iconTooltip || null; } } diff --git a/lib/parser/contents/classes/MetadataRow.js b/lib/parser/contents/classes/MetadataRow.js index e8288bfd..c74b2456 100644 --- a/lib/parser/contents/classes/MetadataRow.js +++ b/lib/parser/contents/classes/MetadataRow.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class MetadataRow { type = 'MetadataRow'; - + constructor(data) { this.title = new Text(data.title); this.contents = data.contents.map((content) => new Text(content)); diff --git a/lib/parser/contents/classes/MetadataRowContainer.js b/lib/parser/contents/classes/MetadataRowContainer.js index 15b653fc..1cf4bfe5 100644 --- a/lib/parser/contents/classes/MetadataRowContainer.js +++ b/lib/parser/contents/classes/MetadataRowContainer.js @@ -4,7 +4,7 @@ const Parser = require('..'); class MetadataRowContainer { type = 'MetadataRowContainer'; - + constructor(data) { this.rows = Parser.parse(data.rows); this.collapsed_item_count = data.collapsedItemCount; diff --git a/lib/parser/contents/classes/MetadataRowHeader.js b/lib/parser/contents/classes/MetadataRowHeader.js index 3fc344c5..0525595b 100644 --- a/lib/parser/contents/classes/MetadataRowHeader.js +++ b/lib/parser/contents/classes/MetadataRowHeader.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class MetadataRowHeader { type = 'MetadataRowHeader'; - + constructor(data) { this.content = new Text(data.content); this.has_divider_line = data.hasDividerLine; diff --git a/lib/parser/contents/classes/MicroformatData.js b/lib/parser/contents/classes/MicroformatData.js index 1ef6468f..5f93453d 100644 --- a/lib/parser/contents/classes/MicroformatData.js +++ b/lib/parser/contents/classes/MicroformatData.js @@ -1,3 +1,5 @@ +'use strict'; + const Thumbnail = require('./Thumbnail'); class MicroformatData { diff --git a/lib/parser/contents/classes/Mix.js b/lib/parser/contents/classes/Mix.js index 0f396615..fd121d4a 100644 --- a/lib/parser/contents/classes/Mix.js +++ b/lib/parser/contents/classes/Mix.js @@ -4,7 +4,7 @@ const Playlist = require('./Playlist'); class Mix extends Playlist { type = 'Mix'; - + constructor(data) { super(data); } diff --git a/lib/parser/contents/classes/Movie.js b/lib/parser/contents/classes/Movie.js index fe313706..0f8310b8 100644 --- a/lib/parser/contents/classes/Movie.js +++ b/lib/parser/contents/classes/Movie.js @@ -9,12 +9,12 @@ const Text = require('./Text'); class Movie { type = 'Movie'; - + constructor(data) { const overlay_time_status = data.thumbnailOverlays .find((overlay) => overlay.thumbnailOverlayTimeStatusRenderer) ?.thumbnailOverlayTimeStatusRenderer.text || 'N/A'; - + this.id = data.videoId; this.title = new Text(data.title); this.description_snippet = data.descriptionSnippet ? new Text(data.descriptionSnippet, '') : null; @@ -22,12 +22,12 @@ class Movie { this.thumbnails = Thumbnail.fromResponse(data.thumbnail); this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays); this.author = new Author(data.longBylineText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail); - + this.duration = { - text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text, + text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text, seconds: Utils.timeToSeconds(data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text) }; - + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.badges = Parser.parse(data.badges); this.use_vertical_poster = data.useVerticalPoster; diff --git a/lib/parser/contents/classes/MovingThumbnail.js b/lib/parser/contents/classes/MovingThumbnail.js index 22834972..366a7827 100644 --- a/lib/parser/contents/classes/MovingThumbnail.js +++ b/lib/parser/contents/classes/MovingThumbnail.js @@ -4,7 +4,7 @@ const Thumbnail = require('./Thumbnail'); class MovingThumbnail { type = 'MovingThumbnail'; - + constructor(data) { return data.movingThumbnailDetails?.thumbnails.map((thumbnail) => new Thumbnail(thumbnail)).sort((a, b) => b.width - a.width); } diff --git a/lib/parser/contents/classes/MusicCarouselShelf.js b/lib/parser/contents/classes/MusicCarouselShelf.js index 89e05376..55b1654c 100644 --- a/lib/parser/contents/classes/MusicCarouselShelf.js +++ b/lib/parser/contents/classes/MusicCarouselShelf.js @@ -4,13 +4,13 @@ const Parser = require('..'); class MusicCarouselShelf { type = 'MusicCarouselShelf'; - + constructor(data) { this.header = Parser.parse(data.header); this.contents = Parser.parse(data.contents); - + if (data.numItemsPerColumn) { - this.num_items_per_column = data.numItemsPerColumn + this.num_items_per_column = data.numItemsPerColumn; } } } diff --git a/lib/parser/contents/classes/MusicCarouselShelfBasicHeader.js b/lib/parser/contents/classes/MusicCarouselShelfBasicHeader.js index 9a06fb68..f426d4f8 100644 --- a/lib/parser/contents/classes/MusicCarouselShelfBasicHeader.js +++ b/lib/parser/contents/classes/MusicCarouselShelfBasicHeader.js @@ -5,19 +5,19 @@ const Thumbnail = require('./Thumbnail'); class MusicCarouselShelfBasicHeader { type = 'MusicCarouselShelfBasicHeader'; - + constructor(data) { if (data.strapline) { - this.strapline = new Text(data.strapline).toString() + this.strapline = new Text(data.strapline).toString(); } - + this.title = new Text(data.title).toString(); - - // this.label = data.accessibilityData.accessibilityData.label; + + // This.label = data.accessibilityData.accessibilityData.label; // ^^ redundant? - + if (data.thumbnail) { - this.thumbnail = Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer.thumbnail) + this.thumbnail = Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer.thumbnail); } } } diff --git a/lib/parser/contents/classes/MusicDescriptionShelf.js b/lib/parser/contents/classes/MusicDescriptionShelf.js index be6958bb..2b3c04f6 100644 --- a/lib/parser/contents/classes/MusicDescriptionShelf.js +++ b/lib/parser/contents/classes/MusicDescriptionShelf.js @@ -4,18 +4,18 @@ const Text = require('./Text'); class MusicDescriptionShelf { type = 'MusicDescriptionShelf'; - + constructor(data) { this.description = new Text(data.description); - + if (this.max_collapsed_lines) { - this.max_collapsed_lines = data.maxCollapsedLines + this.max_collapsed_lines = data.maxCollapsedLines; } - + if (this.max_expanded_lines) { - this.max_expanded_lines = data.maxExpandedLines - } - + this.max_expanded_lines = data.maxExpandedLines; + } + this.footer = new Text(data.footer); } } diff --git a/lib/parser/contents/classes/MusicDetailHeader.js b/lib/parser/contents/classes/MusicDetailHeader.js index 46f5967b..72aeb4ee 100644 --- a/lib/parser/contents/classes/MusicDetailHeader.js +++ b/lib/parser/contents/classes/MusicDetailHeader.js @@ -6,26 +6,28 @@ const Parser = require('..'); class MusicDetailHeader { type = 'MusicDetailHeader'; - + constructor(data) { this.title = new Text(data.title); this.description = new Text(data.description); this.subtitle = new Text(data.subtitle); this.second_subtitle = new Text(data.secondSubtitle); - this.year = this.subtitle.runs.find((run) => /^[12][0-9]{3}$/.test(run.text)).text; + this.year = this.subtitle.runs.find((run) => (/^[12][0-9]{3}$/).test(run.text)).text; this.song_count = this.second_subtitle.runs[0].text; this.total_duration = this.second_subtitle.runs[2].text; this.thumbnails = Thumbnail.fromResponse(data.thumbnail.croppedSquareThumbnailRenderer.thumbnail); this.badges = Parser.parse(data.subtitleBadges); - + const author = this.subtitle.runs.find((run) => run.endpoint.browse?.id.startsWith('UC')); - author && (this.author = { - name: author.text, - channel_id: author.endpoint.browse.id, - endpoint: author.endpoint - }); - + if (author) { + this.author = { + name: author.text, + channel_id: author.endpoint.browse.id, + endpoint: author.endpoint + }; + } + this.menu = Parser.parse(data.menu); } } diff --git a/lib/parser/contents/classes/MusicHeader.js b/lib/parser/contents/classes/MusicHeader.js index 62d5a005..d16037a2 100644 --- a/lib/parser/contents/classes/MusicHeader.js +++ b/lib/parser/contents/classes/MusicHeader.js @@ -4,7 +4,7 @@ const Parser = require('..'); class MusicHeader { type = 'MusicHeader'; - + constructor(data) { this.header = Parser.parse(data.header); } diff --git a/lib/parser/contents/classes/MusicImmersiveHeader.js b/lib/parser/contents/classes/MusicImmersiveHeader.js index ae7f972e..f9caaf4a 100644 --- a/lib/parser/contents/classes/MusicImmersiveHeader.js +++ b/lib/parser/contents/classes/MusicImmersiveHeader.js @@ -5,13 +5,13 @@ const Parser = require('..'); class MusicImmersiveHeader { type = 'MusicImmersiveHeader'; - + constructor(data) { this.title = new Text(data.title); this.description = new Text(data.description); this.thumbnails = Parser.parse(data.thumbnail); - - /** + + /** Not useful for now. this.menu = Parser.parse(data.menu); this.play_button = Parser.parse(data.playButton); diff --git a/lib/parser/contents/classes/MusicInlineBadge.js b/lib/parser/contents/classes/MusicInlineBadge.js index a51e12e7..ad66a852 100644 --- a/lib/parser/contents/classes/MusicInlineBadge.js +++ b/lib/parser/contents/classes/MusicInlineBadge.js @@ -1,6 +1,8 @@ +'use strict'; + class MusicInlineBadge { type = 'MusicInlineBadge'; - + constructor(data) { this.icon_type = data.icon.iconType; this.label = data.accessibilityData.accessibilityData.label; diff --git a/lib/parser/contents/classes/MusicItemThumbnailOverlay.js b/lib/parser/contents/classes/MusicItemThumbnailOverlay.js index 80507798..85aa8556 100644 --- a/lib/parser/contents/classes/MusicItemThumbnailOverlay.js +++ b/lib/parser/contents/classes/MusicItemThumbnailOverlay.js @@ -4,7 +4,7 @@ const Parser = require('..'); class MusicItemThumbnailOverlay { type = 'MusicItemThumbnailOverlay'; - + constructor(data) { this.content = Parser.parse(data.content); this.content_position = data.contentPosition; diff --git a/lib/parser/contents/classes/MusicNavigationButton.js b/lib/parser/contents/classes/MusicNavigationButton.js index 89b77567..350642e0 100644 --- a/lib/parser/contents/classes/MusicNavigationButton.js +++ b/lib/parser/contents/classes/MusicNavigationButton.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class MusicNavigationButton { type = 'MusicNavigationButton'; - + constructor(data) { this.button_text = new Text(data.buttonText).toString(); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); diff --git a/lib/parser/contents/classes/MusicPlayButton.js b/lib/parser/contents/classes/MusicPlayButton.js index 62c8eb82..6ab7a99d 100644 --- a/lib/parser/contents/classes/MusicPlayButton.js +++ b/lib/parser/contents/classes/MusicPlayButton.js @@ -4,20 +4,20 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class MusicPlayButton { type = 'MusicPlayButton'; - + constructor(data) { this.endpoint = new NavigationEndpoint(data.playNavigationEndpoint); this.play_icon_type = data.playIcon.iconType; this.pause_icon_type = data.pauseIcon.iconType; - + if (data.accessibilityPlayData) { - this.play_label = data.accessibilityPlayData.accessibilityData.label - } - + this.play_label = data.accessibilityPlayData.accessibilityData.label; + } + if (data.accessibilityPlayData) { - this.pause_label = data.accessibilityPauseData?.accessibilityData.label - } - + this.pause_label = data.accessibilityPauseData?.accessibilityData.label; + } + this.icon_color = data.iconColor; } } diff --git a/lib/parser/contents/classes/MusicPlaylistShelf.js b/lib/parser/contents/classes/MusicPlaylistShelf.js index 0af9f516..95f41f03 100644 --- a/lib/parser/contents/classes/MusicPlaylistShelf.js +++ b/lib/parser/contents/classes/MusicPlaylistShelf.js @@ -4,16 +4,16 @@ const Parser = require('..'); class MusicPlaylistShelf { type = 'MusicPlaylistShelf'; - + #continuations; - + constructor(data) { this.playlist_id = data.playlistId; this.contents = Parser.parse(data.contents); this.collapsed_item_count = data.collapsedItemCount; this.#continuations = data.continuations; } - + get continuation() { return this.#continuations?.[0]?.nextContinuationData; } diff --git a/lib/parser/contents/classes/MusicQueue.js b/lib/parser/contents/classes/MusicQueue.js index 62bf5196..005c3ecf 100644 --- a/lib/parser/contents/classes/MusicQueue.js +++ b/lib/parser/contents/classes/MusicQueue.js @@ -4,7 +4,7 @@ const Parser = require('..'); class MusicQueue { type = 'MusicQueue'; - + constructor(data) { this.content = Parser.parse(data.content); } diff --git a/lib/parser/contents/classes/MusicResponsiveListItem.js b/lib/parser/contents/classes/MusicResponsiveListItem.js index 2d2b9248..ec55c019 100644 --- a/lib/parser/contents/classes/MusicResponsiveListItem.js +++ b/lib/parser/contents/classes/MusicResponsiveListItem.js @@ -10,21 +10,21 @@ class MusicResponsiveListItem { #flex_columns; #fixed_columns; #playlist_item_data; - + constructor(data) { this.type = null; - + this.#flex_columns = Parser.parse(data.flexColumns); this.#fixed_columns = Parser.parse(data.fixedColumns); - + this.#playlist_item_data = { video_id: data?.playlistItemData?.videoId || null, playlist_set_video_id: data?.playlistItemData?.playlistSetVideoId || null }; - + this.endpoint = data.navigationEndpoint && new NavigationEndpoint(data.navigationEndpoint) || null; - + switch (this.endpoint?.browse?.page_type) { case 'MUSIC_PAGE_TYPE_ALBUM': this.type = 'album'; @@ -43,22 +43,22 @@ class MusicResponsiveListItem { this.#parseVideoOrSong(); break; } - + if (data.index) { - this.index = new Text(data.index) - } - + this.index = new Text(data.index); + } + this.thumbnails = data.thumbnail ? Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer.thumbnail) : []; this.badges = Parser.parse(data.badges) || []; - + this.menu = Parser.parse(data.menu); this.overlay = Parser.parse(data.overlay); } - + #parseVideoOrSong() { const is_video = this.#flex_columns[1].title.runs ?.some((run) => run.text.match(/(.*?) views/)); - + if (is_video) { this.type = 'video'; this.#parseVideo(); @@ -67,31 +67,31 @@ class MusicResponsiveListItem { this.#parseSong(); } } - + #parseSong() { this.id = this.#playlist_item_data.video_id || this.endpoint.watch.video_id; this.title = this.#flex_columns[0].title.toString(); - + const duration_text = this.#flex_columns[1].title.runs?.find( - (run) => /^\d+$/.test(run.text.replace(/:/g, '')))?.text || + (run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.#fixed_columns?.[0]?.title?.text; - + duration_text && (this.duration = { text: duration_text, seconds: Utils.timeToSeconds(duration_text) }); - + const album = this.#flex_columns[1].title.runs?.find((run) => run.endpoint.browse?.id.startsWith('MPR')); - + if (album) { this.album = { id: album.endpoint.browse.id, name: album.text, endpoint: album.endpoint - } + }; } - + const artists = this.#flex_columns[1].title.runs?.filter((run) => run.endpoint.browse?.id.startsWith('UC')); if (artists) { @@ -99,45 +99,45 @@ class MusicResponsiveListItem { name: artist.text, channel_id: artist.endpoint.browse.id, endpoint: artist.endpoint - })) + })); } } - + #parseVideo() { this.id = this.#playlist_item_data.video_id; this.title = this.#flex_columns[0].title.toString(); this.views = this.#flex_columns[1].title.runs .find((run) => run.text.match(/(.*?) views/)).text; - + const authors = this.#flex_columns[1].title.runs?.filter((run) => run.endpoint.browse?.id.startsWith('UC')); - if (authors) { - this.authors = authors.map((author) => ({ - name: author.text, - channel_id: author.endpoint.browse.id, - endpoint: author.endpoint - })) - } - + if (authors) { + this.authors = authors.map((author) => ({ + name: author.text, + channel_id: author.endpoint.browse.id, + endpoint: author.endpoint + })); + } + const duration_text = this.#flex_columns[1].title.runs - .find((run) => /^\d+$/.test(run.text.replace(/:/g, '')))?.text; - + .find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text; + duration_text && (this.duration = { text: duration_text, seconds: Utils.timeToSeconds(duration_text) }); } - + #parseArtist() { this.id = this.endpoint.browse.id; this.name = this.#flex_columns[0].title.toString(); this.subscribers = this.#flex_columns[1].title.runs[2]?.text || ''; } - + #parseAlbum() { this.id = this.endpoint.browse.id; this.title = this.#flex_columns[0].title.toString(); - + const author = this.#flex_columns[1].title.runs.find((run) => run.endpoint.browse?.id.startsWith('UC')); author && (this.author = { @@ -145,16 +145,16 @@ class MusicResponsiveListItem { channel_id: author.endpoint.browse.id, endpoint: author.endpoint }); - + this.year = this.#flex_columns[1].title.runs - .find((run) => /^[12][0-9]{3}$/.test(run.text)).text; + .find((run) => (/^[12][0-9]{3}$/).test(run.text)).text; } - + #parsePlaylist() { this.id = this.endpoint.browse.id; this.title = this.#flex_columns[0].title.toString(); this.item_count = parseInt(this.#flex_columns[1].title.runs.find((run) => run.text.match(/\d+ (song|songs)/)).text.match(/\d+/g)); - + const author = this.#flex_columns[1].title.runs.find((run) => run.endpoint.browse?.id.startsWith('UC')); author && (this.author = { diff --git a/lib/parser/contents/classes/MusicResponsiveListItemFixedColumn.js b/lib/parser/contents/classes/MusicResponsiveListItemFixedColumn.js index bfb2b201..5aba19cd 100644 --- a/lib/parser/contents/classes/MusicResponsiveListItemFixedColumn.js +++ b/lib/parser/contents/classes/MusicResponsiveListItemFixedColumn.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class MusicResponsiveListItemFixedColumn { type = 'musicResponsiveListItemFlexColumnRenderer'; - + constructor(data) { this.title = new Text(data.text); this.display_priority = data.displayPriority; diff --git a/lib/parser/contents/classes/MusicResponsiveListItemFlexColumn.js b/lib/parser/contents/classes/MusicResponsiveListItemFlexColumn.js index 69f03579..f0ce7d05 100644 --- a/lib/parser/contents/classes/MusicResponsiveListItemFlexColumn.js +++ b/lib/parser/contents/classes/MusicResponsiveListItemFlexColumn.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class MusicResponsiveListItemFlexColumn { type = 'musicResponsiveListItemFlexColumnRenderer'; - + constructor(data) { this.title = new Text(data.text); this.display_priority = data.displayPriority; diff --git a/lib/parser/contents/classes/MusicShelf.js b/lib/parser/contents/classes/MusicShelf.js index 12f9c4ba..e50b02af 100644 --- a/lib/parser/contents/classes/MusicShelf.js +++ b/lib/parser/contents/classes/MusicShelf.js @@ -6,22 +6,22 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class MusicShelf { type = 'MusicShelf'; - + constructor(data) { this.title = new Text(data.title).toString(); - + this.contents = Parser.parse(data.contents); - + if (data.bottomEndpoint) { - this.endpoint = new NavigationEndpoint(data.bottomEndpoint) + this.endpoint = new NavigationEndpoint(data.bottomEndpoint); } - + if (this.continuation) { - this.continuation = data.continuations?.[0].nextContinuationData.continuation + this.continuation = data.continuations?.[0].nextContinuationData.continuation; } - + if (data.bottomText) { - this.bottom_text = new Text(data.bottomText) + this.bottom_text = new Text(data.bottomText); } } } diff --git a/lib/parser/contents/classes/MusicThumbnail.js b/lib/parser/contents/classes/MusicThumbnail.js index fd93bd06..35ffe728 100644 --- a/lib/parser/contents/classes/MusicThumbnail.js +++ b/lib/parser/contents/classes/MusicThumbnail.js @@ -4,7 +4,7 @@ const Thumbnail = require('./Thumbnail'); class MusicThumbnail { type = 'MusicThumbnail'; - + constructor(data) { return Thumbnail.fromResponse(data.thumbnail); } diff --git a/lib/parser/contents/classes/MusicTwoRowItem.js b/lib/parser/contents/classes/MusicTwoRowItem.js index 2e540970..8d19bebd 100644 --- a/lib/parser/contents/classes/MusicTwoRowItem.js +++ b/lib/parser/contents/classes/MusicTwoRowItem.js @@ -7,18 +7,18 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class MusicTwoRowItem { type = 'MusicTwoRowItem'; - + constructor(data) { this.title = new Text(data.title); - + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); - + this.id = this.endpoint.browse?.id || this.endpoint.watch.video_id; - + this.subtitle = new Text(data.subtitle); this.badges = Parser.parse(data.subtitleBadges); - + switch (this.endpoint.browse?.page_type) { case 'MUSIC_PAGE_TYPE_ARTIST': this.type = 'artist'; @@ -29,25 +29,25 @@ class MusicTwoRowItem { this.item_count = parseInt(this.subtitle.runs .find((run) => run.text .match(/\d+ (songs|song)/))?.text - .match(/\d+/g)) || null; + .match(/\d+/g)) || null; break; case 'MUSIC_PAGE_TYPE_ALBUM': this.type = 'album'; - + const artists = this.subtitle.runs.filter((run) => run.endpoint.browse?.id.startsWith('UC')); - + if (artists) { this.artists = artists.map((artist) => ({ name: artist.text, channel_id: artist.endpoint.browse.id, endpoint: artist.endpoint - })) + })); } - + this.year = this.subtitle.runs.slice(-1)[0].text; - if (isNaN(this.year)) { + + if (isNaN(this.year)) delete this.year; - } break; default: if (this.subtitle.runs[0].text !== 'Song') { @@ -55,32 +55,34 @@ class MusicTwoRowItem { } else { this.type = 'song'; } - + if (this.type == 'video') { this.views = this.subtitle.runs .find((run) => run.text.match(/(.*?) views/)).text; - + const author = this.subtitle.runs.find((run) => run.endpoint.browse?.id.startsWith('UC')); - - author && (this.author = { - name: author.text, - channel_id: author.endpoint.browse.id, - endpoint: author.endpoint - }); + + if (author) { + this.author = { + name: author.text, + channel_id: author.endpoint.browse.id, + endpoint: author.endpoint + }; + } } else { const artists = this.subtitle.runs.filter((run) => run.endpoint.browse?.id.startsWith('UC')); - + if (artists) { this.artists = artists.map((artist) => ({ name: artist.text, channel_id: artist.endpoint.browse.id, endpoint: artist.endpoint - })) + })); } } break; } - + this.thumbnail = Thumbnail.fromResponse(data.thumbnailRenderer.musicThumbnailRenderer.thumbnail); this.thumbnail_overlay = Parser.parse(data.thumbnailOverlay); this.menu = Parser.parse(data.menu); diff --git a/lib/parser/contents/classes/NavigatableText.js b/lib/parser/contents/classes/NavigatableText.js index e0966d37..c3b0c3cf 100644 --- a/lib/parser/contents/classes/NavigatableText.js +++ b/lib/parser/contents/classes/NavigatableText.js @@ -9,12 +9,12 @@ class NavigatableText extends Text { constructor(node) { super(node); // TODO: is this needed? Text now supports this itself - this.endpoint = - node.runs?.[0]?.navigationEndpoint ? - new NavigationEndpoint(node.runs[0].navigationEndpoint) : - node.navigationEndpoint ? - new NavigationEndpoint(node.navigationEndpoint) : - node.titleNavigationEndpoint ? + this.endpoint = + node.runs?.[0]?.navigationEndpoint ? + new NavigationEndpoint(node.runs[0].navigationEndpoint) : + node.navigationEndpoint ? + new NavigationEndpoint(node.navigationEndpoint) : + node.titleNavigationEndpoint ? new NavigationEndpoint(node.titleNavigationEndpoint) : null; } diff --git a/lib/parser/contents/classes/NavigationEndpoint.js b/lib/parser/contents/classes/NavigationEndpoint.js index 64031e43..6ca5980a 100644 --- a/lib/parser/contents/classes/NavigationEndpoint.js +++ b/lib/parser/contents/classes/NavigationEndpoint.js @@ -8,31 +8,44 @@ class NavigationEndpoint { type = 'NavigationEndpoint'; constructor(data) { + const name = Object.keys(data || {}) + .find( + (item) => + item.endsWith('Endpoint') || + item.endsWith('Command') + ); + + this.payload = data?.[name] || {}; + + if (Reflect.has(this.payload, 'dialog')) { + this.dialog = Parser.parse(this.payload.dialog); + } + if (data?.serviceEndpoint) { - data = data.serviceEndpoint + data = data.serviceEndpoint; } - + this.metadata = {}; - + if (data?.commandMetadata?.webCommandMetadata?.url) { - this.metadata.url = data.commandMetadata.webCommandMetadata.url + this.metadata.url = data.commandMetadata.webCommandMetadata.url; } - + if (data?.commandMetadata?.webCommandMetadata?.webPageType) { - this.metadata.page_type = data.commandMetadata.webCommandMetadata.webPageType + this.metadata.page_type = data.commandMetadata.webCommandMetadata.webPageType; } - + if (data?.commandMetadata?.webCommandMetadata?.apiUrl) { - this.metadata.api_url = data.commandMetadata.webCommandMetadata.apiUrl.replace('/youtubei/v1/', '') + this.metadata.api_url = data.commandMetadata.webCommandMetadata.apiUrl.replace('/youtubei/v1/', ''); } - + if (data?.commandMetadata?.webCommandMetadata?.sendPost) { - this.metadata.send_post = data.commandMetadata.webCommandMetadata.sendPost + this.metadata.send_post = data.commandMetadata.webCommandMetadata.sendPost; } - + if (data?.browseEndpoint) { const configs = data?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig; - + this.browse = { id: data?.browseEndpoint?.browseId || null, params: data?.browseEndpoint.params || null, @@ -40,143 +53,161 @@ class NavigationEndpoint { page_type: configs?.pageType || null }; } - + if (data?.watchEndpoint) { const configs = data?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig; - + this.watch = { video_id: data?.watchEndpoint?.videoId, playlist_id: data?.watchEndpoint.playlistId || null, params: data?.watchEndpoint.params || null, - index: data?.watchEndpoint.index || null, + index: data?.watchEndpoint.index || null, supported_onesie_config: data?.watchEndpoint?.watchEndpointSupportedOnesieConfig, music_video_type: configs?.musicVideoType || null }; } - + if (data?.searchEndpoint) { this.search = { query: data.searchEndpoint.query, params: data.searchEndpoint.params - } + }; } - + if (data?.subscribeEndpoint) { this.subscribe = { channel_ids: data.subscribeEndpoint.channelIds, params: data.subscribeEndpoint.params - } + }; } - + if (data?.unsubscribeEndpoint) { this.unsubscribe = { channel_ids: data.unsubscribeEndpoint.channelIds, params: data.unsubscribeEndpoint.params - } + }; } - + if (data?.likeEndpoint) { this.like = { status: data.likeEndpoint.status, target: { - video_id: data.likeEndpoint.target.videoId, + video_id: data.likeEndpoint.target.videoId, playlist_id: data.likeEndpoint.target.playlistId }, params: data.likeEndpoint?.removeLikeParams || data.likeEndpoint?.likeParams || data.likeEndpoint?.dislikeParams - } + }; } - + if (data?.performCommentActionEndpoint) { this.perform_comment_action = { action: data?.performCommentActionEndpoint.action - } + }; } - + if (data?.offlineVideoEndpoint) { this.offline_video = { video_id: data.offlineVideoEndpoint.videoId, on_add_command: { get_download_action: { video_id: data.offlineVideoEndpoint.videoId, - params: data.offlineVideoEndpoint.onAddCommand.getDownloadActionCommand.params, + params: data.offlineVideoEndpoint.onAddCommand.getDownloadActionCommand.params } } - } + }; } - + if (data?.continuationCommand) { this.continuation = { request: data?.continuationCommand?.request || null, token: data?.continuationCommand?.token || null }; } - + if (data?.feedbackEndpoint) { this.feedback = { token: data.feedbackEndpoint.feedbackToken - } + }; } - + if (data?.watchPlaylistEndpoint) { this.watch_playlist = { playlist_id: data.watchPlaylistEndpoint?.playlistId - } + }; } - + if (data?.playlistEditEndpoint) { this.playlist_edit = { playlist_id: data.playlistEditEndpoint.playlistId, actions: data.playlistEditEndpoint.actions.map((item) => ({ action: item.action, - removed_video_id: item.removedVideoId + removed_video_id: item.removedVideoId })) - } + }; } - + if (data?.addToPlaylistEndpoint) { this.add_to_playlist = { video_id: data.addToPlaylistEndpoint.videoId - } + }; } - + if (data?.addToPlaylistServiceEndpoint) { this.add_to_playlist = { video_id: data.addToPlaylistServiceEndpoint.videoId - } + }; } - + if (data?.getReportFormEndpoint) { this.get_report_form = { params: data.getReportFormEndpoint.params - } + }; } - + if (data?.liveChatItemContextMenuEndpoint) { this.live_chat_item_context_menu = { params: data?.liveChatItemContextMenuEndpoint?.params - } + }; } - + if (data?.sendLiveChatVoteEndpoint) { this.send_live_chat_vote = { params: data.sendLiveChatVoteEndpoint.params - } + }; } - + if (data?.liveChatItemContextMenuEndpoint) { this.live_chat_item_context_menu = { params: data.liveChatItemContextMenuEndpoint.params - } + }; } } - + + /** + * Calls the endpoint. (This is an experiment and may replace {@link call()} in the future.). + * @param {import('../../../core/Actions')} actions + * @param {object} args + */ + async callTest(actions, args = { parse: true, params: {} }) { + if (!actions) + throw new Error('An active caller must be provided'); + + const response = await actions.execute(this.metadata.api_url, { ...this.payload, ...args.params }); + + if (args.parse) { + return Parser.parseResponse(response.data); + } + + return response; + } + async call(actions, client) { if (!actions) throw new Error('An active caller must be provided'); - + if (this.continuation) { switch (this.continuation.request) { case 'CONTINUATION_REQUEST_TYPE_BROWSE': { @@ -192,29 +223,23 @@ class NavigationEndpoint { return Parser.parseResponse(response.data); } default: - throw new Error(this.continuation.request + ' not implemented'); + throw new Error(`${this.continuation.request} not implemented`); } } - + if (this.search) { const response = await actions.search({ query: this.search.query, params: this.search.params, client }); return Parser.parseResponse(response.data); } - + if (this.browse) { - const args = { client }; - - if (this.browse.params) { - args.params = this.browse.params - } - - const response = await actions.browse(this.browse.id, args); + const response = await actions.browse(this.browse.id, { ...this.browse, client }); return Parser.parseResponse(response.data); } - + if (this.like) { const response = await actions.engage(this.metadata.api_url, { video_id: this.like.target.video_id, params: this.like.params }); - return response; + return response; } } } diff --git a/lib/parser/contents/classes/PlayerAnnotationsExpanded.js b/lib/parser/contents/classes/PlayerAnnotationsExpanded.js index 4637ff43..e33cfb8e 100644 --- a/lib/parser/contents/classes/PlayerAnnotationsExpanded.js +++ b/lib/parser/contents/classes/PlayerAnnotationsExpanded.js @@ -6,7 +6,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class PlayerAnnotationsExpanded { type = 'PlayerAnnotationsExpanded'; - + constructor(data) { this.featured_channel = { start_time_ms: data.featuredChannel.startTimeMs, @@ -15,7 +15,7 @@ class PlayerAnnotationsExpanded { channel_name: data.featuredChannel.channelName, endpoint: new NavigationEndpoint(data.featuredChannel.navigationEndpoint), subscribe_button: Parser.parse(data.featuredChannel.subscribeButton) - } + }; this.allow_swipe_dismiss = data.allowSwipeDismiss; this.annotation_id = data.annotationId; } diff --git a/lib/parser/contents/classes/PlayerCaptionsTracklist.js b/lib/parser/contents/classes/PlayerCaptionsTracklist.js index 69805aa1..0fa44f87 100644 --- a/lib/parser/contents/classes/PlayerCaptionsTracklist.js +++ b/lib/parser/contents/classes/PlayerCaptionsTracklist.js @@ -3,7 +3,7 @@ const Text = require('./Text'); class PlayerCaptionsTracklist { - type = 'PlayerCaptionsTracklist' + type = 'PlayerCaptionsTracklist'; constructor(data) { this.caption_tracks = data.captionTracks.map((ct) => ({ @@ -14,11 +14,11 @@ class PlayerCaptionsTracklist { kind: ct.kind, is_translatable: ct.isTranslatable })); - + this.audio_tracks = data.audioTracks.map((at) => ({ caption_track_indices: at.captionTrackIndices })); - + this.translation_languages = data.translationLanguages.map((tl) => ({ language_code: tl.languageCode, language_name: new Text(tl.languageName) diff --git a/lib/parser/contents/classes/PlayerErrorMessage.js b/lib/parser/contents/classes/PlayerErrorMessage.js index aed4091e..a530df03 100644 --- a/lib/parser/contents/classes/PlayerErrorMessage.js +++ b/lib/parser/contents/classes/PlayerErrorMessage.js @@ -6,7 +6,7 @@ const Thumbnail = require('./Thumbnail'); class PlayerErrorMessage { type = 'PlayerErrorMessage'; - + constructor(data) { this.subreason = new Text(data.subreason); this.reason = new Text(data.reason); diff --git a/lib/parser/contents/classes/PlayerLiveStoryboardSpec.js b/lib/parser/contents/classes/PlayerLiveStoryboardSpec.js index 491e348d..960b1551 100644 --- a/lib/parser/contents/classes/PlayerLiveStoryboardSpec.js +++ b/lib/parser/contents/classes/PlayerLiveStoryboardSpec.js @@ -2,7 +2,7 @@ class PlayerLiveStoryboardSpec { type = 'PlayerLiveStoryboardSpec'; - + constructor() { // TODO: A little bit different from PlayerLiveStoryboardSpec // https://i.ytimg.com/sb/5qap5aO4i9A/storyboard_live_90_2x2_b2/M$M.jpg?rs=AOn4CLC9s6IeOsw_gKvEbsbU9y-e2FVRTw#159#90#2#2 diff --git a/lib/parser/contents/classes/PlayerMicroformat.js b/lib/parser/contents/classes/PlayerMicroformat.js index f076d056..6a6e90f3 100644 --- a/lib/parser/contents/classes/PlayerMicroformat.js +++ b/lib/parser/contents/classes/PlayerMicroformat.js @@ -5,7 +5,7 @@ const Thumbnail = require('./Thumbnail'); class PlayerMicroformat { type = 'PlayerMicroformat'; - + constructor(data) { this.title = new Text(data.title); this.description = new Text(data.description); @@ -16,13 +16,13 @@ class PlayerMicroformat { flash_secure_url: data.embed.flashSecureUrl, width: data.embed.width, height: data.embed.height - } + }; this.length_seconds = parseInt(data.lengthSeconds); this.channel = { id: data.externalChannelId, name: data.ownerChannelName, url: data.ownerProfileUrl - } + }; this.is_family_safe = data.isFamilySafe; this.is_unlisted = data.isUnlisted; this.has_ypc_metadata = data.hasYpcMetadata; diff --git a/lib/parser/contents/classes/PlayerOverlay.js b/lib/parser/contents/classes/PlayerOverlay.js index c7e1c536..26cd221a 100644 --- a/lib/parser/contents/classes/PlayerOverlay.js +++ b/lib/parser/contents/classes/PlayerOverlay.js @@ -4,7 +4,7 @@ const Parser = require('..'); class PlayerOverlay { type = 'PlayerOverlay'; - + constructor(data) { this.end_screen = Parser.parse(data.endScreen); this.autoplay = Parser.parse(data.autoplay); diff --git a/lib/parser/contents/classes/PlayerOverlayAutoplay.js b/lib/parser/contents/classes/PlayerOverlayAutoplay.js index 616d1377..6ab66af1 100644 --- a/lib/parser/contents/classes/PlayerOverlayAutoplay.js +++ b/lib/parser/contents/classes/PlayerOverlayAutoplay.js @@ -7,7 +7,7 @@ const Thumbnail = require('./Thumbnail'); class PlayerOverlayAutoplay { type = 'PlayerOverlayAutoplay'; - + constructor(data) { this.title = new Text(data.title); this.video_id = data.videoId; diff --git a/lib/parser/contents/classes/PlayerStoryboardSpec.js b/lib/parser/contents/classes/PlayerStoryboardSpec.js index 49f7afc6..5ea97e6c 100644 --- a/lib/parser/contents/classes/PlayerStoryboardSpec.js +++ b/lib/parser/contents/classes/PlayerStoryboardSpec.js @@ -2,11 +2,11 @@ class PlayerStoryboardSpec { type = 'PlayerStoryboardSpec'; - + constructor(data) { const parts = data.spec.split('|'); const url = new URL(parts.shift()); - + this.boards = parts.map((part, i) => { let [ thumbnail_width, @@ -16,9 +16,9 @@ class PlayerStoryboardSpec { rows, interval, name, - sigh, + sigh ] = part.split('#'); - + url.searchParams.set('sigh', sigh); thumbnail_count = parseInt(thumbnail_count, 10); @@ -35,7 +35,7 @@ class PlayerStoryboardSpec { interval: parseInt(interval, 10), columns, rows, - storyboard_count, + storyboard_count }; }); } diff --git a/lib/parser/contents/classes/Playlist.js b/lib/parser/contents/classes/Playlist.js index 5990d396..c0bd130d 100644 --- a/lib/parser/contents/classes/Playlist.js +++ b/lib/parser/contents/classes/Playlist.js @@ -8,21 +8,21 @@ const PlaylistAuthor = require('./PlaylistAuthor'); class Playlist { type = 'Playlist'; - + constructor(data) { this.id = data.playlistId; this.title = new Text(data.title); - + this.author = data.shortBylineText?.simpleText ? new Text(data.shortBylineText) : new PlaylistAuthor(data.longBylineText, data.ownerBadges, null); - + this.thumbnails = Thumbnail.fromResponse(data.thumbnail || { thumbnails: data.thumbnails.map((th) => th.thumbnails).flat(1) }); this.video_count = new Text(data.thumbnailText); this.video_count_short = new Text(data.videoCountShortText); this.first_videos = Parser.parse(data.videos) || []; this.share_url = data.shareUrl || null; - + this.menu = Parser.parse(data.menu); this.badges = Parser.parse(data.ownerBadges); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); diff --git a/lib/parser/contents/classes/PlaylistAuthor.js b/lib/parser/contents/classes/PlaylistAuthor.js index 36e04ad1..b5cc3173 100644 --- a/lib/parser/contents/classes/PlaylistAuthor.js +++ b/lib/parser/contents/classes/PlaylistAuthor.js @@ -5,7 +5,7 @@ const Author = require('./Author'); class PlaylistAuthor extends Author { constructor(data) { super(data); - + delete this.badges; delete this.is_verified; delete this.is_verified_artist; diff --git a/lib/parser/contents/classes/PlaylistHeader.js b/lib/parser/contents/classes/PlaylistHeader.js index 3583fbe1..33390824 100644 --- a/lib/parser/contents/classes/PlaylistHeader.js +++ b/lib/parser/contents/classes/PlaylistHeader.js @@ -6,7 +6,7 @@ const Parser = require('..'); class PlaylistHeader { type = 'PlaylistHeader'; - + constructor(data) { this.id = data.playlistId; this.title = new Text(data.title); diff --git a/lib/parser/contents/classes/PlaylistPanel.js b/lib/parser/contents/classes/PlaylistPanel.js index 2a9fc0b5..fb8e8e9f 100644 --- a/lib/parser/contents/classes/PlaylistPanel.js +++ b/lib/parser/contents/classes/PlaylistPanel.js @@ -5,7 +5,7 @@ const Text = require('./Text'); class PlaylistPanel { type = 'PlaylistPanel'; - + constructor(data) { this.title = data.title; this.title_text = new Text(data.titleText); diff --git a/lib/parser/contents/classes/PlaylistPanelVideo.js b/lib/parser/contents/classes/PlaylistPanelVideo.js index a267bebe..1c117c02 100644 --- a/lib/parser/contents/classes/PlaylistPanelVideo.js +++ b/lib/parser/contents/classes/PlaylistPanelVideo.js @@ -8,24 +8,24 @@ const Utils = require('../../../utils/Utils'); class PlaylistPanelVideo { type = 'PlaylistPanelVideo'; - + constructor(data) { this.title = new Text(data.title); this.thumbnail = Thumbnail.fromResponse(data.thumbnail); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.selected = data.selected; this.video_id = data.videoId; - + this.duration = { text: new Text(data.lengthText).toString(), seconds: Utils.timeToSeconds(new Text(data.lengthText).toString()) - } - + }; + const album = new Text(data.longBylineText).runs.find((run) => run.endpoint.browse?.id.startsWith('MPR')); const artists = new Text(data.longBylineText).runs.filter((run) => run.endpoint.browse?.id.startsWith('UC')); - + this.author = new Text(data.shortBylineText).toString(); - + album && (this.album = { id: album.endpoint.browse.id, name: album.text, @@ -34,11 +34,11 @@ class PlaylistPanelVideo { }); this.artists = artists.map((artist) => ({ - name: artist.text, + name: artist.text, channel_id: artist.endpoint.browse.id, endpoint: artist.endpoint })); - + this.badges = Parser.parse(data.badges); this.menu = Parser.parse(data.menu); this.set_video_id = data.playlistSetVideoId; diff --git a/lib/parser/contents/classes/PlaylistSidebar.js b/lib/parser/contents/classes/PlaylistSidebar.js index b935a010..d017e8c0 100644 --- a/lib/parser/contents/classes/PlaylistSidebar.js +++ b/lib/parser/contents/classes/PlaylistSidebar.js @@ -4,11 +4,11 @@ const Parser = require('..'); class PlaylistSidebar { type = 'PlaylistSidebar'; - + constructor(data) { this.items = Parser.parse(data.items); } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/PlaylistSidebarPrimaryInfo.js b/lib/parser/contents/classes/PlaylistSidebarPrimaryInfo.js index 7f3d397e..36bb8d6b 100644 --- a/lib/parser/contents/classes/PlaylistSidebarPrimaryInfo.js +++ b/lib/parser/contents/classes/PlaylistSidebarPrimaryInfo.js @@ -8,7 +8,7 @@ class PlaylistSidebarPrimaryInfo { type = 'PlaylistSidebarPrimaryInfo'; constructor(data) { - this.stats = data.stats.map(stat => new Text(stat)); + this.stats = data.stats.map((stat) => new Text(stat)); this.thumbnail_renderer = Parser.parse(data.thumbnailRenderer); this.title = new Text(data.title); this.menu = data.menu && Parser.parse(data.menu); diff --git a/lib/parser/contents/classes/PlaylistVideo.js b/lib/parser/contents/classes/PlaylistVideo.js index 5c1b49ab..86d7f563 100644 --- a/lib/parser/contents/classes/PlaylistVideo.js +++ b/lib/parser/contents/classes/PlaylistVideo.js @@ -8,7 +8,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class PlaylistVideo { type = 'PlaylistVideo'; - + constructor(data) { this.id = data.videoId; this.index = new Text(data.index); @@ -23,8 +23,8 @@ class PlaylistVideo { this.duration = { text: new Text(data.lengthText).text, seconds: parseInt(data.lengthSeconds) - } + }; } } -module.exports = PlaylistVideo; \ No newline at end of file +module.exports = PlaylistVideo; \ No newline at end of file diff --git a/lib/parser/contents/classes/PlaylistVideoList.js b/lib/parser/contents/classes/PlaylistVideoList.js index 9e0c25bd..8185b8c6 100644 --- a/lib/parser/contents/classes/PlaylistVideoList.js +++ b/lib/parser/contents/classes/PlaylistVideoList.js @@ -4,7 +4,7 @@ const Parser = require('..'); class PlaylistVideoList { type = 'PlaylistVideoList'; - + constructor(data) { this.id = data.playlistId; this.is_editable = data.isEditable; diff --git a/lib/parser/contents/classes/PlaylistVideoThumbnail.js b/lib/parser/contents/classes/PlaylistVideoThumbnail.js index 43e995a4..1467ecc2 100644 --- a/lib/parser/contents/classes/PlaylistVideoThumbnail.js +++ b/lib/parser/contents/classes/PlaylistVideoThumbnail.js @@ -1,3 +1,5 @@ +'use strict'; + const Thumbnail = require('./Thumbnail'); class PlaylistVideoThumbnail { diff --git a/lib/parser/contents/classes/Poll.js b/lib/parser/contents/classes/Poll.js index faa41b9b..15e81441 100644 --- a/lib/parser/contents/classes/Poll.js +++ b/lib/parser/contents/classes/Poll.js @@ -6,7 +6,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class Poll { type = 'Poll'; - + constructor(data) { this.choices = data.choices.map((choice) => ({ text: new Text(choice.text).toString(), @@ -18,7 +18,7 @@ class Poll { vote_percentage_if_not_selected: new Text(choice.votePercentageIfSelected), image: Thumbnail.fromResponse(choice.image) })); - + this.total_votes = new Text(data.totalVotes); this.poll_type = data.type; } diff --git a/lib/parser/contents/classes/Post.js b/lib/parser/contents/classes/Post.js index 73168bb2..b38ec741 100644 --- a/lib/parser/contents/classes/Post.js +++ b/lib/parser/contents/classes/Post.js @@ -4,7 +4,7 @@ const BackstagePost = require('./BackstagePost'); class Post extends BackstagePost { type = 'Post'; - + constructor(data) { super(data); } diff --git a/lib/parser/contents/classes/ProfileColumn.js b/lib/parser/contents/classes/ProfileColumn.js index 88c84007..c5a99989 100644 --- a/lib/parser/contents/classes/ProfileColumn.js +++ b/lib/parser/contents/classes/ProfileColumn.js @@ -4,11 +4,11 @@ const Parser = require('..'); class ProfileColumn { type = 'ProfileColumn'; - + constructor(data) { this.items = Parser.parse(data.items); } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/ProfileColumnStats.js b/lib/parser/contents/classes/ProfileColumnStats.js index edc5a494..5a1029be 100644 --- a/lib/parser/contents/classes/ProfileColumnStats.js +++ b/lib/parser/contents/classes/ProfileColumnStats.js @@ -4,11 +4,11 @@ const Parser = require('..'); class ProfileColumnStats { type = 'ProfileColumnStats'; - + constructor(data) { this.items = Parser.parse(data.items); } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/ProfileColumnStatsEntry.js b/lib/parser/contents/classes/ProfileColumnStatsEntry.js index 1b69380e..726f9f26 100644 --- a/lib/parser/contents/classes/ProfileColumnStatsEntry.js +++ b/lib/parser/contents/classes/ProfileColumnStatsEntry.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ProfileColumnStatsEntry { type = 'ProfileColumnStatsEntry'; - + constructor(data) { this.label = new Text(data.label); this.value = new Text(data.value); diff --git a/lib/parser/contents/classes/ProfileColumnUserInfo.js b/lib/parser/contents/classes/ProfileColumnUserInfo.js index c85502a8..808271f7 100644 --- a/lib/parser/contents/classes/ProfileColumnUserInfo.js +++ b/lib/parser/contents/classes/ProfileColumnUserInfo.js @@ -5,7 +5,7 @@ const Thumbnail = require('./Thumbnail'); class ProfileColumnUserInfo { type = 'ProfileColumnUserInfo'; - + constructor(data) { this.title = new Text(data.title); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); diff --git a/lib/parser/contents/classes/ReelItem.js b/lib/parser/contents/classes/ReelItem.js index a4b844fa..4ffb9ffe 100644 --- a/lib/parser/contents/classes/ReelItem.js +++ b/lib/parser/contents/classes/ReelItem.js @@ -1,3 +1,5 @@ +'use strict'; + const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); const Thumbnail = require('./Thumbnail'); diff --git a/lib/parser/contents/classes/ReelShelf.js b/lib/parser/contents/classes/ReelShelf.js index 11f25176..bf48ac26 100644 --- a/lib/parser/contents/classes/ReelShelf.js +++ b/lib/parser/contents/classes/ReelShelf.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); @@ -10,7 +12,7 @@ class ReelShelf { this.items = Parser.parse(data.items); this.endpoint = data.endpoint ? new NavigationEndpoint(data.endpoint) : null; } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/RelatedChipCloud.js b/lib/parser/contents/classes/RelatedChipCloud.js index 939f503e..de71bbd7 100644 --- a/lib/parser/contents/classes/RelatedChipCloud.js +++ b/lib/parser/contents/classes/RelatedChipCloud.js @@ -4,7 +4,7 @@ const Parser = require('..'); class RelatedChipCloud { type = 'RelatedChipCloud'; - + constructor(data) { this.content = Parser.parse(data.content); } diff --git a/lib/parser/contents/classes/RichGrid.js b/lib/parser/contents/classes/RichGrid.js index bda58bb8..b9597ac3 100644 --- a/lib/parser/contents/classes/RichGrid.js +++ b/lib/parser/contents/classes/RichGrid.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); class RichGrid { diff --git a/lib/parser/contents/classes/RichItem.js b/lib/parser/contents/classes/RichItem.js index e8369b91..3096ab4b 100644 --- a/lib/parser/contents/classes/RichItem.js +++ b/lib/parser/contents/classes/RichItem.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); class RichItem { diff --git a/lib/parser/contents/classes/SearchBox.js b/lib/parser/contents/classes/SearchBox.js index ad92fb44..d2f83ed9 100644 --- a/lib/parser/contents/classes/SearchBox.js +++ b/lib/parser/contents/classes/SearchBox.js @@ -6,7 +6,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class SearchBox { type = 'SearchBox'; - + constructor(data) { this.endpoint = new NavigationEndpoint(data.endpoint); this.search_button = Parser.parse(data.searchButton); diff --git a/lib/parser/contents/classes/SecondarySearchContainer.js b/lib/parser/contents/classes/SecondarySearchContainer.js index ca13a10b..805e838f 100644 --- a/lib/parser/contents/classes/SecondarySearchContainer.js +++ b/lib/parser/contents/classes/SecondarySearchContainer.js @@ -4,7 +4,7 @@ const Parser = require('..'); class SecondarySearchContainer { type = 'SecondarySearchContainer'; - + constructor(data) { this.contents = Parser.parse(data.contents); } diff --git a/lib/parser/contents/classes/SectionList.js b/lib/parser/contents/classes/SectionList.js index dbfbbd44..483c6d55 100644 --- a/lib/parser/contents/classes/SectionList.js +++ b/lib/parser/contents/classes/SectionList.js @@ -4,14 +4,14 @@ const Parser = require('..'); class SectionList { type = 'SectionList'; - + constructor(data) { if (data.targetId) { this.target_id = data.targetId; } - + this.contents = Parser.parse(data.contents); - + if (data.continuations) { if (data.continuations[0].nextContinuationData) { this.continuation = data.continuations[0].nextContinuationData.continuation; @@ -19,7 +19,7 @@ class SectionList { this.continuation = data.continuations[0].reloadContinuationData.continuation; } } - + if (data.header) { this.header = Parser.parse(data.header); } diff --git a/lib/parser/contents/classes/Shelf.js b/lib/parser/contents/classes/Shelf.js index 176bf35a..21e82f3b 100644 --- a/lib/parser/contents/classes/Shelf.js +++ b/lib/parser/contents/classes/Shelf.js @@ -6,22 +6,22 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class Shelf { type = 'Shelf'; - + constructor(data) { this.title = new Text(data.title); - + if (data.endpoint) { - this.endpoint = new NavigationEndpoint(data.endpoint) + this.endpoint = new NavigationEndpoint(data.endpoint); } - + this.content = Parser.parse(data.content) || []; - + if (data.icon?.iconType) { - this.icon_type = data.icon?.iconType + this.icon_type = data.icon?.iconType; } - + if (data.menu) { - this.menu = Parser.parse(data.menu) + this.menu = Parser.parse(data.menu); } } } diff --git a/lib/parser/contents/classes/ShowingResultsFor.js b/lib/parser/contents/classes/ShowingResultsFor.js index 7fe5d9e2..91f4881a 100644 --- a/lib/parser/contents/classes/ShowingResultsFor.js +++ b/lib/parser/contents/classes/ShowingResultsFor.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class ShowingResultsFor { type = 'ShowingResultsFor'; - + constructor(data) { this.corrected_query = new Text(data.correctedQuery); this.endpoint = new NavigationEndpoint(data.correctedQueryEndpoint); diff --git a/lib/parser/contents/classes/SimpleCardTeaser.js b/lib/parser/contents/classes/SimpleCardTeaser.js index 46dd189a..1520ea25 100644 --- a/lib/parser/contents/classes/SimpleCardTeaser.js +++ b/lib/parser/contents/classes/SimpleCardTeaser.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class SimpleCardTeaser { type = 'SimpleCardTeaser'; - + constructor(data) { this.message = new Text(data.message); this.prominent = data.prominent; diff --git a/lib/parser/contents/classes/SingleActionEmergencySupport.js b/lib/parser/contents/classes/SingleActionEmergencySupport.js index bd543dbb..bb9beba8 100644 --- a/lib/parser/contents/classes/SingleActionEmergencySupport.js +++ b/lib/parser/contents/classes/SingleActionEmergencySupport.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class SingleActionEmergencySupport { type = 'SingleActionEmergencySupport'; - + constructor(data) { this.action_text = new Text(data.actionText); this.nav_text = new Text(data.navigationText); diff --git a/lib/parser/contents/classes/SingleColumnBrowseResults.js b/lib/parser/contents/classes/SingleColumnBrowseResults.js index e86325cb..2d0562cd 100644 --- a/lib/parser/contents/classes/SingleColumnBrowseResults.js +++ b/lib/parser/contents/classes/SingleColumnBrowseResults.js @@ -4,10 +4,10 @@ const Parser = require('..'); class SingleColumnBrowseResults { type = 'SingleColumnBrowseResults'; - + constructor(data) { this.tabs = Parser.parse(data.tabs); } } -module.exports = SingleColumnBrowseResults; \ No newline at end of file +module.exports = SingleColumnBrowseResults; \ No newline at end of file diff --git a/lib/parser/contents/classes/SingleColumnMusicWatchNextResults.js b/lib/parser/contents/classes/SingleColumnMusicWatchNextResults.js index 248ea0d3..cee04b1b 100644 --- a/lib/parser/contents/classes/SingleColumnMusicWatchNextResults.js +++ b/lib/parser/contents/classes/SingleColumnMusicWatchNextResults.js @@ -4,7 +4,7 @@ const Parser = require('..'); class SingleColumnMusicWatchNextResults { type = 'SingleColumnMusicWatchNextResults'; - + constructor(data) { return Parser.parse(data); } diff --git a/lib/parser/contents/classes/SingleHeroImage.js b/lib/parser/contents/classes/SingleHeroImage.js index bb6d2de7..035089b6 100644 --- a/lib/parser/contents/classes/SingleHeroImage.js +++ b/lib/parser/contents/classes/SingleHeroImage.js @@ -4,7 +4,7 @@ const Thumbnail = require('./Thumbnail'); class SingleHeroImage { type = 'SingleHeroImage'; - + constructor(data) { this.thumbnails = new Thumbnail(data.thumbnail).thumbnails; this.style = data.style; diff --git a/lib/parser/contents/classes/SortFilterSubMenu.js b/lib/parser/contents/classes/SortFilterSubMenu.js index e22a9c1d..b2934fda 100644 --- a/lib/parser/contents/classes/SortFilterSubMenu.js +++ b/lib/parser/contents/classes/SortFilterSubMenu.js @@ -4,7 +4,7 @@ const { observe } = require('../../../utils/Utils'); class SortFilterSubMenu { type = 'SortFilterSubMenu'; - + constructor(data) { this.sub_menu_items = observe(data.subMenuItems.map((item) => ({ title: item.title, @@ -12,7 +12,7 @@ class SortFilterSubMenu { continuation: item.continuation?.reloadContinuationData.continuation, subtitle: item.subtitle }))); - + this.label = data.accessibility.accessibilityData.label; } } diff --git a/lib/parser/contents/classes/SubFeedOption.js b/lib/parser/contents/classes/SubFeedOption.js index f133616c..2de7450f 100644 --- a/lib/parser/contents/classes/SubFeedOption.js +++ b/lib/parser/contents/classes/SubFeedOption.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class SubFeedOption { type = 'SubFeedOption'; - + constructor(data) { this.name = new Text(data.name); this.is_selected = data.isSelected; diff --git a/lib/parser/contents/classes/SubFeedSelector.js b/lib/parser/contents/classes/SubFeedSelector.js index a612661c..5a1f273d 100644 --- a/lib/parser/contents/classes/SubFeedSelector.js +++ b/lib/parser/contents/classes/SubFeedSelector.js @@ -5,7 +5,7 @@ const Text = require('./Text'); class SubFeedSelector { type = 'SubFeedSelector'; - + constructor(data) { this.title = new Text(data.title); this.options = Parser.parse(data.options); diff --git a/lib/parser/contents/classes/SubscribeButton.js b/lib/parser/contents/classes/SubscribeButton.js index fb17b3e6..5d1c42da 100644 --- a/lib/parser/contents/classes/SubscribeButton.js +++ b/lib/parser/contents/classes/SubscribeButton.js @@ -6,7 +6,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class SubscribeButton { type = 'SubscribeButton'; - + constructor(data) { this.title = new Text(data.buttonText); this.subscribed = data.subscribed; @@ -18,7 +18,7 @@ class SubscribeButton { this.unsubscribed_text = new Text(data.unsubscribedButtonText); this.notification_preference_button = Parser.parse(data.notificationPreferenceButton); this.endpoint = new NavigationEndpoint(data.serviceEndpoints?.[0] || data.onSubscribeEndpoints?.[0]); - } + } } module.exports = SubscribeButton; \ No newline at end of file diff --git a/lib/parser/contents/classes/SubscriptionNotificationToggleButton.js b/lib/parser/contents/classes/SubscriptionNotificationToggleButton.js index 3d1b0c21..1ddfd4a1 100644 --- a/lib/parser/contents/classes/SubscriptionNotificationToggleButton.js +++ b/lib/parser/contents/classes/SubscriptionNotificationToggleButton.js @@ -4,14 +4,14 @@ const Parser = require('..'); class SubscriptionNotificationToggleButton { type = 'SubscriptionNotificationToggleButton'; - + constructor(data) { this.states = data.states.map((state) => ({ - id: state.stateId, + id: state.stateId, next_id: state.nextStateId, - state: Parser.parse(state.state) + state: Parser.parse(state.state) })); - + this.current_state_id = data.currentStateId; this.target_id = data.targetId; } diff --git a/lib/parser/contents/classes/Tab.js b/lib/parser/contents/classes/Tab.js index a1fd7406..fcfff7c0 100644 --- a/lib/parser/contents/classes/Tab.js +++ b/lib/parser/contents/classes/Tab.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class Tab { type = 'Tab'; - + constructor(data) { this.title = data.title || 'N/A'; this.selected = data.selected || false; diff --git a/lib/parser/contents/classes/Tabbed.js b/lib/parser/contents/classes/Tabbed.js index 86ff6113..17983285 100644 --- a/lib/parser/contents/classes/Tabbed.js +++ b/lib/parser/contents/classes/Tabbed.js @@ -4,7 +4,7 @@ const Parser = require('..'); class Tabbed { type = 'Tabbed'; - + constructor(data) { return Parser.parse(data); } diff --git a/lib/parser/contents/classes/TabbedSearchResults.js b/lib/parser/contents/classes/TabbedSearchResults.js index 8be47b2a..b2ca51e1 100644 --- a/lib/parser/contents/classes/TabbedSearchResults.js +++ b/lib/parser/contents/classes/TabbedSearchResults.js @@ -4,13 +4,13 @@ const Parser = require('..'); class TabbedSearchResults { type = 'TabbedSearchResults'; - + #data; - + constructor(data) { this.#data = data; } - + get tabs() { return Parser.parse(this.#data.tabs); } diff --git a/lib/parser/contents/classes/Text.js b/lib/parser/contents/classes/Text.js index c2a4fb92..1cac6f6d 100644 --- a/lib/parser/contents/classes/Text.js +++ b/lib/parser/contents/classes/Text.js @@ -5,20 +5,20 @@ const EmojiRun = require('./EmojiRun'); class Text { text; - + constructor(data) { if (data?.hasOwnProperty('runs')) { this.runs = data.runs.map((run) => run.emoji && new EmojiRun(run) || new TextRun(run) ); - + this.text = this.runs.map((run) => run.text).join(''); } else { this.text = data?.simpleText || 'N/A'; } } - + toString() { return this.text; } diff --git a/lib/parser/contents/classes/TextHeader.js b/lib/parser/contents/classes/TextHeader.js index 78c56ae4..16c429e7 100644 --- a/lib/parser/contents/classes/TextHeader.js +++ b/lib/parser/contents/classes/TextHeader.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class TextHeader { type = 'TextHeader'; - + constructor(data) { this.title = new Text(data.title); this.style = data.style; diff --git a/lib/parser/contents/classes/Thumbnail.js b/lib/parser/contents/classes/Thumbnail.js index 47713f62..52117308 100644 --- a/lib/parser/contents/classes/Thumbnail.js +++ b/lib/parser/contents/classes/Thumbnail.js @@ -1,3 +1,5 @@ +'use strict'; + class Thumbnail { /** * @type {string} @@ -11,7 +13,7 @@ class Thumbnail { * @type {number} */ height; - + constructor ({ url, width, height }) { this.url = url; this.width = width; @@ -26,7 +28,7 @@ class Thumbnail { */ static fromResponse(data) { if (!data || !data.thumbnails) return; - return data.thumbnails.map(x => new Thumbnail(x)).sort((a, b) => b.width - a.width); + return data.thumbnails.map((x) => new Thumbnail(x)).sort((a, b) => b.width - a.width); } } diff --git a/lib/parser/contents/classes/ThumbnailOverlayBottomPanel.js b/lib/parser/contents/classes/ThumbnailOverlayBottomPanel.js index 052ea540..413a2668 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayBottomPanel.js +++ b/lib/parser/contents/classes/ThumbnailOverlayBottomPanel.js @@ -2,7 +2,7 @@ class ThumbnailOverlayBottomPanel { type = 'ThumbnailOverlayBottomPanel'; - + constructor(data) { this.type = data.icon.iconType; } diff --git a/lib/parser/contents/classes/ThumbnailOverlayEndorsement.js b/lib/parser/contents/classes/ThumbnailOverlayEndorsement.js index 8c882cac..d5ba42e0 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayEndorsement.js +++ b/lib/parser/contents/classes/ThumbnailOverlayEndorsement.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ThumbnailOverlayEndorsement { type = 'ThumbnailOverlayEndorsement'; - + constructor(data) { this.text = new Text(data.text).toString(); } diff --git a/lib/parser/contents/classes/ThumbnailOverlayHoverText.js b/lib/parser/contents/classes/ThumbnailOverlayHoverText.js index c839ddc4..137f3710 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayHoverText.js +++ b/lib/parser/contents/classes/ThumbnailOverlayHoverText.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ThumbnailOverlayHoverText { type = 'ThumbnailOverlayHoverText'; - + constructor(data) { this.text = new Text(data.text); this.type = data.icon.iconType; diff --git a/lib/parser/contents/classes/ThumbnailOverlayInlineUnplayable.js b/lib/parser/contents/classes/ThumbnailOverlayInlineUnplayable.js index a647be59..ae85fea5 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayInlineUnplayable.js +++ b/lib/parser/contents/classes/ThumbnailOverlayInlineUnplayable.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ThumbnailOverlayInlineUnplayable { type = 'ThumbnailOverlayInlineUnplayable'; - + constructor(data) { this.text = new Text(data.text).toString(); this.icon_type = data.icon.iconType; diff --git a/lib/parser/contents/classes/ThumbnailOverlayLoadingPreview.js b/lib/parser/contents/classes/ThumbnailOverlayLoadingPreview.js index 65be4ce2..ab5af6ed 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayLoadingPreview.js +++ b/lib/parser/contents/classes/ThumbnailOverlayLoadingPreview.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ThumbnailOverlayLoadingPreview { type = 'ThumbnailOverlayLoadingPreview'; - + constructor(data) { this.text = new Text(data.text); } diff --git a/lib/parser/contents/classes/ThumbnailOverlayNowPlaying.js b/lib/parser/contents/classes/ThumbnailOverlayNowPlaying.js index 38d5e1f5..ad4bc313 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayNowPlaying.js +++ b/lib/parser/contents/classes/ThumbnailOverlayNowPlaying.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ThumbnailOverlayNowPlaying { type = 'ThumbnailOverlayNowPlaying'; - + constructor(data) { this.text = new Text(data.text).text; } diff --git a/lib/parser/contents/classes/ThumbnailOverlayPinking.js b/lib/parser/contents/classes/ThumbnailOverlayPinking.js index 46da97aa..15123b6d 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayPinking.js +++ b/lib/parser/contents/classes/ThumbnailOverlayPinking.js @@ -2,7 +2,7 @@ class ThumbnailOverlayPinking { type = 'ThumbnailOverlayPinking'; - + constructor(data) { this.hack = data.hack; } diff --git a/lib/parser/contents/classes/ThumbnailOverlayPlaybackStatus.js b/lib/parser/contents/classes/ThumbnailOverlayPlaybackStatus.js index 67097c9a..91f43719 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayPlaybackStatus.js +++ b/lib/parser/contents/classes/ThumbnailOverlayPlaybackStatus.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ThumbnailOverlayPlaybackStatus { type = 'ThumbnailOverlayPlaybackStatus'; - + constructor(data) { this.text = data.texts.map((text) => new Text(text))[0].toString(); } diff --git a/lib/parser/contents/classes/ThumbnailOverlayResumePlayback.js b/lib/parser/contents/classes/ThumbnailOverlayResumePlayback.js index 435d22ab..553b3c3c 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayResumePlayback.js +++ b/lib/parser/contents/classes/ThumbnailOverlayResumePlayback.js @@ -2,7 +2,7 @@ class ThumbnailOverlayResumePlayback { type = 'ThumbnailOverlayResumePlayback'; - + constructor(data) { this.percent_duration_watched = data.percentDurationWatched; } diff --git a/lib/parser/contents/classes/ThumbnailOverlaySidePanel.js b/lib/parser/contents/classes/ThumbnailOverlaySidePanel.js index 3c0f53ae..c6c465ab 100644 --- a/lib/parser/contents/classes/ThumbnailOverlaySidePanel.js +++ b/lib/parser/contents/classes/ThumbnailOverlaySidePanel.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ThumbnailOverlaySidePanel { type = 'ThumbnailOverlaySidePanel'; - + constructor(data) { this.text = new Text(data.text); this.type = data.icon.iconType; diff --git a/lib/parser/contents/classes/ThumbnailOverlayTimeStatus.js b/lib/parser/contents/classes/ThumbnailOverlayTimeStatus.js index f1c49105..fa4880f4 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayTimeStatus.js +++ b/lib/parser/contents/classes/ThumbnailOverlayTimeStatus.js @@ -4,7 +4,7 @@ const Text = require('./Text'); class ThumbnailOverlayTimeStatus { type = 'ThumbnailOverlayTimeStatus'; - + constructor(data) { this.text = new Text(data.text).text; } diff --git a/lib/parser/contents/classes/ThumbnailOverlayToggleButton.js b/lib/parser/contents/classes/ThumbnailOverlayToggleButton.js index 6c24ca4d..2ed9e855 100644 --- a/lib/parser/contents/classes/ThumbnailOverlayToggleButton.js +++ b/lib/parser/contents/classes/ThumbnailOverlayToggleButton.js @@ -4,20 +4,20 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class ThumbnailOverlayToggleButton { type = 'ThumbnailOverlayToggleButton'; - + constructor(data) { this.is_toggled = data.isToggled || null; - + this.icon_type = { toggled: data.toggledIcon.iconType, untoggled: data.untoggledIcon.iconType - } - + }; + this.tooltip = { toggled: data.toggledTooltip, untoggled: data.untoggledTooltip - } - + }; + this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint); this.untoggled_endpoint = new NavigationEndpoint(data.untoggledServiceEndpoint); } diff --git a/lib/parser/contents/classes/ToggleButton.js b/lib/parser/contents/classes/ToggleButton.js index c0a71e37..022ec3eb 100644 --- a/lib/parser/contents/classes/ToggleButton.js +++ b/lib/parser/contents/classes/ToggleButton.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class ToggleButton { type = 'ToggleButton'; - + constructor(data) { this.text = new Text(data.defaultText); this.toggled_text = new Text(data.toggledText); @@ -14,25 +14,24 @@ class ToggleButton { this.is_toggled = data.isToggled; this.is_disabled = data.isDisabled; this.icon_type = data.defaultIcon.iconType; - + const acc_label = - data.defaultText?.accessibility?.accessibilityData.label || - data?.accessibility?.label; - - if (this.icon_type == 'LIKE') { - this.like_count = parseInt(acc_label.replace(/\D/g, '')) - if (this.like_count) { - this.short_like_count = new Text(data.defaultText).toString() - } - } - - this.endpoint = - data.defaultServiceEndpoint?.commandExecutorCommand?.commands && + data?.defaultText?.accessibility?.accessibilityData.label || + data?.accessibilityData?.accessibilityData.label || + data?.accessibility?.label; + + if (this.icon_type == 'LIKE') { + this.like_count = parseInt(acc_label.replace(/\D/g, '')); + this.short_like_count = new Text(data.defaultText).toString(); + } + + this.endpoint = + data.defaultServiceEndpoint?.commandExecutorCommand?.commands && new NavigationEndpoint(data.defaultServiceEndpoint.commandExecutorCommand.commands.pop()) || new NavigationEndpoint(data.defaultServiceEndpoint); - - this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint); - + + this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint); + this.button_id = data.toggleButtonSupportedData?.toggleButtonIdData?.id || null; this.target_id = data.targetId || null; } diff --git a/lib/parser/contents/classes/ToggleMenuServiceItem.js b/lib/parser/contents/classes/ToggleMenuServiceItem.js index 5dd8cd18..029315e8 100644 --- a/lib/parser/contents/classes/ToggleMenuServiceItem.js +++ b/lib/parser/contents/classes/ToggleMenuServiceItem.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class ToggleMenuServiceItem { type = 'ToggleMenuServiceItem'; - + constructor(data) { this.text = new Text(data.defaultText); this.toggled_text = new Text(data.toggledText); diff --git a/lib/parser/contents/classes/Tooltip.js b/lib/parser/contents/classes/Tooltip.js index c63babba..45465eea 100644 --- a/lib/parser/contents/classes/Tooltip.js +++ b/lib/parser/contents/classes/Tooltip.js @@ -5,16 +5,16 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class Tooltip { type = 'Tooltip'; - + constructor(data) { this.promo_config = { promo_id: data.promoConfig.promoId, impression_endpoints: data.promoConfig.impressionEndpoints .map((endpoint) => new NavigationEndpoint(endpoint)), accept: new NavigationEndpoint(data.promoConfig.acceptCommand), - dismiss: new NavigationEndpoint(data.promoConfig.dismissCommand), - } - + dismiss: new NavigationEndpoint(data.promoConfig.dismissCommand) + }; + this.target_id = data.targetId; this.details = new Text(data.detailsText); this.suggested_position = data.suggestedPosition.type; diff --git a/lib/parser/contents/classes/TwoColumnBrowseResults.js b/lib/parser/contents/classes/TwoColumnBrowseResults.js index 8fe1ac29..00c7b8b9 100644 --- a/lib/parser/contents/classes/TwoColumnBrowseResults.js +++ b/lib/parser/contents/classes/TwoColumnBrowseResults.js @@ -4,7 +4,7 @@ const Parser = require('..'); class TwoColumnBrowseResults { type = 'TwoColumnBrowseResults'; - + constructor(data) { this.tabs = Parser.parse(data.tabs); this.secondary_contents = Parser.parse(data.secondaryContents); diff --git a/lib/parser/contents/classes/TwoColumnSearchResults.js b/lib/parser/contents/classes/TwoColumnSearchResults.js index f0fe799f..7f54f9da 100644 --- a/lib/parser/contents/classes/TwoColumnSearchResults.js +++ b/lib/parser/contents/classes/TwoColumnSearchResults.js @@ -4,7 +4,7 @@ const Parser = require('..'); class TwoColumnSearchResults { type = 'TwoColumnSearchResults'; - + constructor(data) { this.primary_contents = Parser.parse(data.primaryContents); this.secondary_contents = Parser.parse(data.secondaryContents); diff --git a/lib/parser/contents/classes/TwoColumnWatchNextResults.js b/lib/parser/contents/classes/TwoColumnWatchNextResults.js index 17f83ed3..d92c76eb 100644 --- a/lib/parser/contents/classes/TwoColumnWatchNextResults.js +++ b/lib/parser/contents/classes/TwoColumnWatchNextResults.js @@ -4,7 +4,7 @@ const Parser = require('..'); class TwoColumnWatchNextResults { type = 'TwoColumnWatchNextResults'; - + constructor(data) { this.results = Parser.parse(data.results?.results.contents); this.secondary_results = Parser.parse(data.secondaryResults?.secondaryResults.results); diff --git a/lib/parser/contents/classes/UniversalWatchCard.js b/lib/parser/contents/classes/UniversalWatchCard.js index 408c8794..9840d8f8 100644 --- a/lib/parser/contents/classes/UniversalWatchCard.js +++ b/lib/parser/contents/classes/UniversalWatchCard.js @@ -4,7 +4,7 @@ const Parser = require('..'); class UniversalWatchCard { type = 'UniversalWatchCard'; - + constructor(data) { this.header = Parser.parse(data.header); this.call_to_action = Parser.parse(data.callToAction); diff --git a/lib/parser/contents/classes/VerticalList.js b/lib/parser/contents/classes/VerticalList.js index f83e7e7f..aa005331 100644 --- a/lib/parser/contents/classes/VerticalList.js +++ b/lib/parser/contents/classes/VerticalList.js @@ -5,13 +5,13 @@ const Text = require('./Text'); class VerticalList { type = 'VerticalList'; - + constructor(data) { this.items = Parser.parse(data.items); this.collapsed_item_count = data.collapsedItemCount; this.collapsed_state_button_text = new Text(data.collapsedStateButtonText); } - + // XXX: alias for consistency get contents() { return this.items; diff --git a/lib/parser/contents/classes/VerticalWatchCardList.js b/lib/parser/contents/classes/VerticalWatchCardList.js index f5c51059..0837351f 100644 --- a/lib/parser/contents/classes/VerticalWatchCardList.js +++ b/lib/parser/contents/classes/VerticalWatchCardList.js @@ -6,7 +6,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class VerticalWatchCardList { type = 'VerticalWatchCardList'; - + constructor(data) { this.items = Parser.parse(data.items); this.contents = this.items; // XXX: alias for consistency diff --git a/lib/parser/contents/classes/Video.js b/lib/parser/contents/classes/Video.js index 114bcd67..feba9e6b 100644 --- a/lib/parser/contents/classes/Video.js +++ b/lib/parser/contents/classes/Video.js @@ -9,21 +9,21 @@ const Utils = require('../../../utils/Utils'); class Video { type = 'Video'; - + constructor(data) { const overlay_time_status = data.thumbnailOverlays .find((overlay) => overlay.thumbnailOverlayTimeStatusRenderer) ?.thumbnailOverlayTimeStatusRenderer.text || 'N/A'; - + this.id = data.videoId; this.title = new Text(data.title); this.description_snippet = data.descriptionSnippet ? new Text(data.descriptionSnippet, '') : null; - + this.snippets = data.detailedMetadataSnippets?.map((snippet) => ({ text: new Text(snippet.snippetText), - hover_text: new Text(snippet.snippetHoverText), + hover_text: new Text(snippet.snippetHoverText) })) || []; - + this.thumbnails = Thumbnail.fromResponse(data.thumbnail); this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays); this.rich_thumbnail = data.richThumbnail && Parser.parse(data.richThumbnail); @@ -32,15 +32,15 @@ class Video { this.published = new Text(data.publishedTimeText); this.view_count_text = new Text(data.viewCountText); this.short_view_count_text = new Text(data.shortViewCountText); - + const upcoming = data.upcomingEventData && Number(`${data.upcomingEventData.startTime}000`); if (upcoming) this.upcoming = new Date(upcoming); - + this.duration = { - text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text, + text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text, seconds: Utils.timeToSeconds(data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text) }; - + this.show_action_menu = data.showActionMenu; this.is_watched = data.isWatched || false; this.menu = Parser.parse(data.menu); @@ -50,20 +50,19 @@ class Video { /** * @returns {string} */ - get description() { + get description() { if (this.snippets.length > 0) { - return this.snippets.map(snip => snip.text.toString()).join('') - } + return this.snippets.map((snip) => snip.text.toString()).join(''); + } return this.description_snippet?.toString() || ''; } - /** * @type {boolean} */ - get is_live() { - return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW'); + get is_live() { + return this.badges.some((badge) => badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW'); } /** @@ -77,7 +76,7 @@ class Video { * @type {boolean} */ get has_captions() { - return this.badges.some(badge => badge.label === 'CC'); + return this.badges.some((badge) => badge.label === 'CC'); } /** diff --git a/lib/parser/contents/classes/VideoDetails.js b/lib/parser/contents/classes/VideoDetails.js index a1c469f6..6cb571dd 100644 --- a/lib/parser/contents/classes/VideoDetails.js +++ b/lib/parser/contents/classes/VideoDetails.js @@ -6,44 +6,44 @@ class VideoDetails { /** * @type {string} */ - id; - /** - * @type {string} - */ - channel_id; - /** - * @type {string} - */ - title; - /** - * @type {string[]} - */ - keywords; - /** - * @type {string} - */ - short_description; - /** - * @type {string} - */ - author; + id; + /** + * @type {string} + */ + channel_id; + /** + * @type {string} + */ + title; + /** + * @type {string[]} + */ + keywords; + /** + * @type {string} + */ + short_description; + /** + * @type {string} + */ + author; - constructor(data) { - this.id = data.videoId; - this.channel_id = data.channelId; - this.title = data.title; - this.duration = parseInt(data.lengthSeconds); - this.keywords = data.keywords; - this.is_owner_viewing = !!data.isOwnerViewing; - this.short_description = data.shortDescription; - this.thumbnail = Thumbnail.fromResponse(data.thumbnail); - this.allow_ratings = !!data.allowRatings; - this.view_count = parseInt(data.viewCount); - this.author = data.author; - this.is_private = !!data.isPrivate; - this.is_live_content = !!data.isLiveContent; - this.is_crawlable = !!data.isCrawlable; - } + constructor(data) { + this.id = data.videoId; + this.channel_id = data.channelId; + this.title = data.title; + this.duration = parseInt(data.lengthSeconds); + this.keywords = data.keywords; + this.is_owner_viewing = !!data.isOwnerViewing; + this.short_description = data.shortDescription; + this.thumbnail = Thumbnail.fromResponse(data.thumbnail); + this.allow_ratings = !!data.allowRatings; + this.view_count = parseInt(data.viewCount); + this.author = data.author; + this.is_private = !!data.isPrivate; + this.is_live_content = !!data.isLiveContent; + this.is_crawlable = !!data.isCrawlable; + } } module.exports = VideoDetails; \ No newline at end of file diff --git a/lib/parser/contents/classes/VideoInfoCardContent.js b/lib/parser/contents/classes/VideoInfoCardContent.js index c62b27ae..24fe28db 100644 --- a/lib/parser/contents/classes/VideoInfoCardContent.js +++ b/lib/parser/contents/classes/VideoInfoCardContent.js @@ -6,7 +6,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class VideoInfoCardContent { type = 'VideoInfoCardContent'; - + constructor(data) { this.title = new Text(data.videoTitle); this.channel_name = new Text(data.channelName); diff --git a/lib/parser/contents/classes/VideoOwner.js b/lib/parser/contents/classes/VideoOwner.js index 1ce5f0d3..f8070f28 100644 --- a/lib/parser/contents/classes/VideoOwner.js +++ b/lib/parser/contents/classes/VideoOwner.js @@ -5,13 +5,13 @@ const Author = require('./Author'); class VideoOwner { type = 'VideoOwner'; - + constructor(data) { this.subscription_button = data.subscriptionButton || null; this.subscriber_count = new Text(data.subscriberCountText); this.author = new Author({ - ...data.title, - navigationEndpoint: data.navigationEndpoint + ...data.title, + navigationEndpoint: data.navigationEndpoint }, data.badges, data.thumbnail); } } diff --git a/lib/parser/contents/classes/VideoPrimaryInfo.js b/lib/parser/contents/classes/VideoPrimaryInfo.js index 8643ec94..fc623767 100644 --- a/lib/parser/contents/classes/VideoPrimaryInfo.js +++ b/lib/parser/contents/classes/VideoPrimaryInfo.js @@ -5,7 +5,7 @@ const Text = require('./Text'); class VideoPrimaryInfo { type = 'VideoPrimaryInfo'; - + constructor(data) { this.title = new Text(data.title); this.super_title_link = new Text(data.superTitleLink); diff --git a/lib/parser/contents/classes/VideoSecondaryInfo.js b/lib/parser/contents/classes/VideoSecondaryInfo.js index 52bc3a6c..7f95ad7a 100644 --- a/lib/parser/contents/classes/VideoSecondaryInfo.js +++ b/lib/parser/contents/classes/VideoSecondaryInfo.js @@ -5,7 +5,7 @@ const Text = require('./Text'); class VideoSecondaryInfo { type = 'VideoSecondaryInfo'; - + constructor(data) { this.owner = Parser.parse(data.owner); this.description = new Text(data.description); diff --git a/lib/parser/contents/classes/WatchCardCompactVideo.js b/lib/parser/contents/classes/WatchCardCompactVideo.js index db1c4241..963c655c 100644 --- a/lib/parser/contents/classes/WatchCardCompactVideo.js +++ b/lib/parser/contents/classes/WatchCardCompactVideo.js @@ -5,14 +5,14 @@ const { timeToSeconds } = require('../../../utils/Utils'); class WatchCardCompactVideo { type = 'WatchCardCompactVideo'; - + constructor(data) { this.title = new Text(data.title); - this.subtitle = new Text(data.subtitle); + this.subtitle = new Text(data.subtitle); this.duration = { text: new Text(data.lengthText).toString(), seconds: timeToSeconds(data.lengthText.simpleText) - } + }; this.style = data.style; } } diff --git a/lib/parser/contents/classes/WatchCardHeroVideo.js b/lib/parser/contents/classes/WatchCardHeroVideo.js index ea8ca212..498f4865 100644 --- a/lib/parser/contents/classes/WatchCardHeroVideo.js +++ b/lib/parser/contents/classes/WatchCardHeroVideo.js @@ -5,7 +5,7 @@ const NavigationEndpoint = require('./NavigationEndpoint'); class WatchCardHeroVideo { type = 'WatchCardHeroVideo'; - + constructor(data) { this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.call_to_action_button = Parser.parse(data.callToActionButton); diff --git a/lib/parser/contents/classes/WatchCardRichHeader.js b/lib/parser/contents/classes/WatchCardRichHeader.js index 5d5cd5c0..daf90e3f 100644 --- a/lib/parser/contents/classes/WatchCardRichHeader.js +++ b/lib/parser/contents/classes/WatchCardRichHeader.js @@ -1,3 +1,5 @@ +'use strict'; + const Author = require('./Author'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); @@ -9,7 +11,7 @@ class WatchCardRichHeader { this.title = new Text(data.title); this.title_endpoint = new NavigationEndpoint(data.titleNavigationEndpoint); this.subtitle = new Text(data.subtitle); - this.author = new Author(data, [data.titleBadge], data.avatar); + this.author = new Author(data, data.titleBadge ? [ data.titleBadge ] : null, data.avatar); this.author.name = this.title; this.style = data.style; } diff --git a/lib/parser/contents/classes/WatchCardSectionSequence.js b/lib/parser/contents/classes/WatchCardSectionSequence.js index 8cca6083..a6d7952c 100644 --- a/lib/parser/contents/classes/WatchCardSectionSequence.js +++ b/lib/parser/contents/classes/WatchCardSectionSequence.js @@ -4,7 +4,7 @@ const Parser = require('..'); class WatchCardSectionSequence { type = 'WatchCardSectionSequence'; - + constructor(data) { this.lists = Parser.parse(data.lists); } diff --git a/lib/parser/contents/classes/WatchNextTabbedResults.js b/lib/parser/contents/classes/WatchNextTabbedResults.js index 2356ae78..91c0b23e 100644 --- a/lib/parser/contents/classes/WatchNextTabbedResults.js +++ b/lib/parser/contents/classes/WatchNextTabbedResults.js @@ -4,7 +4,7 @@ const TwoColumnBrowseResults = require('./TwoColumnBrowseResults'); class WatchNextTabbedResults extends TwoColumnBrowseResults { type = 'WatchNextTabbedResults'; - + constructor(data) { super(data); } diff --git a/lib/parser/contents/classes/comments/AuthorCommentBadge.js b/lib/parser/contents/classes/comments/AuthorCommentBadge.js new file mode 100644 index 00000000..4d84e8a9 --- /dev/null +++ b/lib/parser/contents/classes/comments/AuthorCommentBadge.js @@ -0,0 +1,25 @@ +'use strict'; + +class AuthorCommentBadge { + type = 'AuthorCommentBadge'; + + #data; + + constructor(data) { + this.icon_type = data.icon.iconType; + this.tooltip = data.iconTooltip; + + // *** For consistency + this.tooltip === 'Verified' && + (this.style = 'BADGE_STYLE_TYPE_VERIFIED') && + (data.style = 'BADGE_STYLE_TYPE_VERIFIED'); + + this.#data = data; + } + + get orig_badge() { + return this.#data; + } +} + +module.exports = AuthorCommentBadge; \ No newline at end of file diff --git a/lib/parser/contents/classes/comments/CommentActionButtons.js b/lib/parser/contents/classes/comments/CommentActionButtons.js new file mode 100644 index 00000000..72535f39 --- /dev/null +++ b/lib/parser/contents/classes/comments/CommentActionButtons.js @@ -0,0 +1,15 @@ +'use strict'; + +const Parser = require('../..'); + +class CommentActionButtons { + type = 'CommentActionButtons'; + + constructor(data) { + this.like_button = Parser.parse(data.likeButton); + this.dislike_button = Parser.parse(data.dislikeButton); + this.reply_button = Parser.parse(data.replyButton); + } +} + +module.exports = CommentActionButtons; \ No newline at end of file diff --git a/lib/parser/contents/classes/comments/CommentReplies.js b/lib/parser/contents/classes/comments/CommentReplies.js new file mode 100644 index 00000000..1bb2e3f3 --- /dev/null +++ b/lib/parser/contents/classes/comments/CommentReplies.js @@ -0,0 +1,15 @@ +'use strict'; + +const Parser = require('../..'); + +class CommentReplies { + type = 'CommentReplies'; + + constructor(data) { + this.contents = Parser.parse(data.contents); + this.view_replies = Parser.parse(data.viewReplies); + this.hide_replies = Parser.parse(data.hideReplies); + } +} + +module.exports = CommentReplies; \ No newline at end of file diff --git a/lib/parser/contents/classes/comments/CommentSimplebox.js b/lib/parser/contents/classes/comments/CommentSimplebox.js new file mode 100644 index 00000000..a8652129 --- /dev/null +++ b/lib/parser/contents/classes/comments/CommentSimplebox.js @@ -0,0 +1,19 @@ +'use strict'; + +const Parser = require('../..'); +const Thumbnail = require('../Thumbnail'); +const Text = require('../Text'); + +class CommentSimplebox { + type = 'CommentSimplebox'; + + constructor(data) { + this.submit_button = Parser.parse(data.submitButton); + this.cancel_button = Parser.parse(data.cancelButton); + this.author_thumbnails = Thumbnail.fromResponse(data.authorThumbnail); + this.placeholder = new Text(data.placeholderText); + this.avatar_size = data.avatarSize; + } +} + +module.exports = CommentSimplebox; \ No newline at end of file diff --git a/lib/parser/contents/classes/livechat/AddChatItemAction.js b/lib/parser/contents/classes/livechat/AddChatItemAction.js index e6f32552..f325bdf8 100644 --- a/lib/parser/contents/classes/livechat/AddChatItemAction.js +++ b/lib/parser/contents/classes/livechat/AddChatItemAction.js @@ -4,7 +4,7 @@ const Parser = require('../..'); class AddChatItemAction { type = 'AddChatItemAction'; - + constructor(data) { this.item = Parser.parse(data.item, 'livechat/items'); this.client_id = data.clientId || null; diff --git a/lib/parser/contents/classes/livechat/AddLiveChatTickerItemAction.js b/lib/parser/contents/classes/livechat/AddLiveChatTickerItemAction.js index ac50d55f..58effaa3 100644 --- a/lib/parser/contents/classes/livechat/AddLiveChatTickerItemAction.js +++ b/lib/parser/contents/classes/livechat/AddLiveChatTickerItemAction.js @@ -4,7 +4,7 @@ const Parser = require('../..'); class AddLiveChatTickerItemAction { type = 'AddLiveChatTickerItemAction'; - + constructor(data) { this.item = Parser.parse(data.item, 'livechat/items'); this.duration_sec = data.durationSec; diff --git a/lib/parser/contents/classes/livechat/LiveChatActionPanel.js b/lib/parser/contents/classes/livechat/LiveChatActionPanel.js index b4880ef7..0f8746cf 100644 --- a/lib/parser/contents/classes/livechat/LiveChatActionPanel.js +++ b/lib/parser/contents/classes/livechat/LiveChatActionPanel.js @@ -4,7 +4,7 @@ const Parser = require('../..'); class LiveChatActionPanel { type = 'LiveChatActionPanel'; - + constructor(data) { this.id = data.id; this.contents = Parser.parse(data.contents, 'livechat/items'); diff --git a/lib/parser/contents/classes/livechat/MarkChatItemAsDeletedAction.js b/lib/parser/contents/classes/livechat/MarkChatItemAsDeletedAction.js index 5c633609..170bf966 100644 --- a/lib/parser/contents/classes/livechat/MarkChatItemAsDeletedAction.js +++ b/lib/parser/contents/classes/livechat/MarkChatItemAsDeletedAction.js @@ -4,7 +4,7 @@ const Text = require('../Text'); class MarkChatItemAsDeletedAction { type = 'MarkChatItemAsDeletedAction'; - + constructor(data) { this.deleted_state_message = new Text(data.deletedStateMessage); this.target_item_id = data.targetItemId; diff --git a/lib/parser/contents/classes/livechat/MarkChatItemsByAuthorAsDeletedAction.js b/lib/parser/contents/classes/livechat/MarkChatItemsByAuthorAsDeletedAction.js index ac0f4d9a..9df4d646 100644 --- a/lib/parser/contents/classes/livechat/MarkChatItemsByAuthorAsDeletedAction.js +++ b/lib/parser/contents/classes/livechat/MarkChatItemsByAuthorAsDeletedAction.js @@ -4,7 +4,7 @@ const Text = require('../Text'); class MarkChatItemsByAuthorAsDeletedAction { type = 'MarkChatItemsByAuthorAsDeletedAction'; - + constructor(data) { this.deleted_state_message = new Text(data.deletedStateMessage); this.channel_id = data.externalChannelId; diff --git a/lib/parser/contents/classes/livechat/ReplayChatItemAction.js b/lib/parser/contents/classes/livechat/ReplayChatItemAction.js index 25247879..6e324d6c 100644 --- a/lib/parser/contents/classes/livechat/ReplayChatItemAction.js +++ b/lib/parser/contents/classes/livechat/ReplayChatItemAction.js @@ -4,13 +4,13 @@ const Parser = require('../..'); class ReplayChatItemAction { type = 'ReplayChatItemAction'; - + constructor(data) { this.actions = Parser.parse(data.actions?.map((action) => { delete action.clickTrackingParams; return action; }), 'livechat') || []; - + this.video_offset_time_msec = data.videoOffsetTimeMsec; } } diff --git a/lib/parser/contents/classes/livechat/ShowLiveChatTooltipCommand.js b/lib/parser/contents/classes/livechat/ShowLiveChatTooltipCommand.js index 8adc9989..de583c5c 100644 --- a/lib/parser/contents/classes/livechat/ShowLiveChatTooltipCommand.js +++ b/lib/parser/contents/classes/livechat/ShowLiveChatTooltipCommand.js @@ -4,7 +4,7 @@ const Parser = require('../..'); class ShowLiveChatTooltipCommand { type = 'ShowLiveChatTooltipCommand'; - + constructor(data) { this.tooltip = Parser.parse(data.tooltip); } diff --git a/lib/parser/contents/classes/livechat/UpdateDateTextAction.js b/lib/parser/contents/classes/livechat/UpdateDateTextAction.js index 0ff0d1f5..1035ba6b 100644 --- a/lib/parser/contents/classes/livechat/UpdateDateTextAction.js +++ b/lib/parser/contents/classes/livechat/UpdateDateTextAction.js @@ -4,7 +4,7 @@ const Text = require('../Text'); class UpdateDateTextAction { type = 'UpdateDateTextAction'; - + constructor(data) { this.date_text = new Text(data.dateText).toString(); } diff --git a/lib/parser/contents/classes/livechat/UpdateDescriptionAction.js b/lib/parser/contents/classes/livechat/UpdateDescriptionAction.js index 9a68b10d..455cb260 100644 --- a/lib/parser/contents/classes/livechat/UpdateDescriptionAction.js +++ b/lib/parser/contents/classes/livechat/UpdateDescriptionAction.js @@ -4,7 +4,7 @@ const Text = require('../Text'); class UpdateDescriptionAction { type = 'UpdateDescriptionAction'; - + constructor(data) { this.description = new Text(data.description); } diff --git a/lib/parser/contents/classes/livechat/UpdateLiveChatPollAction.js b/lib/parser/contents/classes/livechat/UpdateLiveChatPollAction.js index 54f726bc..6e012472 100644 --- a/lib/parser/contents/classes/livechat/UpdateLiveChatPollAction.js +++ b/lib/parser/contents/classes/livechat/UpdateLiveChatPollAction.js @@ -4,7 +4,7 @@ const Parser = require('../..'); class UpdateLiveChatPollAction { type = 'UpdateLiveChatPollAction'; - + constructor(data) { this.poll_to_update = Parser.parse(data.pollToUpdate, 'livechat/items'); } diff --git a/lib/parser/contents/classes/livechat/UpdateTitleAction.js b/lib/parser/contents/classes/livechat/UpdateTitleAction.js index 238623e5..38fb05a1 100644 --- a/lib/parser/contents/classes/livechat/UpdateTitleAction.js +++ b/lib/parser/contents/classes/livechat/UpdateTitleAction.js @@ -4,7 +4,7 @@ const Text = require('../Text'); class UpdateTitleAction { type = 'UpdateTitleAction'; - + constructor(data) { this.title = new Text(data.title); } diff --git a/lib/parser/contents/classes/livechat/UpdateToggleButtonTextAction.js b/lib/parser/contents/classes/livechat/UpdateToggleButtonTextAction.js index 717994ac..400b6a7b 100644 --- a/lib/parser/contents/classes/livechat/UpdateToggleButtonTextAction.js +++ b/lib/parser/contents/classes/livechat/UpdateToggleButtonTextAction.js @@ -4,7 +4,7 @@ const Text = require('../Text'); class UpdateToggleButtonTextAction { type = 'UpdateToggleButtonTextAction'; - + constructor(data) { this.default_text = new Text(data.defaultText).toString(); this.toggled_text = new Text(data.toggledText).toString(); diff --git a/lib/parser/contents/classes/livechat/UpdateViewershipAction.js b/lib/parser/contents/classes/livechat/UpdateViewershipAction.js index 8344fca6..c674abbc 100644 --- a/lib/parser/contents/classes/livechat/UpdateViewershipAction.js +++ b/lib/parser/contents/classes/livechat/UpdateViewershipAction.js @@ -4,10 +4,10 @@ const Text = require('../Text'); class UpdateViewershipAction { type = 'UpdateViewershipAction'; - + constructor(data) { const view_count_renderer = data.viewCount.videoViewCountRenderer; - + this.view_count = new Text(view_count_renderer.viewCount); this.extra_short_view_count = new Text(view_count_renderer.extraShortViewCount); this.is_live = view_count_renderer.isLive; diff --git a/lib/parser/contents/classes/livechat/items/LiveChatBanner.js b/lib/parser/contents/classes/livechat/items/LiveChatBanner.js index c42e598c..650b5f10 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatBanner.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatBanner.js @@ -4,7 +4,7 @@ const Parser = require('../../..'); class LiveChatBanner { type = 'LiveChatBanner'; - + constructor(data) { this.header = Parser.parse(data.header, 'livechat/items'); this.contents = Parser.parse(data.contents, 'livechat/items'); diff --git a/lib/parser/contents/classes/livechat/items/LiveChatBannerHeader.js b/lib/parser/contents/classes/livechat/items/LiveChatBannerHeader.js index a115cb75..eae879df 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatBannerHeader.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatBannerHeader.js @@ -5,7 +5,7 @@ const Text = require('../../Text'); class LiveChatBannerHeader { type = 'LiveChatBannerHeader'; - + constructor(data) { this.text = new Text(data.text).toString(); this.icon_type = data.icon.iconType; diff --git a/lib/parser/contents/classes/livechat/items/LiveChatBannerPoll.js b/lib/parser/contents/classes/livechat/items/LiveChatBannerPoll.js index 2bf76bf9..d934c413 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatBannerPoll.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatBannerPoll.js @@ -6,16 +6,16 @@ const Thumbnail = require('../../Thumbnail'); class LiveChatBannerPoll { type = 'LiveChatBannerPoll'; - + constructor(data) { this.poll_question = new Text(data.pollQuestion); this.author_photo = Thumbnail.fromResponse(data.authorPhoto); - + this.choices = data.pollChoices.map((choice) => ({ option_id: choice.pollOptionId, text: new Text(choice.text).toString() })); - + this.collapsed_state_entity_key = data.collapsedStateEntityKey; this.live_chat_poll_state_entity_key = data.liveChatPollStateEntityKey; this.context_menu_button = Parser.parse(data.contextMenuButton); diff --git a/lib/parser/contents/classes/livechat/items/LiveChatMembershipItem.js b/lib/parser/contents/classes/livechat/items/LiveChatMembershipItem.js index a95f04fe..7801a59a 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatMembershipItem.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatMembershipItem.js @@ -7,19 +7,19 @@ const NavigationEndpoint = require('../../NavigationEndpoint'); class LiveChatMembershipItem { type = 'LiveChatMembershipItem'; - + constructor(data) { this.id = data.id; this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000); this.header_subtext = new Text(data.headerSubtext); - + this.author = { id: data.authorExternalChannelId, name: new Text(data?.authorName), thumbnails: Thumbnail.fromResponse(data.authorPhoto), badges: Parser.parse(data.authorBadges) }; - + this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint); } } diff --git a/lib/parser/contents/classes/livechat/items/LiveChatPaidMessage.js b/lib/parser/contents/classes/livechat/items/LiveChatPaidMessage.js index cb74a3a7..7358ff6a 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatPaidMessage.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatPaidMessage.js @@ -7,26 +7,26 @@ const Parser = require('../../..'); class LiveChatPaidMessage { type = 'LiveChatPaidMessage'; - + constructor(data) { this.message = new Text(data.message); - + this.author = { id: data.authorExternalChannelId, name: new Text(data.authorName), thumbnails: Thumbnail.fromResponse(data.authorPhoto), badges: Parser.parse(data.authorBadges) }; - + const badges = Parser.parse(data.authorBadges); - + this.author.badges = badges; this.author.is_moderator = badges?.some((badge) => badge.icon_type == 'MODERATOR') || null; this.author.is_verified = badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED') || null; this.author.is_verified_artist = badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST') || null; - + this.purchase_amount = new Text(data.purchaseAmountText).toString(); - + this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint); this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000); this.timestamp_text = new Text(data.timestampText).toString(); diff --git a/lib/parser/contents/classes/livechat/items/LiveChatPlaceholderItem.js b/lib/parser/contents/classes/livechat/items/LiveChatPlaceholderItem.js index 5c3b6214..9e8fc978 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatPlaceholderItem.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatPlaceholderItem.js @@ -2,7 +2,7 @@ class LiveChatPlaceholderItem { type = 'LiveChatPlaceholderItem'; - + constructor(data) { this.id = data.id; this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000); diff --git a/lib/parser/contents/classes/livechat/items/LiveChatTextMessage.js b/lib/parser/contents/classes/livechat/items/LiveChatTextMessage.js index 9576fe66..6f46c98b 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatTextMessage.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatTextMessage.js @@ -7,24 +7,24 @@ const Parser = require('../../..'); class LiveChatTextMessage { type = 'LiveChatTextMessage'; - + constructor(data) { this.message = new Text(data.message); - - + + this.author = { id: data.authorExternalChannelId, name: new Text(data.authorName), - thumbnails: Thumbnail.fromResponse(data.authorPhoto), + thumbnails: Thumbnail.fromResponse(data.authorPhoto) }; - + const badges = Parser.parse(data.authorBadges); - + this.author.badges = badges; this.author.is_moderator = badges?.some((badge) => badge.icon_type == 'MODERATOR') || null; this.author.is_verified = badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED') || null; this.author.is_verified_artist = badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST') || null; - + this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint); this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000); this.id = data.id; diff --git a/lib/parser/contents/classes/livechat/items/LiveChatTickerPaidMessageItem.js b/lib/parser/contents/classes/livechat/items/LiveChatTickerPaidMessageItem.js index bf2141fa..80414b8b 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatTickerPaidMessageItem.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatTickerPaidMessageItem.js @@ -7,27 +7,27 @@ const Parser = require('../../..'); class LiveChatTickerPaidMessageItem { type = 'LiveChatTickerPaidMessageItem'; - + constructor(data) { this.author = { id: data.authorExternalChannelId, thumbnails: Thumbnail.fromResponse(data.authorPhoto), badges: Parser.parse(data.authorBadges) }; - + const badges = Parser.parse(data.authorBadges); - + this.author.badges = badges; this.author.is_moderator = badges?.some((badge) => badge.icon_type == 'MODERATOR') || null; this.author.is_verified = badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED') || null; this.author.is_verified_artist = badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST') || null; - + this.amount = new Text(data.amount); this.duration_sec = data.durationSec; this.full_duration_sec = data.fullDurationSec; - + this.show_item = Parser.parse(data.showItemEndpoint.showLiveChatItemEndpoint.renderer, 'livechat/items'); - + this.show_item_endpoint = new NavigationEndpoint(data.showItemEndpoint); this.id = data.id; } diff --git a/lib/parser/contents/classes/livechat/items/LiveChatTickerSponsorItem.js b/lib/parser/contents/classes/livechat/items/LiveChatTickerSponsorItem.js index d55796ae..72afc7cd 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatTickerSponsorItem.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatTickerSponsorItem.js @@ -1,27 +1,27 @@ 'use strict'; -// const Parser = require('../../..'); +// Const Parser = require('../../..'); const Text = require('../../Text'); const Thumbnail = require('../../Thumbnail'); class LiveChatTickerSponsorItem { type = 'LiveChatTickerSponsorItem'; - + constructor(data) { this.id = data.id; this.detail_text = new Text(data.detailText).toString(); - + this.author = { id: data.authorExternalChannelId, name: new Text(data?.authorName), - thumbnails: Thumbnail.fromResponse(data.sponsorPhoto), + thumbnails: Thumbnail.fromResponse(data.sponsorPhoto) }; - + this.duration_sec = data.durationSec; - + // TODO: finish this - // console.log(data) + // Console.log(data) } } diff --git a/lib/parser/contents/classes/livechat/items/LiveChatViewerEngagementMessage.js b/lib/parser/contents/classes/livechat/items/LiveChatViewerEngagementMessage.js index ed90bf18..e614b78e 100644 --- a/lib/parser/contents/classes/livechat/items/LiveChatViewerEngagementMessage.js +++ b/lib/parser/contents/classes/livechat/items/LiveChatViewerEngagementMessage.js @@ -5,13 +5,13 @@ const Parser = require('../../..'); class LiveChatViewerEngagementMessage extends LiveChatTextMessage { type = 'LiveChatViewerEngagementMessage'; - + constructor(data) { super(data); - + delete this.author; delete this.menu_endpoint; - + this.icon_type = data.icon.iconType; this.action_button = Parser.parse(data.actionButton); } diff --git a/lib/parser/contents/classes/livechat/items/Poll.js b/lib/parser/contents/classes/livechat/items/Poll.js index 8d5d8a9e..86523661 100644 --- a/lib/parser/contents/classes/livechat/items/Poll.js +++ b/lib/parser/contents/classes/livechat/items/Poll.js @@ -6,10 +6,10 @@ const NavigationEndpoint = require('../../NavigationEndpoint'); class Poll { type = 'Poll'; - + constructor(data) { this.header = Parser.parse(data.header, 'livechat/items'); - + this.choices = data.choices.map((choice) => ({ text: new Text(choice.text).toString(), selected: choice.selected, @@ -17,7 +17,7 @@ class Poll { vote_percentage: new Text(choice.votePercentage).toString(), select_endpoint: new NavigationEndpoint(choice.selectServiceEndpoint) })); - + this.live_chat_poll_id = data.liveChatPollId; } } diff --git a/lib/parser/contents/classes/livechat/items/PollHeader.js b/lib/parser/contents/classes/livechat/items/PollHeader.js index 8ca5e72d..c1b898dc 100644 --- a/lib/parser/contents/classes/livechat/items/PollHeader.js +++ b/lib/parser/contents/classes/livechat/items/PollHeader.js @@ -6,7 +6,7 @@ const Parser = require('../../..'); class PollHeader { type = 'PollHeader'; - + constructor(data) { this.poll_question = new Text(data.pollQuestion); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index 5a279fae..b59104e0 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -6,7 +6,7 @@ const VideoDetails = require('./classes/VideoDetails'); class AppendContinuationItemsAction { type = 'appendContinuationItemsAction'; - + constructor (data) { this.contents = Parser.parse(data.continuationItems); } @@ -14,16 +14,16 @@ class AppendContinuationItemsAction { class ReloadContinuationItemsCommand { type = 'reloadContinuationItemsCommand'; - + constructor (data) { this.target_id = data.targetId; - this.contents = Parser.parse(data.continuationItems) + this.contents = Parser.parse(data.continuationItems); } } class SectionListContinuation { type = 'sectionListContinuation'; - + constructor(data) { this.contents = Parser.parse(data.contents); this.continuation = data.continuations[0].nextContinuationData.continuation; @@ -32,7 +32,7 @@ class SectionListContinuation { class TimedContinuation { type = 'timedContinuationData'; - + constructor(data) { this.timeout_ms = data.timeoutMs || data.timeUntilLastMessageMsec; this.token = data.continuation; @@ -41,19 +41,19 @@ class TimedContinuation { class LiveChatContinuation { type = 'liveChatContinuation'; - + constructor(data) { this.actions = Parser.parse(data.actions?.map((action) => { delete action.clickTrackingParams; return action; }), 'livechat') || []; - + this.action_panel = Parser.parse(data.actionPanel); this.item_list = Parser.parse(data.itemList); this.header = Parser.parse(data.header); this.participants_list = Parser.parse(data.participantsList); this.popout_message = Parser.parse(data.popoutMessage); - + this.emojis = data.emojis?.map((emoji) => ({ emoji_id: emoji.emojiId, shortcuts: emoji.shortcuts, @@ -61,41 +61,41 @@ class LiveChatContinuation { image: emoji.image, is_custom_emoji: emoji.isCustomEmoji })) || null; - + this.continuation = new TimedContinuation( data.continuations?.[0].timedContinuationData || data.continuations?.[0].invalidationContinuationData || data.continuations?.[0].liveChatReplayContinuationData); - + this.viewer_name = data.viewerName; } } class Parser { static #memo = new Map(); - + static #clearMemo() { Parser.#memo = null; } - + static #createMemo() { Parser.#memo = new Map(); } - + static #addToMemo(classname, result) { if (!Parser.#memo) return; - + if (!Parser.#memo.has(classname)) - return Parser.#memo.set(classname, [result]); - + return Parser.#memo.set(classname, [ result ]); + Parser.#memo.get(classname).push(result); } /** * Parses InnerTube response. - * - * @param {object} data + * + * @param {object} data * @returns {*} */ static parseResponse(data) { @@ -115,12 +115,12 @@ class Parser { const on_response_received_endpoints = data.onResponseReceivedEndpoints ? Parser.parseRR(data.onResponseReceivedEndpoints) : null; const on_response_received_endpoints_memo = Parser.#memo; this.#clearMemo(); - + this.#createMemo(); const on_response_received_commands = data.onResponseReceivedCommands ? Parser.parseRR(data.onResponseReceivedCommands) : null; const on_response_received_commands_memo = Parser.#memo; this.#clearMemo(); - + return { contents, contents_memo, @@ -162,7 +162,7 @@ class Parser { /** @type {import('./classes/Format')[]} */ adaptive_formats: Parser.parseFormats(data.streamingData.adaptiveFormats), dash_manifest_url: data.streamingData?.dashManifestUrl || null, - dls_manifest_url: data.streamingData?.dashManifestUrl || null, + dls_manifest_url: data.streamingData?.dashManifestUrl || null }, captions: Parser.parse(data.captions), /** @type {import('./classes/VideoDetails')} */ @@ -172,31 +172,31 @@ class Parser { /** @type {import('./classes/Endscreen')} */ endscreen: Parser.parse(data.endscreen), /** @type {import('./classes/CardCollection')} */ - cards: Parser.parse(data.cards), - } + cards: Parser.parse(data.cards) + }; } - + static parseC(data) { - if (data.timedContinuationData) + if (data.timedContinuationData) return new TimedContinuation(data.timedContinuationData); } - + static parseLC(data) { if (data.sectionListContinuation) return new SectionListContinuation(data.sectionListContinuation); - if (data.liveChatContinuation) + if (data.liveChatContinuation) return new LiveChatContinuation(data.liveChatContinuation); } - + static parseRR(actions) { return observe(actions.map((action) => { if (action.reloadContinuationItemsCommand) return new ReloadContinuationItemsCommand(action.reloadContinuationItemsCommand); if (action.appendContinuationItemsAction) return new AppendContinuationItemsAction(action.appendContinuationItemsAction); - }).filter((item) => item)); + }).filter((item) => item)); } - + static parseLA(data) { if (Array.isArray(data)) { return Parser.parse(data.map((action) => { @@ -204,39 +204,39 @@ class Parser { return action; }), 'livechat'); } - + return Parser.parse(data) || null; } - + static parseFormats(formats) { return observe(formats?.map((format) => new Format(format)) || []); } - + /** * Parses the `contents` property of the response. * - * @param {object} data - * @param {string} module + * @param {object} data - contents to be parsed. + * @param {string} module - a folder for specific DA classes. * @returns {*} */ static parse(data, module) { if (!data) return null; - - if (Array.isArray(data)) { - let results = []; - for (let item of data) { + if (Array.isArray(data)) { + const results = []; + + for (const item of data) { const keys = Object.keys(item); const classname = this.sanitizeClassName(keys[0]); - + if (!this.shouldIgnore(classname)) { try { - const path = module ? module + '/' : ''; - - const TargetClass = require('./classes/' + path + classname); + const path = module ? `${module}/` : ''; + + const TargetClass = require(`./classes/${path}${classname}`); const result = new TargetClass(item[keys[0]]); - + results.push(result); this.#addToMemo(classname, result); } catch (err) { @@ -244,56 +244,58 @@ class Parser { } } } - - return observe(results); - } else { - const keys = Object.keys(data); - const classname = this.sanitizeClassName(keys[0]); - - if (!this.shouldIgnore(classname)) { - try { - const path = module ? module + '/' : ''; - - const TargetClass = require('./classes/' + path + classname); - const result = new TargetClass(data[keys[0]]); - - this.#addToMemo(classname, result); - return result; - } catch (err) { - this.formatError({ classname, classdata: data[keys[0]], err }); - return null; - } - } + return observe(results); } + const keys = Object.keys(data); + const classname = this.sanitizeClassName(keys[0]); + + if (!this.shouldIgnore(classname)) { + try { + const path = module ? `${module}/` : ''; + + const TargetClass = require(`./classes/${path}${classname}`); + const result = new TargetClass(data[keys[0]]); + + this.#addToMemo(classname, result); + + return result; + } catch (err) { + this.formatError({ classname, classdata: data[keys[0]], err }); + return null; + } + } + } - + static formatError({ classname, classdata, err }) { if (err.code == 'MODULE_NOT_FOUND') { return console.warn( - new InnertubeError(classname + ' not found!\n' + - 'This is a bug, please report it at ' + require('../../../package.json').bugs.url, - classdata) - ); - } - - console.warn( - new InnertubeError('Something went wrong at ' + classname + '!\n' + - 'This is a bug, please report it at ' + require('../../../package.json').bugs.url, - { stack: err.stack }) + new InnertubeError(`${classname} not found!\n` + + `This is a bug, please report it at ${require('../../../package.json').bugs.url}`, + classdata) ); + } + + console.warn( + new InnertubeError(`Something went wrong at ${classname}!\n` + + `This is a bug, please report it at ${require('../../../package.json').bugs.url}`, + { stack: err.stack }) + ); } - + static sanitizeClassName(input) { return (input.charAt(0).toUpperCase() + input.slice(1)) - .replace(/Renderer|Model/g, '') - .replace(/Radio/g, 'Mix').trim(); + .replace(/Renderer|Model/g, '') + .replace(/Radio/g, 'Mix').trim(); } - + static shouldIgnore(classname) { return [ 'DisplayAd', + 'SearchPyv', 'MealbarPromo', + 'BackgroundPromo', 'PromotedSparklesWeb', 'RunAttestationCommand' ].includes(classname); diff --git a/lib/parser/index.js b/lib/parser/index.js index 8b785836..0016471b 100644 --- a/lib/parser/index.js +++ b/lib/parser/index.js @@ -20,58 +20,58 @@ class Parser { parse() { const client = this.args.client; - const data_type = this.args.data_type + const data_type = this.args.data_type; let processed_data; switch (client) { case 'YOUTUBE': - processed_data = (() => { - switch (data_type) { - case 'SEARCH': - return this.#processSearch(); - case 'CHANNEL': - return this.#processChannel(); - case 'PLAYLIST': - return this.#processPlaylist(); - case 'SUBSFEED': - return this.#processSubscriptionFeed(); - case 'HOMEFEED': - return this.#processHomeFeed(); - case 'LIBRARY': - return this.#processLibrary(); // WIP - case 'TRENDING': - return this.#processTrending(); - case 'HISTORY': - return this.#processHistory(); - case 'COMMENTS': - return this.#processComments(); - case 'VIDEO_INFO': - return this.#processVideoInfo(); - case 'NOTIFICATIONS': - return this.#processNotifications(); - case 'SEARCH_SUGGESTIONS': - return this.#processSearchSuggestions(); - default: - // this is just for maximum compatibility, this is most definitely a bad way to handle this - throw new TypeError('undefined is not a function'); - } - })() + processed_data = (() => { + switch (data_type) { + case 'SEARCH': + return this.#processSearch(); + case 'CHANNEL': + return this.#processChannel(); + case 'PLAYLIST': + return this.#processPlaylist(); + case 'SUBSFEED': + return this.#processSubscriptionFeed(); + case 'HOMEFEED': + return this.#processHomeFeed(); + case 'LIBRARY': + return this.#processLibrary(); // WIP + case 'TRENDING': + return this.#processTrending(); + case 'HISTORY': + return this.#processHistory(); + case 'COMMENTS': + return this.#processComments(); + case 'VIDEO_INFO': + return this.#processVideoInfo(); + case 'NOTIFICATIONS': + return this.#processNotifications(); + case 'SEARCH_SUGGESTIONS': + return this.#processSearchSuggestions(); + default: + // This is just for maximum compatibility, this is most definitely a bad way to handle this + throw new TypeError('undefined is not a function'); + } + })(); break; case 'YTMUSIC': - processed_data = (() => { - switch (data_type) { - case 'SEARCH': - return this.#processMusicSearch(); - case 'PLAYLIST': - return this.#processMusicPlaylist(); - case 'SEARCH_SUGGESTIONS': - return this.#processMusicSearchSuggestions(); - default: - // this is just for maximum compatibility, this is most definitely a bad way to handle this - throw new TypeError('undefined is not a function'); - } - })() + processed_data = (() => { + switch (data_type) { + case 'SEARCH': + return this.#processMusicSearch(); + case 'PLAYLIST': + return this.#processMusicPlaylist(); + case 'SEARCH_SUGGESTIONS': + return this.#processMusicSearchSuggestions(); + default: + // This is just for maximum compatibility, this is most definitely a bad way to handle this + throw new TypeError('undefined is not a function'); + } + })(); break; default: throw new Utils.InnertubeError('Invalid client'); @@ -87,33 +87,33 @@ class Parser { const parseItems = (contents) => { const content = contents[0].itemSectionRenderer.contents; - + processed_data.query = content[0]?.showingResultsForRenderer?.originalQuery?.simpleText || this.args.query; processed_data.corrected_query = content[0]?.showingResultsForRenderer?.correctedQueryEndpoint?.searchEndpoint?.query || 'N/A'; processed_data.estimated_results = parseInt(this.data.estimatedResults); processed_data.videos = YTDataItems.VideoResultItem.parse(content); - + processed_data.getContinuation = async () => { const citem = contents.find((item) => item.continuationItemRenderer); const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; const response = await this.session.actions.search({ ctoken }); - + const continuation_items = Utils.findNode(response.data, 'onResponseReceivedCommands', 'itemSectionRenderer', 4, false); return parseItems(continuation_items); }; return processed_data; - } + }; return parseItems(contents); } - + #processMusicSearch() { const tabs = Utils.findNode(this.data, 'contents', 'tabs').tabs; const contents = Utils.findNode(tabs, '0', 'contents', 5); - + const did_you_mean_item = contents.find((content) => content.itemSectionRenderer); const did_you_mean_renderer = did_you_mean_item?.itemSectionRenderer.contents[0].didYouMeanRenderer; @@ -147,26 +147,26 @@ class Parser { return processed_data; } - + #processSearchSuggestions() { return YTDataItems.SearchSuggestionItem.parse(JSON.parse(this.data.replace(')]}\'', ''))); } - + #processMusicSearchSuggestions() { const contents = this.data.contents[0].searchSuggestionsSectionRenderer.contents; return YTMusicDataItems.MusicSearchSuggestionItem.parse(contents); } - + #processPlaylist() { const details = this.data.sidebar.playlistSidebarRenderer.items[0]; - + const metadata = { title: this.data.metadata.playlistMetadataRenderer.title, description: details.playlistSidebarPrimaryInfoRenderer?.description?.simpleText || 'N/A', total_items: details.playlistSidebarPrimaryInfoRenderer.stats[0].runs[0]?.text || 'N/A', last_updated: details.playlistSidebarPrimaryInfoRenderer.stats[2].runs[1]?.text || 'N/A', views: details.playlistSidebarPrimaryInfoRenderer.stats[1].simpleText - } + }; const list = Utils.findNode(this.data, 'contents', 'contents', 13, false); const items = YTDataItems.PlaylistItem.parse(list.contents); @@ -174,7 +174,7 @@ class Parser { return { ...metadata, items - } + }; } #processMusicPlaylist() { @@ -196,7 +196,7 @@ class Parser { return { ...metadata, items - } + }; } /** @@ -227,40 +227,38 @@ class Parser { mf_raw_data.forEach((entry) => { const key = Utils.camelToSnake(entry[0]); if (Constants.METADATA_KEYS.includes(key)) { - if (key == 'view_count') { - processed_data.metadata[key] = parseInt(entry[1]); - } else if (key == 'owner_profile_url') { - processed_data.metadata.channel_url = entry[1] - } else if (key == 'owner_channel_name') { - processed_data.metadata.channel_name = entry[1] - } else { - processed_data.metadata[key] = entry[1]; - } + if (key == 'view_count') { + processed_data.metadata[key] = parseInt(entry[1]); + } else if (key == 'owner_profile_url') { + processed_data.metadata.channel_url = entry[1]; + } else if (key == 'owner_channel_name') { + processed_data.metadata.channel_name = entry[1]; + } else { + processed_data.metadata[key] = entry[1]; + } } else { processed_data[key] = entry[1]; } }); - // Extracts extra details + // Extracts extra details dt_raw_data.forEach((entry) => { const key = Utils.camelToSnake(entry[0]); if (Constants.BLACKLISTED_KEYS.includes(key)) return; if (Constants.METADATA_KEYS.includes(key)) { - if (key == 'view_count') { - processed_data.metadata[key] = parseInt(entry[1]); - } else { - processed_data.metadata[key] = entry[1]; - } + if (key == 'view_count') { + processed_data.metadata[key] = parseInt(entry[1]); + } else { + processed_data.metadata[key] = entry[1]; + } + } else if (key == 'short_description') { + processed_data.description = entry[1]; + } else if (key == 'thumbnail') { + processed_data.thumbnail = entry[1].thumbnails.slice(-1)[0]; + } else if (key == 'video_id') { + processed_data.id = entry[1]; } else { - if (key == 'short_description') { - processed_data.description = entry[1]; - } else if (key == 'thumbnail') { - processed_data.thumbnail = entry[1].thumbnails.slice(-1)[0]; - } else if (key == 'video_id') { - processed_data.id = entry[1]; - } else { - processed_data[key] = entry[1]; - } + processed_data[key] = entry[1]; } }); @@ -304,100 +302,100 @@ class Parser { processed_data.metadata.owner_badges = secondary_info_renderer.owner.videoOwnerRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || []; } - if (streaming_data && streaming_data.adaptiveFormats) { - processed_data.metadata.available_qualities = [...new Set(streaming_data.adaptiveFormats.filter(v => v.qualityLabel).map(v => v.qualityLabel).sort((a, b) => +a.replace(/\D/gi, '') - +b.replace(/\D/gi, '')))] - } else { - processed_data.metadata.available_qualities = []; - } + if (streaming_data && streaming_data.adaptiveFormats) { + processed_data.metadata.available_qualities = [ ...new Set(streaming_data.adaptiveFormats.filter((v) => v.qualityLabel).map((v) => v.qualityLabel).sort((a, b) => +a.replace(/\D/gi, '') - +b.replace(/\D/gi, ''))) ]; + } else { + processed_data.metadata.available_qualities = []; + } return processed_data; } - + #processComments() { - if (!this.data.onResponseReceivedEndpoints) + if (!this.data.onResponseReceivedEndpoints) throw new Utils.UnavailableContentError('Comments section not available', this.args); - + const header = Utils.findNode(this.data, 'onResponseReceivedEndpoints', 'commentsHeaderRenderer', 5, false); const comment_count = parseInt(header.commentsHeaderRenderer.countText.runs[0].text.replace(/,/g, '')); const page_count = parseInt(comment_count / 20); - + const parseComments = (data) => { const items = Utils.findNode(data, 'onResponseReceivedEndpoints', 'commentRenderer', 4, false); - + const response = { page_count, comment_count, - items: [] + items: [] }; - + response.items = items.map((item) => { const comment = YTDataItems.CommentThread.parseItem(item); if (comment) { comment.like = () => this.session.actions.engage('comment/perform_comment_action', { comment_action: 'like', comment_id: comment.metadata.id, video_id: this.args.video_id }); comment.dislike = () => this.session.actions.engage('comment/perform_comment_action', { comment_action: 'dislike', comment_id: comment.metadata.id, video_id: this.args.video_id }); comment.reply = (text) => this.session.actions.engage('comment/create_comment_reply', { text, comment_id: comment.metadata.id, video_id: this.args.video_id }); - + comment.report = async () => { const payload = Utils.findNode(item, 'commentThreadRenderer', 'params', 10, false); const form = await this.session.actions.flag('flag/get_form', { params: payload.params }); - + const action = Utils.findNode(form, 'actions', 'flagAction', 13, false); const flag = await this.session.actions.flag('flag/flag', { action: action.flagAction }); - + return flag; }; - + comment.getReplies = async () => { if (comment.metadata.reply_count === 0) throw new Utils.InnertubeError('This comment has no replies', comment); const payload = Proto.encodeCommentRepliesParams(this.args.video_id, comment.metadata.id); const next = await this.session.actions.next({ ctoken: payload }); return parseComments(next.data); }; - + comment.translate = async (target_language) => { const response = await this.session.actions.engage('comment/perform_comment_action', { - text: comment.text, + text: comment.text, comment_action: 'translate', comment_id: comment.metadata.id, video_id: this.args.video_id, target_language }); - + const translated_content = Utils.findNode(response.data, 'frameworkUpdates', 'content', 7, false); - + return { success: response.success, status_code: response.status_code, translated_content: translated_content.content - } - } - + }; + }; + return comment; } }).filter((c) => c); - + response.comment = (text) => this.session.actions.engage('comment/create_comment', { video_id: this.args.video_id, text }); - + response.getContinuation = async () => { const continuation_item = items.find((item) => item.continuationItemRenderer); if (!continuation_item) throw new Utils.InnertubeError('You\'ve reached the end'); - + const is_reply = !!continuation_item.continuationItemRenderer.button; const payload = Utils.findNode(continuation_item, 'continuationItemRenderer', 'token', is_reply ? 5 : 3); const next = await this.session.actions.next({ ctoken: payload.token }); - + return parseComments(next.data); }; - + return response; }; - + return parseComments(this.data); } - + #processHomeFeed() { - const contents = Utils.findNode(this.data, 'contents', 'videoRenderer', 9, false) - + const contents = Utils.findNode(this.data, 'contents', 'videoRenderer', 9, false); + const parseItems = (contents) => { const videos = YTDataItems.VideoItem.parse(contents); @@ -406,46 +404,46 @@ class Parser { const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; const response = await this.session.actions.browse(ctoken, { is_ctoken: true }); - + return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems); - } + }; return { videos, getContinuation }; - } + }; return parseItems(contents); } - + #processLibrary() { // TODO: Finish this const profile_data = Utils.findNode(this.data, 'contents', 'profileColumnRenderer', 3); const stats_data = profile_data.profileColumnRenderer.items.find((item) => item.profileColumnStatsRenderer); const stats_items = stats_data.profileColumnStatsRenderer.items; const userinfo = profile_data.profileColumnRenderer.items.find((item) => item.profileColumnUserInfoRenderer); - + const stats = {}; - + stats_items.forEach((item) => { const label = item.profileColumnStatsEntryRenderer.label.runs.map((run) => run.text).join(''); stats[label.toLowerCase()] = parseInt(item.profileColumnStatsEntryRenderer.value.simpleText); }); - + const profile = { name: userinfo.profileColumnUserInfoRenderer?.title?.simpleText, thumbnails: userinfo.profileColumnUserInfoRenderer?.thumbnail.thumbnails, stats - } - - // const content = Utils.findNode(this.data, 'contents', 'content', 8, false); - // console.info(content[0].itemSectionRenderer.contents[0].shelfRenderer); - + }; + + // Const content = Utils.findNode(this.data, 'contents', 'content', 8, false); + // Console.info(content[0].itemSectionRenderer.contents[0].shelfRenderer); + return { profile - } + }; } - + #processSubscriptionFeed() { const contents = Utils.findNode(this.data, 'contents', 'contents', 9, false); - + const subsfeed = { items: [] }; const parseItems = (contents) => { @@ -455,9 +453,9 @@ class Parser { const section_contents = section.itemSectionRenderer.contents[0]; const section_title = section_contents.shelfRenderer.title.runs[0].text; const section_items = section_contents.shelfRenderer.content.gridRenderer.items; - + const items = YTDataItems.GridVideoItem.parse(section_items); - + subsfeed.items.push({ date: section_title, videos: items @@ -469,19 +467,19 @@ class Parser { const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; const response = await this.session.actions.browse(ctoken, { is_ctoken: true }); - + const ccontents = Utils.findNode(response.data, 'onResponseReceivedActions', 'itemSectionRenderer', 4, false); subsfeed.items = []; return parseItems(ccontents); - } + }; return subsfeed; }; - + return parseItems(contents); } - + #processChannel() { const tabs = this.data.contents.twoColumnBrowseResultsRenderer.tabs; const metadata = this.data.metadata; @@ -508,19 +506,19 @@ class Parser { return YTDataItems.GridPlaylistItem.parseItem(item); } }); - + home_shelves.push(shelf); } }); - + const ch_info = YTDataItems.ChannelMetadata.parse(metadata); - + return { ...ch_info, content: { // Home page of the channel, always available in the first request. home_page: home_shelves, - + // TODO: Implement these (note: they require additional requests) getVideos: () => {}, getPlaylists: () => {}, @@ -528,72 +526,72 @@ class Parser { getChannels: () => {}, getAbout: () => {} } - } + }; } - + #processNotifications() { const contents = this.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0]; if (!contents.multiPageMenuNotificationSectionRenderer) throw new Utils.InnertubeError('No notifications'); - + const parseItems = (items) => { const parsed_items = YTDataItems.NotificationItem.parse(items); - + const getContinuation = async () => { const citem = items.find((item) => item.continuationItemRenderer); const ctoken = citem?.continuationItemRenderer?.continuationEndpoint?.getNotificationMenuEndpoint?.ctoken; const response = await this.session.actions.notifications('get_notification_menu', { ctoken }); - + return parseItems(response.data.actions[0].appendContinuationItemsAction.continuationItems); - } + }; return { items: parsed_items, getContinuation }; - } - + }; + return parseItems(contents.multiPageMenuNotificationSectionRenderer.items); } - + #processTrending() { const tabs = Utils.findNode(this.data, 'contents', 'tabRenderer', 4, false); const categories = {}; - + tabs.forEach((tab) => { const tab_renderer = tab.tabRenderer; const tab_content = tab_renderer?.content; const category_title = tab_renderer.title.toLowerCase(); - + categories[category_title] = {}; - + if (tab_content) { // The “Now” category is always available const contents = tab_content.sectionListRenderer.contents; - + categories[category_title].content = contents.map((content) => { const shelf = content.itemSectionRenderer.contents[0].shelfRenderer; const parsed_shelf = YTDataItems.ShelfRenderer.parse(shelf); return parsed_shelf; }); - } else { // The rest can only be fetched with additional calls + } else { // The rest can only be fetched with additional calls const params = tab_renderer.endpoint.browseEndpoint.params; categories[category_title].getVideos = async () => { const response = await this.session.actions.browse('FEtrending', { params }); - + const tabs = Utils.findNode(response, 'contents', 'tabRenderer', 4, false); const tab = tabs.find((tab) => tab.tabRenderer.title === tab_renderer.title); - + const contents = tab.tabRenderer.content.sectionListRenderer.contents; const items = Utils.findNode(contents, 'itemSectionRenderer', 'items', 8, false); - + return YTDataItems.VideoItem.parse(items); }; } }); - + return categories; } - + #processHistory() { - const contents = Utils.findNode(this.data, 'contents', 'videoRenderer', 9, false) - + const contents = Utils.findNode(this.data, 'contents', 'videoRenderer', 9, false); + const history = { items: [] }; const parseItems = (contents) => { @@ -617,14 +615,14 @@ class Parser { const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token; const response = await this.session.actions.browse(ctoken, { is_ctoken: true }); - + history.items = []; return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems); - } + }; return history; - } + }; return parseItems(contents); } diff --git a/lib/parser/youtube/Analytics.js b/lib/parser/youtube/Analytics.js index ffccd6ba..03305e43 100644 --- a/lib/parser/youtube/Analytics.js +++ b/lib/parser/youtube/Analytics.js @@ -5,19 +5,19 @@ const Parser = require('../contents'); /** @namespace */ class Analytics { #page; - + /** * @param {object} response - API response. */ constructor(response) { this.#page = Parser.parseResponse(response); - + const tab = this.#page.contents.tabs.get({ selected: true }); const item = tab.content.contents.get({ target_id: null }); - + this.sections = item.contents; } - + get page() { return this.#page; } diff --git a/lib/parser/youtube/Channel.js b/lib/parser/youtube/Channel.js index 31d66f7c..d21d9f22 100644 --- a/lib/parser/youtube/Channel.js +++ b/lib/parser/youtube/Channel.js @@ -4,7 +4,7 @@ const TabbedFeed = require('../../core/TabbedFeed'); class Channel extends TabbedFeed { #tab; - + constructor(actions, data, already_parsed = false) { super(actions, data, already_parsed); this.header = { @@ -14,18 +14,18 @@ class Channel extends TabbedFeed { tv_banner: this.page.header.tv_banner, mobile_banner: this.page.header.mobile_banner, header_links: this.page.header.header_links - } - + }; + this.metadata = { ...this.page.metadata, ...this.page.microformat }; - + this.sponsor_button = this.page.header.sponsor_button || null; this.subscribe_button = this.page.header.subscribe_button || null; - + const tab = this.page.contents.tabs.get({ selected: true }); - + this.current_tab = tab; } - + async getVideos() { const tab = await this.getTab('Videos'); return new Channel(this.actions, tab.page, true); @@ -54,7 +54,7 @@ class Channel extends TabbedFeed { /** * Retrieves the channel about page. * Note that this does not return a new {@link Channel} object. - * + * * @returns {Promise} */ async getAbout() { diff --git a/lib/parser/youtube/Comments.js b/lib/parser/youtube/Comments.js new file mode 100644 index 00000000..722c2323 --- /dev/null +++ b/lib/parser/youtube/Comments.js @@ -0,0 +1,77 @@ +'use strict'; + +const Parser = require('../contents'); +const { InnertubeError } = require('../../utils/Utils'); + +class Comments { + #page; + #actions; + #continuation; + + constructor(actions, data, already_parsed = false) { + this.#page = already_parsed ? data : Parser.parseResponse(data); + this.#actions = actions; + + const contents = this.#page.on_response_received_endpoints; + + /** @type {import('../contents/classes/CommentsHeader')} */ + this.header = contents[0].contents.get({ type: 'CommentsHeader' }); + + const threads = contents[1].contents.findAll({ type: 'CommentThread' }); + + /** @type {import('../contents/classes/CommentThread')[]} */ + this.contents = threads.map((thread) => { + thread.comment.setActions(this.#actions); + thread.setActions(this.#actions); + return thread; + }); + + this.#continuation = contents[1].contents.get({ type: 'ContinuationItem' }); + } + + /** + * Creates a top-level comment. + * @param {string} text + * @returns {Promise.<{ success: boolean, status_code: number, data: object }>} + */ + async comment(text) { + const button = this.header.create_renderer.submit_button; + + const payload = { + params: { + commentText: text + }, + parse: false + }; + + const response = await button.endpoint.callTest(this.#actions, payload); + + return response; + } + + /** + * Retrieves next batch of comments. + * @returns {Promise.} + */ + async getContinuation() { + if (!this.#continuation) + throw new InnertubeError('Continuation not found'); + + const data = await this.#continuation.endpoint.callTest(this.#actions); + + // Copy the previous page so we can keep the header. + const page = Object.assign({}, this.#page); + + // Remove previous items and append the continuation. + page.on_response_received_endpoints.pop(); + page.on_response_received_endpoints.push(data.on_response_received_endpoints[0]); + + return new Comments(this.#actions, page, true); + } + + get page() { + return this.#page; + } +} + +module.exports = Comments; \ No newline at end of file diff --git a/lib/parser/youtube/History.js b/lib/parser/youtube/History.js index 848e310c..a4ff70ac 100644 --- a/lib/parser/youtube/History.js +++ b/lib/parser/youtube/History.js @@ -12,15 +12,15 @@ class History extends Feed { * @param {boolean} already_parsed */ constructor(actions, data, already_parsed = false) { - super(actions, data, already_parsed) - + super(actions, data, already_parsed); + this.sections = this.memo.get('ItemSection'); this.feed_actions = this.memo.get('BrowseFeedActions')?.[0] || []; } - + /** * Retrieves next batch of contents. - * + * * @returns {Promise.} */ async getContinuation() { diff --git a/lib/parser/youtube/Library.js b/lib/parser/youtube/Library.js index 2032fc86..21ed3f70 100644 --- a/lib/parser/youtube/Library.js +++ b/lib/parser/youtube/Library.js @@ -10,7 +10,7 @@ const { observe } = require('../../utils/Utils'); class Library { #actions; #page; - + /** * @param {object} response - API response. * @param {import('../../core/Actions')} actions @@ -18,15 +18,15 @@ class Library { constructor(response, actions) { this.#actions = actions; this.#page = Parser.parseResponse(response); - + const tab = this.#page.contents.tabs.get({ selected: true }); const shelves = tab.content.contents.map((section) => section.contents[0]); - + const stats = this.#page.contents.secondary_contents.items.get({ type: 'ProfileColumnStats' }).items; const user_info = this.#page.contents.secondary_contents.items.get({ type: 'ProfileColumnUserInfo' }); - + this.profile = { stats, user_info }; - + /** @type {{ type: string, title: import('../contents/classes/Text'), contents: object[], getAll: Promise. }[] } */ this.sections = observe(shelves.map((shelf) => ({ type: shelf.icon_type, @@ -35,16 +35,16 @@ class Library { getAll: () => this.#getAll(shelf) }))); } - + async #getAll(shelf) { - if (!shelf.menu?.top_level_buttons) - throw new Error('The ' + shelf.title.text + ' section doesn\'t have more items'); - + if (!shelf.menu?.top_level_buttons) + throw new Error(`The ${shelf.title.text} section doesn't have more items`); + const button = await shelf.menu.top_level_buttons.get({ text: 'See all' }); const page = await button.endpoint.call(this.#actions); - + switch (shelf.icon_type) { - case 'LIKE': + case 'LIKE': case 'WATCH_LATER': return new Playlist(this.#actions, page, true); case 'WATCH_HISTORY': @@ -54,27 +54,27 @@ class Library { default: } } - + get history() { - return this.sections.get({ type: 'WATCH_HISTORY' }) + return this.sections.get({ type: 'WATCH_HISTORY' }); } - + get watch_later() { return this.sections.get({ type: 'WATCH_LATER' }); } - + get liked_videos() { return this.sections.get({ type: 'LIKE' }); } - + get playlists() { return this.sections.get({ type: 'PLAYLISTS' }); } - + get clips() { return this.sections.get({ type: 'CONTENT_CUT' }); } - + get page() { return this.#page; } diff --git a/lib/parser/youtube/LiveChat.js b/lib/parser/youtube/LiveChat.js index de193e6e..5b7df367 100644 --- a/lib/parser/youtube/LiveChat.js +++ b/lib/parser/youtube/LiveChat.js @@ -60,7 +60,7 @@ class LiveChat { #pollLivechat() { const lc_poller = setTimeout(() => { (async () => { - const endpoint = this.is_replay ? 'live_chat/get_live_chat_replay': 'live_chat/get_live_chat'; + const endpoint = this.is_replay ? 'live_chat/get_live_chat_replay' : 'live_chat/get_live_chat'; const response = await this.#actions.livechat(endpoint, { ctoken: this.#continuation }); const data = Parser.parseResponse(response.data); @@ -90,8 +90,8 @@ class LiveChat { * @param {object} actions */ async #emitSmoothedActions(actions) { - let base = 1E4; - let delay = actions.length < base / 80 ? 1: 0; + const base = 1E4; + let delay = actions.length < base / 80 ? 1 : 0; const emit_delay_ms = delay == 1 ? ( @@ -99,7 +99,7 @@ class LiveChat { delay *= Math.random() + 0.5, delay = Math.min(1E3, delay), delay = Math.max(80, delay) - ): delay = 80; + ) : delay = 80; for (const action of actions) { await this.#wait(emit_delay_ms); @@ -113,8 +113,8 @@ class LiveChat { const payload = { video_id: this.#video_info.basic_info.id }; if (this.#mcontinuation) { - payload.ctoken = this.#mcontinuation - } + payload.ctoken = this.#mcontinuation; + } const response = await this.#actions.livechat('updated_metadata', payload); const data = Parser.parseResponse(response.data); diff --git a/lib/parser/youtube/Playlist.js b/lib/parser/youtube/Playlist.js index 15c10340..67bfa486 100644 --- a/lib/parser/youtube/Playlist.js +++ b/lib/parser/youtube/Playlist.js @@ -1,14 +1,14 @@ -'use strict' +'use strict'; const Feed = require('../../core/Feed'); class Playlist extends Feed { constructor(actions, data, already_parsed = false) { super(actions, data, already_parsed); - + const primary_info = this.page.sidebar.contents.get({ type: 'PlaylistSidebarPrimaryInfo' }); const secondary_info = this.page.sidebar.contents.get({ type: 'PlaylistSidebarSecondaryInfo' }); - + this.info = { ...this.page.metadata, ...{ @@ -22,8 +22,8 @@ class Playlist extends Feed { is_editable: this.page.header.is_editable, privacy: this.page.header.privacy } - } - + }; + this.menu = primary_info.menu; this.endpoint = primary_info.endpoint; } diff --git a/lib/parser/youtube/Search.js b/lib/parser/youtube/Search.js index 2639a1d1..862fbd8f 100644 --- a/lib/parser/youtube/Search.js +++ b/lib/parser/youtube/Search.js @@ -2,7 +2,7 @@ const Feed = require('../../core/Feed'); const { InnertubeError } = require('../../utils/Utils'); - + /** @namespace */ class Search extends Feed { /** @@ -12,21 +12,21 @@ class Search extends Feed { */ constructor(actions, data, already_parsed = false) { super(actions, data, already_parsed); - + const contents = this.page.contents?.primary_contents.contents || this.page.on_response_received_commands[0].contents; - + const secondary_contents = this.page.contents?.secondary_contents?.contents; - + /** @type {object[]} */ this.results = contents.get({ type: 'ItemSection' }).contents; - + const card_list = this.results.get({ type: 'HorizontalCardList' }, true); const universal_watch_card = secondary_contents?.get({ type: 'UniversalWatchCard' }); - + this.refinements = this.page.refinements || []; this.estimated_results = this.page.estimated_results; - + this.watch_card = { /** @type {import('../contents/classes/UniversalWatchCard')} */ header: universal_watch_card?.header || null, @@ -34,16 +34,16 @@ class Search extends Feed { call_to_action: universal_watch_card?.call_to_action || null, /** @type {import('../contents/classes/WatchCardSectionSequence')[]} */ sections: universal_watch_card?.sections || [] - } - + }; + this.refinement_cards = { /** @type {import('../contents/classes/RichListHeader')} */ header: card_list?.header || null, /** @type {import('../contents/classes/SearchRefinementCard')} */ cards: card_list?.cards || [] - } + }; } - + /** * Applies given refinement card and returns a new {@link Search} object. * @@ -52,7 +52,7 @@ class Search extends Feed { */ async selectRefinementCard(card) { let target_card; - + if (typeof card === 'string') { target_card = this.refinement_cards.cards.get({ query: card }); if (!target_card) @@ -64,19 +64,19 @@ class Search extends Feed { } else { throw new InnertubeError('Invalid refinement card!'); } - + const page = await target_card.endpoint.call(this.actions); return new Search(this.actions, page, true); } - + /** @type {string[]} */ get refinement_card_queries() { return this.refinement_cards.cards.map((card) => card.query); } - + /** * Retrieves next batch of results. - * + * * @returns {Promise.} */ async getContinuation() { diff --git a/lib/parser/youtube/VideoInfo.js b/lib/parser/youtube/VideoInfo.js index 6b487b24..e3801f95 100644 --- a/lib/parser/youtube/VideoInfo.js +++ b/lib/parser/youtube/VideoInfo.js @@ -9,7 +9,7 @@ const LiveChat = require('./LiveChat'); const Constants = require('../../utils/Constants'); const CancelToken = Axios.CancelToken; -/** namespace */ +/** Namespace */ class VideoInfo { #page; #actions; @@ -28,15 +28,15 @@ class VideoInfo { this.#actions = actions; this.#player = player; this.#cpn = cpn; - + const info = Parser.parseResponse(data[0]); const next = Parser.parseResponse(data[1].data || {}); - this.#page = [info, next]; + this.#page = [ info, next ]; if (info.playability_status.status === 'ERROR') throw new InnertubeError('This video is unavailable', info.playability_status); - + /** * @type {import('../contents/classes/VideoDetails')} */ @@ -54,7 +54,7 @@ class VideoInfo { has_ypc_metadata: info.microformat.has_ypc_metadata } }; - + this.streaming_data = info.streaming_data || null; this.playability_status = info.playability_status; @@ -67,7 +67,7 @@ class VideoInfo { * @type {import('../contents/classes/PlayerStoryboardSpec')} */ this.storyboards = info.storyboards; - + /** * @type {import('../contents/classes/Endscreen')} */ @@ -85,7 +85,7 @@ class VideoInfo { const results = next.contents?.results; const secondary_results = next.contents?.secondary_results; - + if (results && secondary_results) { /** * @type {import('../contents/classes/VideoPrimaryInfo')} @@ -101,7 +101,7 @@ class VideoInfo { * @type {import('../contents/classes/MerchandiseShelf')} */ this.merchandise = results?.get({ type: 'MerchandiseShelf' }) || null; - + /** * @type {import('../contents/classes/ChipCloud')} */ @@ -118,14 +118,14 @@ class VideoInfo { this.basic_info.like_count = this.primary_info.menu.top_level_buttons.get({ icon_type: 'LIKE' }).like_count; this.basic_info.is_liked = this.primary_info.menu.top_level_buttons.get({ icon_type: 'LIKE' }).is_toggled; this.basic_info.is_disliked = this.primary_info.menu.top_level_buttons.get({ icon_type: 'DISLIKE' }).is_toggled; - + const comments_entry_point = results.get({ target_id: 'comments-entry-point' }); /** * @type {import('../contents/classes/CommentsEntryPointHeader')} */ this.comments_entry_point_header = comments_entry_point?.contents.get({ type: 'CommentsEntryPointHeader' }) || null; - + /** * @type {import('../contents/classes/LiveChat')} */ @@ -153,10 +153,10 @@ class VideoInfo { return this; } - + /** @typedef {import('../contents/classes/CompactVideo')} CompactVideo */ /** @typedef {import('../contents/classes/CompactMix')} CompactMix */ - + /** * Retrieves watch next feed continuation. * @@ -219,7 +219,7 @@ class VideoInfo { return response; } - + /** * Retrieves Live Chat if available. * @param {string} [mode] - livechat mode @@ -228,19 +228,19 @@ class VideoInfo { async getLiveChat(mode) { if (!this.livechat) throw new InnertubeError('Live Chat is not available', { video_id: this.id }); - + return new LiveChat(this, mode); } - + /** @type {string[]} */ get filters() { return this.related_chip_cloud?.chips.map((chip) => chip.text.toString()) || []; } - + get actions() { return this.#actions; } - + get page() { return this.#page; } @@ -254,96 +254,96 @@ class VideoInfo { */ const metadata = this.secondary_info.metadata; if (!metadata) return []; - + const songs = []; - + let current_song = {}; let is_music_section = false; - + for (let i = 0; i < metadata.rows.length; i++) { const row = metadata.rows[i]; - + if (row.type === 'MetadataRowHeader') { if (row.content.toString().toLowerCase().startsWith('music')) { is_music_section = true; - i++; // skip the learn more link + i++; // Skip the learn more link } continue; } - + if (!is_music_section) continue; - + current_song[row.title.toString().toLowerCase().replace(/ /g, '_')] = row.contents; - + if (row.has_divider_line) { songs.push(current_song); current_song = {}; } } - + if (is_music_section) songs.push(current_song); - + return songs; } chooseFormat(options) { - let formats = [ - ...(this.streaming_data.formats || []), + const formats = [ + ...(this.streaming_data.formats || []), ...(this.streaming_data.adaptive_formats || []) ]; const requires_audio = options.type.includes('audio'); const requires_video = options.type.includes('video'); - + let best_width = -1; - - const is_best = ['best','bestefficiency'].includes(options.quality); - + + const is_best = [ 'best', 'bestefficiency' ].includes(options.quality); + const use_most_efficient = options.quality !== 'best'; - - let candidates = formats.filter(format => { - if (requires_audio && !format.has_audio) + + let candidates = formats.filter((format) => { + if (requires_audio && !format.has_audio) return false; - if (requires_video && !format.has_video) + if (requires_video && !format.has_video) return false; - if (options.format !== 'any' && !format.mime_type.includes(options.format)) + if (options.format !== 'any' && !format.mime_type.includes(options.format)) return false; - if (!is_best && format.quality_label !== options.quality) + if (!is_best && format.quality_label !== options.quality) return false; - if (best_width < format.width) + if (best_width < format.width) best_width = format.width; return true; }); if (candidates.length === 0) { throw new InnertubeError('No matching formats found', { - options + options }); } if (is_best && requires_video) - candidates = candidates.filter(format => format.width === best_width); - + candidates = candidates.filter((format) => format.width === best_width); + if (requires_audio && !requires_video) { - const audio_only = candidates.filter(format => !format.has_video); + const audio_only = candidates.filter((format) => !format.has_video); if (audio_only.length > 0) { candidates = audio_only; } } if (use_most_efficient) { - // sort by bitrate (lower is better) + // Sort by bitrate (lower is better) candidates.sort((a, b) => a.bitrate - b.bitrate); } else { - // sort by bitrate (higher is better) + // Sort by bitrate (higher is better) candidates.sort((a, b) => b.bitrate - a.bitrate); } - + return candidates[0]; } /** - * + * * @param {object} options - download options. * @param {string} [options.quality] - video quality; 360p, 720p, 1080p, etc... also accepts 'best' and 'bestefficiency'. * @param {string} [options.type] - download type, can be: video, audio or videoandaudio @@ -361,19 +361,19 @@ class VideoInfo { let cancelled = false; (async () => { - if (this.playability_status === 'UNPLAYABLE') + if (this.playability_status === 'UNPLAYABLE') return stream.emit('error', new InnertubeError('Video is unplayable', { video: this, error_type: 'UNPLAYABLE' })); if (this.playability_status === 'LOGIN_REQUIRED') return stream.emit('error', new InnertubeError('Video is login required', { video: this, error_type: 'LOGIN_REQUIRED' })); if (!this.streaming_data) return stream.emit('error', new InnertubeError('Streaming data not available.', { video: this, error_type: 'NO_STREAMING_DATA' })); - - const opts = { + + const opts = { quality: '360p', type: 'videoandaudio', format: 'mp4', range: undefined, - ...options + ...options }; const format = this.chooseFormat(opts); @@ -383,24 +383,26 @@ class VideoInfo { if (opts.type === 'videoandaudio' && !options.range) { const response = await Axios.get(`${format_url}&cpn=${this.#cpn}`, { responseType: 'stream', - cancelToken: new CancelToken(function executor(c) { cancel = c; }), + cancelToken: new CancelToken(function executor(c) { + cancel = c; + }), headers: Constants.STREAM_HEADERS }).catch((error) => error); if (response instanceof Error) { stream.emit('error', new InnertubeError(response.message, { type: 'REQUEST_FAILED' })); return stream; - } else { - stream.emit('start'); } + stream.emit('start'); + let downloaded_size = 0; response.data.on('data', (chunk) => { downloaded_size += chunk.length; - - let size = (response.headers['content-length'] / 1024 / 1024).toFixed(2); - let percentage = Math.floor((downloaded_size / response.headers['content-length']) * 100); + + const size = (response.headers['content-length'] / 1024 / 1024).toFixed(2); + const percentage = Math.floor((downloaded_size / response.headers['content-length']) * 100); stream.emit('progress', { size, @@ -431,18 +433,20 @@ class VideoInfo { let must_end = false; stream.emit('start'); - + const downloadChunk = async () => { if (chunk_end >= format.content_length || options.range) { must_end = true; } if (options.range) { - format.content_length = options.range.end + format.content_length = options.range.end; } - + const response = await Axios.get(`${format_url}&cpn=${this.#cpn}&range=${chunk_start}-${chunk_end || ''}`, { responseType: 'stream', - cancelToken: new CancelToken(function executor(c) { cancel = c; }), + cancelToken: new CancelToken(function executor(c) { + cancel = c; + }), headers: Constants.STREAM_HEADERS }).catch((error) => error); @@ -454,8 +458,8 @@ class VideoInfo { response.data.on('data', (chunk) => { downloaded_size += chunk.length; - let size = (format.content_length / 1024 / 1024).toFixed(2); - let percentage = Math.floor((downloaded_size / format.content_length) * 100); + const size = (format.content_length / 1024 / 1024).toFixed(2); + const percentage = Math.floor((downloaded_size / format.content_length) * 100); stream.emit('progress', { size, @@ -490,15 +494,15 @@ class VideoInfo { downloadChunk(); } - })().catch(err => { + })().catch((err) => { stream.emit('error', err); - }) + }); stream.cancel = () => { cancelled = true; cancel && cancel(); }; - + return stream; } } diff --git a/lib/parser/youtube/others/ChannelMetadata.js b/lib/parser/youtube/others/ChannelMetadata.js index a6de2892..08c755e5 100644 --- a/lib/parser/youtube/others/ChannelMetadata.js +++ b/lib/parser/youtube/others/ChannelMetadata.js @@ -13,7 +13,7 @@ class ChannelMetadata { is_family_safe: data.channelMetadataRenderer?.isFamilySafe, keywords: data.channelMetadataRenderer?.keywords } - } + }; } } diff --git a/lib/parser/youtube/others/CommentThread.js b/lib/parser/youtube/others/CommentThread.js index 0523fa03..3fd1f9e2 100644 --- a/lib/parser/youtube/others/CommentThread.js +++ b/lib/parser/youtube/others/CommentThread.js @@ -6,7 +6,7 @@ class CommentThread { static parseItem(item) { if (item.commentThreadRenderer || item.commentRenderer) { const comment = item?.commentThreadRenderer?.comment || item; - + const like_btn = comment.commentRenderer?.actionButtons?.commentActionButtonsRenderer.likeButton; const dislike_btn = comment.commentRenderer?.actionButtons?.commentActionButtonsRenderer.dislikeButton; @@ -16,20 +16,20 @@ class CommentThread { name: comment.commentRenderer.authorText.simpleText, thumbnails: comment.commentRenderer.authorThumbnail.thumbnails, channel_id: comment.commentRenderer.authorEndpoint.browseEndpoint.browseId, - channel_url: Constants.URLS.YT_BASE + comment.commentRenderer.authorEndpoint.browseEndpoint.canonicalBaseUrl + channel_url: Constants.URLS.YT_BASE + comment.commentRenderer.authorEndpoint.browseEndpoint.canonicalBaseUrl }, metadata: { published: comment.commentRenderer.publishedTimeText.runs[0].text, is_reply: !!item.commentRenderer, is_liked: like_btn.toggleButtonRenderer.isToggled, is_disliked: dislike_btn.toggleButtonRenderer.isToggled, - is_pinned: comment.commentRenderer.pinnedCommentBadge ? true : false, + is_pinned: !!comment.commentRenderer.pinnedCommentBadge, is_channel_owner: comment.commentRenderer.authorIsChannelOwner, like_count: parseInt(like_btn?.toggleButtonRenderer?.accessibilityData?.accessibilityData.label.replace(/\D/g, '')), reply_count: comment.commentRenderer.replyCount || 0, - id: comment.commentRenderer.commentId, + id: comment.commentRenderer.commentId } - } + }; } } } diff --git a/lib/parser/youtube/others/GridPlaylistItem.js b/lib/parser/youtube/others/GridPlaylistItem.js index f6e9f4c4..d4b7f559 100644 --- a/lib/parser/youtube/others/GridPlaylistItem.js +++ b/lib/parser/youtube/others/GridPlaylistItem.js @@ -2,16 +2,16 @@ class GridPlaylistItem { static parse(data) { - return data.map((item) => this.parseItem(item)).filter((item) => item); + return data.map((item) => this.parseItem(item)).filter((item) => item); } - + static parseItem(item) { return { id: item?.gridPlaylistRenderer.playlistId, title: item?.gridPlaylistRenderer.title?.runs?.map((run) => run.text).join(''), metadata: { thumbnail: item?.gridPlaylistRenderer.thumbnail?.thumbnails?.slice(-1)[0] || {}, - video_count: item?.gridPlaylistRenderer.videoCountShortText?.simpleText || 'N/A', + video_count: item?.gridPlaylistRenderer.videoCountShortText?.simpleText || 'N/A' } }; } diff --git a/lib/parser/youtube/others/GridVideoItem.js b/lib/parser/youtube/others/GridVideoItem.js index f0e0ba8d..3e95fb4e 100644 --- a/lib/parser/youtube/others/GridVideoItem.js +++ b/lib/parser/youtube/others/GridVideoItem.js @@ -4,9 +4,9 @@ const Constants = require('../../../utils/Constants'); class GridVideoItem { static parse(data) { - return data.map((item) => this.parseItem(item)).filter((item) => item); + return data.map((item) => this.parseItem(item)).filter((item) => item); } - + static parseItem(item) { return { id: item.gridVideoRenderer.videoId, @@ -20,7 +20,7 @@ class GridVideoItem { view_count: item?.gridVideoRenderer?.viewCountText?.simpleText || 'N/A', short_view_count_text: { simple_text: item?.gridVideoRenderer?.shortViewCountText?.simpleText || 'N/A', - accessibility_label: item?.gridVideoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A', + accessibility_label: item?.gridVideoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A' }, thumbnail: item?.gridVideoRenderer?.thumbnail?.thumbnails.slice(-1)[0] || [], moving_thumbnail: item?.gridVideoRenderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || {}, diff --git a/lib/parser/youtube/others/NotificationItem.js b/lib/parser/youtube/others/NotificationItem.js index 9a21ed21..46241c7e 100644 --- a/lib/parser/youtube/others/NotificationItem.js +++ b/lib/parser/youtube/others/NotificationItem.js @@ -2,13 +2,13 @@ class NotificationItem { static parse(data) { - return data.map((item) => this.parseItem(item)).filter((item) => item); + return data.map((item) => this.parseItem(item)).filter((item) => item); } - + static parseItem(item) { if (item.notificationRenderer) { const notification = item.notificationRenderer; - + return { title: notification?.shortMessage?.simpleText, sent_time: notification?.sentTimeText?.simpleText, diff --git a/lib/parser/youtube/others/PlaylistItem.js b/lib/parser/youtube/others/PlaylistItem.js index daa8aacb..b882f9cb 100644 --- a/lib/parser/youtube/others/PlaylistItem.js +++ b/lib/parser/youtube/others/PlaylistItem.js @@ -4,9 +4,9 @@ const Utils = require('../../../utils/Utils'); class PlaylistItem { static parse(data) { - return data.map((item) => this.parseItem(item)).filter((item) => item); + return data.map((item) => this.parseItem(item)).filter((item) => item); } - + static parseItem(item) { if (item.playlistVideoRenderer) return { @@ -18,7 +18,7 @@ class PlaylistItem { simple_text: item?.playlistVideoRenderer?.lengthText?.simpleText || 'N/A', accessibility_label: item?.playlistVideoRenderer?.lengthText?.accessibility?.accessibilityData?.label || 'N/A' }, - thumbnails: item?.playlistVideoRenderer?.thumbnail?.thumbnails, + thumbnails: item?.playlistVideoRenderer?.thumbnail?.thumbnails }; } } diff --git a/lib/parser/youtube/others/ShelfRenderer.js b/lib/parser/youtube/others/ShelfRenderer.js index 279a1de6..11fb1f14 100644 --- a/lib/parser/youtube/others/ShelfRenderer.js +++ b/lib/parser/youtube/others/ShelfRenderer.js @@ -8,32 +8,32 @@ class ShelfRenderer { return { title: this.getTitle(data.title), videos: this.parseItems(data.content) - } + }; } - + static getTitle(data) { if ('runs' in (data || {})) { return data.runs.map((run) => run.text).join(''); } else if ('simpleText' in (data || {})) { return data.simpleText; - } else { - return 'Others'; } + return 'Others'; + } - + static parseItems(data) { let items; - + if ('expandedShelfContentsRenderer' in data) { items = data.expandedShelfContentsRenderer.items; } else if ('horizontalListRenderer' in data) { items = data.horizontalListRenderer.items; - } - + } + const videos = ('gridVideoRenderer' in items[0]) - && GridVideoItem.parse(items) + && GridVideoItem.parse(items) || VideoItem.parse(items); - + return videos; } } diff --git a/lib/parser/youtube/others/VideoItem.js b/lib/parser/youtube/others/VideoItem.js index 2e1768e0..3005e639 100644 --- a/lib/parser/youtube/others/VideoItem.js +++ b/lib/parser/youtube/others/VideoItem.js @@ -5,14 +5,14 @@ const Constants = require('../../../utils/Constants'); class VideoItem { static parse(data) { - return data.map((item) => this.parseItem(item)).filter((item) => item); + return data.map((item) => this.parseItem(item)).filter((item) => item); } - + static parseItem(item) { item = (item.richItemRenderer && item.richItemRenderer.content.videoRenderer) && item.richItemRenderer.content || item; - + if (item.videoRenderer) return { id: item.videoRenderer.videoId, title: item.videoRenderer.title.runs.map((run) => run.text).join(' '), @@ -26,7 +26,7 @@ class VideoItem { view_count: item?.videoRenderer?.viewCountText?.simpleText || 'N/A', short_view_count_text: { simple_text: item?.videoRenderer?.shortViewCountText?.simpleText || 'N/A', - accessibility_label: item?.videoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A', + accessibility_label: item?.videoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A' }, thumbnail: item?.videoRenderer?.thumbnail?.thumbnails.slice(-1)[0] || {}, moving_thumbnail: item?.videoRenderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || {}, @@ -39,7 +39,7 @@ class VideoItem { badges: item?.videoRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.label) || [], owner_badges: item?.videoRenderer?.ownerBadges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || [] } - } + }; } } diff --git a/lib/parser/youtube/search/SearchSuggestionItem.js b/lib/parser/youtube/search/SearchSuggestionItem.js index fcdcc6e2..2a5e039e 100644 --- a/lib/parser/youtube/search/SearchSuggestionItem.js +++ b/lib/parser/youtube/search/SearchSuggestionItem.js @@ -5,7 +5,7 @@ class SearchSuggestionItem { return { query: data[0], results: data[1].map((res) => res[0]) - } + }; } } diff --git a/lib/parser/youtube/search/VideoResultItem.js b/lib/parser/youtube/search/VideoResultItem.js index 6462f732..5b87b0b3 100644 --- a/lib/parser/youtube/search/VideoResultItem.js +++ b/lib/parser/youtube/search/VideoResultItem.js @@ -5,9 +5,9 @@ const Constants = require('../../../utils/Constants'); class VideoResultItem { static parse(data) { - return data.map((item) => this.parseItem(item)).filter((item) => item); + return data.map((item) => this.parseItem(item)).filter((item) => item); } - + static parseItem(item) { const renderer = item.videoRenderer || item.compactVideoRenderer; if (renderer) return { @@ -24,7 +24,7 @@ class VideoResultItem { view_count: renderer?.viewCountText?.simpleText || 'N/A', short_view_count_text: { simple_text: renderer?.shortViewCountText?.simpleText || 'N/A', - accessibility_label: renderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A', + accessibility_label: renderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A' }, thumbnails: renderer?.thumbnail.thumbnails, duration: { diff --git a/lib/parser/ytmusic/Album.js b/lib/parser/ytmusic/Album.js index 2cbc5b10..f6da1fd7 100644 --- a/lib/parser/ytmusic/Album.js +++ b/lib/parser/ytmusic/Album.js @@ -6,7 +6,7 @@ const Parser = require('../contents'); class Album { #page; #actions; - + /** * @param {object} response - API response. * @param {import('../../core/Actions')} actions @@ -14,18 +14,18 @@ class Album { constructor(response, actions) { this.#page = Parser.parseResponse(response.data); this.#actions = actions; - + /** @type {import('../contents/classes/MusicDetailHeader')[]} */ this.header = this.#page.header; - + /** @type {string} */ this.url = this.#page.microformat.url_canonical; - + /** @type {import('../contents/classes/MusicResponsiveListItem')[]} */ this.contents = this.#page.contents_memo.get('MusicShelf')?.[0].contents; this.sections = this.#page.contents_memo.get('MusicCarouselShelf') || []; } - + get page() { return this.#page; } diff --git a/lib/parser/ytmusic/Artist.js b/lib/parser/ytmusic/Artist.js index 640054b3..c266b6c5 100644 --- a/lib/parser/ytmusic/Artist.js +++ b/lib/parser/ytmusic/Artist.js @@ -7,7 +7,7 @@ const { observe } = require('../../utils/Utils'); class Artist { #page; #actions; - + /** * @param {object} response - API response. * @param {import('../../core/Actions')} actions @@ -15,22 +15,22 @@ class Artist { constructor(response, actions) { this.#page = Parser.parseResponse(response.data); this.#actions = actions; - + this.header = this.page.header; - + const music_shelf = this.#page.contents_memo.get('MusicShelf'); const music_carousel_shelf = this.#page.contents_memo.get('MusicCarouselShelf'); - + /** @type {import('../contents/classes/MusicShelf')[] | import('../contents/classes/MusicCarouselShelf')[]} */ this.sections = observe([ ...music_shelf, ...music_carousel_shelf ]); } - + async getAllSongs() { const shelf = this.sections.get({ type: 'MusicShelf' }); const page = await shelf.endpoint.call(this.#actions, 'YTMUSIC'); return page.contents_memo.get('MusicPlaylistShelf')?.[0] || []; } - + get page() { return this.#page; } diff --git a/lib/parser/ytmusic/Explore.js b/lib/parser/ytmusic/Explore.js index 6219026f..c58aa27f 100644 --- a/lib/parser/ytmusic/Explore.js +++ b/lib/parser/ytmusic/Explore.js @@ -5,19 +5,19 @@ const Parser = require('../contents'); /** @namespace */ class Explore { #page; - + /** * @param {object} response - API response. */ constructor(response) { this.#page = Parser.parseResponse(response.data); - + const tab = this.page.contents.tabs.get({ selected: true }); - + this.top_buttons = tab.content.contents.get({ type: 'Grid' }).items; this.sections = tab.content.contents.findAll({ type: 'MusicCarouselShelf' }); } - + get page() { return this.#page; } diff --git a/lib/parser/ytmusic/HomeFeed.js b/lib/parser/ytmusic/HomeFeed.js index 5fcd94cc..240d76ad 100644 --- a/lib/parser/ytmusic/HomeFeed.js +++ b/lib/parser/ytmusic/HomeFeed.js @@ -7,7 +7,7 @@ class HomeFeed { #page; #actions; #continuation; - + /** * @param {object} response - API response. * @param {import('../../core/Actions')} actions @@ -15,15 +15,15 @@ class HomeFeed { constructor(response, actions) { this.#actions = actions; this.#page = Parser.parseResponse(response.data); - + const tab = this.#page.contents.tabs.get({ title: 'Home' }); - + this.#continuation = tab.content?.continuation || this.#page.continuation_contents.continuation; - + /** @type {import('../contents/classes/MusicCarouselShelf')[]} */ this.sections = tab.content?.contents || this.#page.continuation_contents.contents; } - + /** * Retrieves home feed continuation. * diff --git a/lib/parser/ytmusic/Library.js b/lib/parser/ytmusic/Library.js index ba2bdd57..3cfe127f 100644 --- a/lib/parser/ytmusic/Library.js +++ b/lib/parser/ytmusic/Library.js @@ -1,12 +1,12 @@ 'use strict'; const Parser = require('../contents'); -// const { observe, InnertubeError } = require('../../utils/Utils'); +// Const { observe, InnertubeError } = require('../../utils/Utils'); /** @namespace */ class Library { #page; - + /** * @param {object} response - API response. */ @@ -14,7 +14,7 @@ class Library { this.#page = Parser.parseResponse(response.data); // TODO: finish this } - + get page() { return this.#page; } diff --git a/lib/parser/ytmusic/Search.js b/lib/parser/ytmusic/Search.js index d5fe4da1..d1ee147b 100644 --- a/lib/parser/ytmusic/Search.js +++ b/lib/parser/ytmusic/Search.js @@ -9,7 +9,7 @@ class Search { #actions; #continuation; #header; - + /** * @param {object} response - API response. * @param {import('../../core/Actions')} actions @@ -19,39 +19,39 @@ class Search { */ constructor(response, actions, args = {}) { this.#actions = actions; - + this.#page = args.is_continuation - && response + && response || Parser.parseResponse(response.data); - + const tab = this.#page.contents.tabs.get({ selected: true }); const shelves = tab.content.contents; - + const item_section = shelves.get({ type: 'ItemSection' }); - + this.#header = tab.content.header; - + /** * @type {import('../contents/classes/DidYouMean')} */ this.did_you_mean = item_section?.contents.get({ type: 'DidYouMean' }) || null; - + /** * @type {import('../contents/classes/ShowingResultsFor')} */ this.showing_results_for = item_section?.contents.get({ type: 'ShowingResultsFor' }) || null; - + (!!this.did_you_mean || !!this.showing_results_for) && shelves.shift(); - + if (args.is_continuation || args.is_filtered) { const shelf = shelves.get({ type: 'MusicShelf' }); - + this.results = shelf.contents; this.#continuation = shelf.continuation; - + return; } - + /** @type {{ title: string; items: object[]; getMore: Promise.; }} */ this.sections = observe(shelves.map((shelf) => ({ title: shelf.title, @@ -59,15 +59,15 @@ class Search { getMore: () => this.#getMore(shelf) }))); } - + async #getMore(shelf) { if (!shelf.endpoint) - throw new InnertubeError(shelf.title + ' doesn\'t have more items'); - + throw new InnertubeError(`${shelf.title} doesn't have more items`); + const response = await shelf.endpoint.call(this.#actions, 'YTMUSIC'); return new Search(response, this.#actions, { is_continuation: true }); } - + /** * Retrieves continuation, only works for individual sections or filtered results. * @@ -76,16 +76,16 @@ class Search { async getContinuation() { if (!this.#continuation) throw new InnertubeError('Looks like you\'ve reached the end'); - + const response = await this.#actions.search({ ctoken: this.#continuation, client: 'YTMUSIC' }); const data = response.data.continuationContents.musicShelfContinuation; - + this.results = Parser.parse(data.contents); this.#continuation = data?.continuations?.[0]?.nextContinuationData?.continuation; - + return this; } - + /** * Applies given filter to the search. * @@ -95,50 +95,50 @@ class Search { async selectFilter(name) { if (!this.filters.includes(name)) throw new InnertubeError('Invalid filter', { available_filters: this.filters }); - + const filter = this.#header.chips.get({ text: name }); if (filter.is_selected) return this; - + const response = await filter.endpoint.call(this.#actions, 'YTMUSIC'); - + return new Search(response, this.#actions, { is_continuation: true }); } - + /** @type {boolean} */ get has_continuation() { return !!this.#continuation; } - + /** @type {string[]} */ get filters() { return this.#header.chips.map((chip) => chip.text); } - + /** @type {import('../contents/classes/MusicResponsiveListItem')[]} */ get songs() { return this.sections.get({ title: 'Songs' }); } - + /** @type {import('../contents/classes/MusicResponsiveListItem')[]} */ get videos() { return this.sections.get({ title: 'Videos' }); } - + /** @type {import('../contents/classes/MusicResponsiveListItem')[]} */ get albums() { return this.sections.get({ title: 'Albums' }); } - + /** @type {import('../contents/classes/MusicResponsiveListItem')[]} */ get artists() { return this.sections.get({ title: 'Artists' }); } - + /** @type {import('../contents/classes/MusicResponsiveListItem')[]} */ get playlists() { return this.sections.get({ title: 'Community playlists' }); } - + get page() { return this.#page; } diff --git a/lib/parser/ytmusic/others/PlaylistItem.js b/lib/parser/ytmusic/others/PlaylistItem.js index 16c35f98..a5710fbd 100644 --- a/lib/parser/ytmusic/others/PlaylistItem.js +++ b/lib/parser/ytmusic/others/PlaylistItem.js @@ -4,9 +4,9 @@ const Utils = require('../../../utils/Utils'); class PlaylistItem { static parse(data) { - return data.map((item) => this.parseItem(item)).filter((item) => item.id); + return data.map((item) => this.parseItem(item)).filter((item) => item.id); } - + static parseItem(item) { const item_renderer = item.musicResponsiveListItemRenderer; const fixed_columns = item_renderer.fixedColumns; @@ -18,10 +18,10 @@ class PlaylistItem { author: flex_columns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text, duration: { seconds: Utils.timeToSeconds(fixed_columns[0].musicResponsiveListItemFixedColumnRenderer.text.runs[0].text || '0'), - simple_text: fixed_columns[0].musicResponsiveListItemFixedColumnRenderer.text.runs[0].text, + simple_text: fixed_columns[0].musicResponsiveListItemFixedColumnRenderer.text.runs[0].text }, thumbnails: item_renderer.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails - } + }; } } diff --git a/lib/parser/ytmusic/search/AlbumResultItem.js b/lib/parser/ytmusic/search/AlbumResultItem.js index 2c5078a3..0230d836 100644 --- a/lib/parser/ytmusic/search/AlbumResultItem.js +++ b/lib/parser/ytmusic/search/AlbumResultItem.js @@ -4,7 +4,7 @@ class AlbumResultItem { static parse(data) { return data.map((item) => this.parseItem(item)); } - + static parseItem(item) { const list_item = item.musicResponsiveListItemRenderer; return { @@ -12,7 +12,7 @@ class AlbumResultItem { title: list_item.flexColumns[0]?.musicResponsiveListItemFlexColumnRenderer.text.runs[0]?.text, author: list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs[2]?.text, year: list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs - .find((run) => /^[12][0-9]{3}$/.test(run.text)).text, + .find((run) => (/^[12][0-9]{3}$/).test(run.text)).text, thumbnails: list_item?.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails, playlistId: list_item?.overlay.musicItemThumbnailOverlayRenderer.content.musicPlayButtonRenderer.playNavigationEndpoint.watchPlaylistEndpoint.playlistId }; diff --git a/lib/parser/ytmusic/search/ArtistResultItem.js b/lib/parser/ytmusic/search/ArtistResultItem.js index 2f7057cc..c2eb4c1a 100644 --- a/lib/parser/ytmusic/search/ArtistResultItem.js +++ b/lib/parser/ytmusic/search/ArtistResultItem.js @@ -11,7 +11,7 @@ class ArtistResultItem { id: list_item.navigationEndpoint.browseEndpoint.browseId, name: list_item.flexColumns[0]?.musicResponsiveListItemFlexColumnRenderer.text.runs[0]?.text, subscribers: list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs[2]?.text, - thumbnails: list_item?.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails, + thumbnails: list_item?.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails }; } } diff --git a/lib/parser/ytmusic/search/MusicSearchSuggestionItem.js b/lib/parser/ytmusic/search/MusicSearchSuggestionItem.js index ffef018f..8bb04431 100644 --- a/lib/parser/ytmusic/search/MusicSearchSuggestionItem.js +++ b/lib/parser/ytmusic/search/MusicSearchSuggestionItem.js @@ -5,18 +5,18 @@ class MusicSearchSuggestionItem { return { query: this.parseItem(data[0]).runs[0].text.trim(), results: data.map((item) => this.parseItem(item).runs.map((run) => run.text).join('').trim()) - } + }; } - + static parseItem(item) { let suggestion; - - if (item.historySuggestionRenderer) { - suggestion = item.historySuggestionRenderer.suggestion - } else { - suggestion = item.searchSuggestionRenderer.suggestion - } - + + if (item.historySuggestionRenderer) { + suggestion = item.historySuggestionRenderer.suggestion; + } else { + suggestion = item.searchSuggestionRenderer.suggestion; + } + return suggestion; } } diff --git a/lib/parser/ytmusic/search/PlaylistResultItem.js b/lib/parser/ytmusic/search/PlaylistResultItem.js index f7decca9..5e465547 100644 --- a/lib/parser/ytmusic/search/PlaylistResultItem.js +++ b/lib/parser/ytmusic/search/PlaylistResultItem.js @@ -8,14 +8,14 @@ class PlaylistResultItem { static parseItem(item) { const list_item = item.musicResponsiveListItemRenderer; const watch_playlist_endpoint = list_item?.overlay?.musicItemThumbnailOverlayRenderer - ?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchPlaylistEndpoint; + ?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchPlaylistEndpoint; return { id: watch_playlist_endpoint?.playlistId, title: list_item.flexColumns[0]?.musicResponsiveListItemFlexColumnRenderer.text.runs[0]?.text, author: list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs[2]?.text, channel_id: list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs[2]?.navigationEndpoint?.browseEndpoint.browseId || '0', - total_items: parseInt(list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs[4]?.text.match(/\d+/g)), + total_items: parseInt(list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs[4]?.text.match(/\d+/g)) }; } } diff --git a/lib/parser/ytmusic/search/SongResultItem.js b/lib/parser/ytmusic/search/SongResultItem.js index 894d4708..d14133a8 100644 --- a/lib/parser/ytmusic/search/SongResultItem.js +++ b/lib/parser/ytmusic/search/SongResultItem.js @@ -10,7 +10,7 @@ class SongResultItem { if (list_item.playlistItemData) { let artists = list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs; // Remove parts of array that just includes information that the item is a song - artists.splice(0,2); + artists.splice(0, 2); // Remove parts of array that contains data like album and duration const meta = artists.splice(artists.length - 4, 4); // Keep only even parts of array, the odd ones are just a delimiter between artists (&) @@ -21,7 +21,7 @@ class SongResultItem { artist: artists.map((artist) => artist.text), album: meta[1]?.text, duration: meta[3]?.text, - thumbnails: list_item.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails, + thumbnails: list_item.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails }; } } diff --git a/lib/parser/ytmusic/search/TopResultItem.js b/lib/parser/ytmusic/search/TopResultItem.js index 70ab8cc6..04806e92 100644 --- a/lib/parser/ytmusic/search/TopResultItem.js +++ b/lib/parser/ytmusic/search/TopResultItem.js @@ -19,11 +19,11 @@ class TopResultItem { case 'playlist': return PlaylistResultItem.parseItem(item); case 'song': - return SongResultItem.parseItem(item); + return SongResultItem.parseItem(item); case 'video': return VideoResultItem.parseItem(item); case 'artist': - return ArtistResultItem.parseItem(item); + return ArtistResultItem.parseItem(item); case 'album': return AlbumResultItem.parseItem(item); case 'single': @@ -31,11 +31,11 @@ class TopResultItem { default: return undefined; } - })() + })(); if (parsed_item) { - parsed_item.type = type - } + parsed_item.type = type; + } return parsed_item; }).filter((item) => item); diff --git a/lib/parser/ytmusic/search/VideoResultItem.js b/lib/parser/ytmusic/search/VideoResultItem.js index ad3c0f57..d0cfb57f 100644 --- a/lib/parser/ytmusic/search/VideoResultItem.js +++ b/lib/parser/ytmusic/search/VideoResultItem.js @@ -2,15 +2,15 @@ class VideoResultItem { static parse(data) { - return data.map((item) => this.parseItem(item)).filter((item) => item); + return data.map((item) => this.parseItem(item)).filter((item) => item); } - + static parseItem(item) { const list_item = item.musicResponsiveListItemRenderer; if (list_item.playlistItemData) { let authors = list_item.flexColumns[1]?.musicResponsiveListItemFlexColumnRenderer.text.runs; // Remove parts of array that just includes information that the item is a video - authors.splice(0,2); + authors.splice(0, 2); // Remove parts of array that contains data like number of views and duration const meta = authors.splice(authors.length - 4, 4); // Keep only even parts of array, the odd ones are just a delimiter between authors (&) @@ -21,7 +21,7 @@ class VideoResultItem { author: authors.map((author) => author.text), views: meta[1]?.text, duration: meta[3]?.text, - thumbnails: list_item?.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails, + thumbnails: list_item?.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails }; } } diff --git a/lib/proto/index.js b/lib/proto/index.js index 66b3d974..4031fada 100644 --- a/lib/proto/index.js +++ b/lib/proto/index.js @@ -14,7 +14,7 @@ class Proto { const buf = messages.VisitorData.encode({ id, timestamp }); return encodeURIComponent(Buffer.from(buf).toString('base64').replace(/\/|\+/g, '_')); } - + /** * Encodes basic channel analytics parameters. * @@ -22,17 +22,21 @@ class Proto { * @returns {string} */ static encodeChannelAnalyticsParams(channel_id) { - const buf = messages.ChannelAnalytics.encode({ params: { channel_id } }); + const buf = messages.ChannelAnalytics.encode({ + params: { + channel_id + } + }); return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** * Encodes search filters. * * @param {object} filters * @param {string} [filters.upload_date] - all | hour | today | week | month | year * @param {string} [filters.type] - all | video | channel | playlist | movie - * @param {string} [filters.duration] - all | short | medium | long + * @param {string} [filters.duration] - all | short | medium | long * @param {string} [filters.sort_by] - relevance | rating | upload_date | view_count * @returns {string} */ @@ -70,37 +74,34 @@ class Proto { const data = {}; - if (filters) { - data.filters = {}; - } else { - data.no_filter = 0; - } + if (filters) data.filters = {}; + else data.no_filter = 0; if (filters) { - if (filters.upload_date && filters.type !== 'video') - throw new Error('Upload date filter cannot be used with type ' + filters.type); + if (filters.upload_date && filters.type !== 'video') + throw new Error(`Upload date filter cannot be used with type ${filters.type}`); - if (filters.upload_date) { - data.filters.upload_date = upload_date[filters.upload_date] - } + if (filters.upload_date) { + data.filters.upload_date = upload_date[filters.upload_date]; + } - if (filters.type) { - data.filters.type = type[filters.type] - } + if (filters.type) { + data.filters.type = type[filters.type]; + } - if (filters.duration) { - data.filters.duration = duration[filters.duration] - } + if (filters.duration) { + data.filters.duration = duration[filters.duration]; + } + + if (filters.sort_by && filters.sort_by !== 'relevance') { + data.sort_by = order[filters.sort_by]; + } + } - if (filters.sort_by && filters.sort_by !== 'relevance') { - data.sort_by = order[filters.sort_by] - } - } - const buf = messages.SearchFilter.encode(data); return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** * Encodes YouTube Music search filters. * @@ -109,14 +110,18 @@ class Proto { * @returns {string} */ static encodeMusicSearchFilters(filters = {}) { - const data = { filters: { type: {} } }; - + const data = { + filters: { + type: {} + } + }; + data.filters.type[filters.type || 'all'] = 1; - + const buf = messages.MusicSearchFilter.encode(data); return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** * Encodes livechat message parameters. * @@ -126,13 +131,17 @@ class Proto { */ static encodeMessageParams(channel_id, video_id) { const buf = messages.LiveMessageParams.encode({ - params: { ids: { channel_id, video_id } }, + params: { + ids: { + channel_id, video_id + } + }, number_0: 1, number_1: 4 }); return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64'); } - + /** * Encodes comment section parameters. * @@ -143,10 +152,15 @@ class Proto { * @returns {string} */ static encodeCommentsSectionParams(video_id, options = {}) { - const sort_options = { TOP_COMMENTS: 0, NEWEST_FIRST: 1 }; - + const sort_options = { + TOP_COMMENTS: 0, + NEWEST_FIRST: 1 + }; + const buf = messages.GetCommentsSectionParams.encode({ - ctx: { video_id }, + ctx: { + video_id + }, unk_param: 6, params: { opts: { @@ -157,12 +171,12 @@ class Proto { target: 'comments-section' } }); - + return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** - * Encodes replies thread parameters. + * Encodes comment replies parameters. * * @param {string} video_id * @param {string} comment_id @@ -170,22 +184,26 @@ class Proto { */ static encodeCommentRepliesParams(video_id, comment_id) { const buf = messages.GetCommentsSectionParams.encode({ - ctx: { video_id }, + ctx: { + video_id + }, unk_param: 6, params: { replies_opts: { video_id, comment_id, - unkopts: { unk_param: 0 }, + unkopts: { + unk_param: 0 + }, unk_param_1: 1, unk_param_2: 10, channel_id: ' ' // Seems like this can be omitted }, target: `comment-replies-item-${comment_id}` } }); - + return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** * Encodes comment parameters. * @@ -194,13 +212,15 @@ class Proto { */ static encodeCommentParams(video_id) { const buf = messages.CreateCommentParams.encode({ - video_id, params: { index: 0 }, + video_id, params: { + index: 0 + }, number: 7 }); - + return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** * Encodes comment reply parameters. * @@ -211,13 +231,15 @@ class Proto { static encodeCommentReplyParams(comment_id, video_id) { const buf = messages.CreateCommentReplyParams.encode({ video_id, comment_id, - params: { unk_num: 0 }, + params: { + unk_num: 0 + }, unk_num: 7 }); return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** * Encodes comment action parameters. * @@ -231,15 +253,15 @@ class Proto { */ static encodeCommentActionParams(type, args = {}) { const data = {}; - + data.type = type; data.video_id = args.video_id || ''; data.comment_id = args.comment_id || ''; data.unk_num = 2; - + if (args.hasOwnProperty('text')) { args.comment_id && (delete data.unk_num); - + data.translate_comment_params = { params: { comment: { @@ -248,13 +270,13 @@ class Proto { }, comment_id: args.comment_id || '', target_language: args.target_language - } + }; } - + const buf = messages.PeformCommentActionParams.encode(data); return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** * Encodes notification preference parameters. * @@ -264,18 +286,20 @@ class Proto { */ static encodeNotificationPref(channel_id, index) { const buf = messages.NotificationPreferences.encode({ - channel_id, pref_id: { index }, + channel_id, pref_id: { + index + }, number_0: 0, number_1: 4 }); return encodeURIComponent(Buffer.from(buf).toString('base64')); } - + /** * Encodes sound info parameters. * * @param {string} id - * @returns {string} + * @returns {string} */ static encodeSoundInfoParams(id) { const data = { @@ -288,7 +312,7 @@ class Proto { } } } - } + }; const buf = messages.SoundInfoParams.encode(data); return encodeURIComponent(Buffer.from(buf).toString('base64')); diff --git a/lib/utils/Constants.js b/lib/utils/Constants.js index 221aa0dc..43c84abb 100644 --- a/lib/utils/Constants.js +++ b/lib/utils/Constants.js @@ -57,7 +57,7 @@ module.exports = { INNERTUBE_HEADERS_BASE: { 'accept': '*/*', 'accept-encoding': 'gzip, deflate', - 'content-type': 'application/json', + 'content-type': 'application/json' }, METADATA_KEYS: [ 'embed', 'view_count', 'average_rating', 'allow_ratings', @@ -81,7 +81,7 @@ module.exports = { USER_MENTION: 'NOTIFICATION_USER_MENTION_WEB_CONTROL', SHARED_CONTENT: 'NOTIFICATION_RETUBING_WEB_CONTROL', - // Privacy + // Privacy PLAYLISTS_PRIVACY: 'PRIVACY_DISCOVERABLE_SAVED_PLAYLISTS', SUBSCRIPTIONS_PRIVACY: 'PRIVACY_DISCOVERABLE_SUBSCRIPTIONS' }, diff --git a/lib/utils/Request.js b/lib/utils/Request.js index 3eb92e99..f7218be6 100644 --- a/lib/utils/Request.js +++ b/lib/utils/Request.js @@ -29,48 +29,48 @@ class Request { #setupInterceptor() { this.instance.interceptors.request.use((config) => { const is_json_payload = typeof config.data == 'object'; - + config.headers['user-agent'] = Utils.getRandomUserAgent('desktop').userAgent; config.headers['accept-language'] = `en-${this.session.config.gl || 'US'}`; config.headers['x-goog-visitor-id'] = this.session.context.client.visitorData || ''; config.headers['x-youtube-client-version'] = this.session.context.client.clientVersion; - + if (is_json_payload) { config.data = { - context: JSON.parse(JSON.stringify(this.session.context)), // deep copies the context object + context: JSON.parse(JSON.stringify(this.session.context)), // Deep copies the context object ...config.data }; - + this.#adjustContext(config.data.context, config.data.client); - + config.headers['x-youtube-client-version'] = config.data.context.client.clientVersion; config.headers['x-origin'] = config.data.context.client.originalUrl; config.headers['origin'] = config.data.context.client.originalUrl; delete config.data.client; - } - + } + if (this.session.logged_in) { const cookie = this.session.config.cookie; - + const token = cookie && this.session.auth_apisid || this.session.access_token; - + config.headers.cookie = cookie || ''; config.headers.authorization = cookie ? token : `Bearer ${token}`; - + !cookie && (delete config.params.key); } - + this.session.config.debug && - console.info('\n', '[' + config.method.toUpperCase() + ']', '>', config.baseURL + config.url, '\n', config?.data, '\n'); - + console.info('\n', `[${config.method.toUpperCase()}]`, '>', config.baseURL + config.url, '\n', config?.data, '\n'); + return config; }, (error) => { throw new Utils.InnertubeError(error.message, error); }); - + /** * Standardizes the API response and catches all errors. */ @@ -80,19 +80,19 @@ class Request { status_code: res.status, data: res.data }; - + if (res.status !== 200) throw new Utils.InnertubeError(`Request to ${res.config.url} failed with status code ${res.status} ${res.statusText}`, response); return response; }); - + this.instance.interceptors.response.use(undefined, (error) => { if (error.info) return Promise.reject(error); throw new Utils.InnertubeError('Could not complete this operation', error.message); }); } - + /** * Adjusts the context according to the given client. * diff --git a/lib/utils/Utils.js b/lib/utils/Utils.js index a62b698b..d9487683 100644 --- a/lib/utils/Utils.js +++ b/lib/utils/Utils.js @@ -7,7 +7,7 @@ const Flatten = require('flat'); class InnertubeError extends Error { constructor (message, info) { super(message); - + if (info) { this.info = info; } @@ -62,15 +62,15 @@ function observe(obj) { return (rule, del_item) => target .find((obj, index) => { const match = deepCompare(rule, obj); - + if (match && del_item) { target.splice(index, 1); } - + return match; }); } - + if (prop == 'findAll') { /** * Returns all objects that match the rule. @@ -83,15 +83,15 @@ function observe(obj) { return (rule, del_items) => target .filter((obj, index) => { const match = deepCompare(rule, obj); - + if (match && del_items) { target.splice(index, 1); } - + return match; }); } - + if (prop == 'remove') { /** * Removes the item at the given index. @@ -102,7 +102,7 @@ function observe(obj) { */ return (index) => target.splice(index, 1); } - + return Reflect.get(...arguments); } }); @@ -119,14 +119,14 @@ function observe(obj) { */ function deepCompare(obj1, obj2) { const keys = Reflect.ownKeys(obj1); - + return keys.some((key) => { const is_text = obj2[key]?.constructor.name === 'Text'; - + if (!is_text && typeof obj2[key] === 'object') { return JSON.stringify(obj1[key]) === JSON.stringify(obj2[key]); } - + return obj1[key] === (is_text ? obj2[key].toString() : obj2[key]); }); } @@ -172,19 +172,19 @@ function getRandomUserAgent(type) { /** * Generates an authentication token from a cookies' sid. * - * @param {string} sid - Sid extracted from cookies + * @param {string} sid - Sid extracted from cookies * @returns {string} */ function generateSidAuth(sid) { const youtube = 'https://www.youtube.com'; const timestamp = Math.floor(new Date().getTime() / 1000); - const input = [timestamp, sid, youtube].join(' '); + const input = [ timestamp, sid, youtube ].join(' '); - let hash = Crypto.createHash('sha1'); - let data = hash.update(input, 'utf-8'); - let gen_hash = data.digest('hex'); + const hash = Crypto.createHash('sha1'); + const data = hash.update(input, 'utf-8'); + const gen_hash = data.digest('hex'); - return ['SAPISIDHASH', [timestamp, gen_hash].join('_')].join(' '); + return [ 'SAPISIDHASH', [ timestamp, gen_hash ].join('_') ].join(' '); } /** @@ -200,29 +200,28 @@ function generateRandomString(length) { for (let i = 0; i < length; i++) { result.push(alphabet.charAt(Math.floor(Math.random() * alphabet.length))); } - + return result.join(''); } /** * Converts time (h:m:s) to seconds. * - * @param {string} time + * @param {string} time * @returns {number} seconds */ function timeToSeconds(time) { - let params = time.split(':'); - + const params = time.split(':'); + switch (params.length) { - case 1: - return parseInt(+params[0]); - case 2: - return parseInt(+params[0] * 60 + +params[1]); - case 3: - return parseInt(+params[0] * 3600 + +params[1] * 60 + +params[2]); - default: - // this is just for maximum compatibility, this is most definitely a bad way to handle this - throw new TypeError('undefined is not a function'); + case 1: + return parseInt(+params[0]); + case 2: + return parseInt(+params[0] * 60 + +params[1]); + case 3: + return parseInt(+params[0] * 3600 + +params[1] * 60 + +params[2]); + default: + break; } } @@ -253,7 +252,7 @@ function isValidClient(client) { * @returns {void} */ function throwIfMissing(params) { - for (const [key, value] of Object.entries(params)) { + for (const [ key, value ] of Object.entries(params)) { if (!value) throw new MissingParamError(`${key} is missing`); } @@ -262,7 +261,7 @@ function throwIfMissing(params) { /** * Turns the ntoken transform data into a valid json array * - * @param {string} data + * @param {string} data * @returns {string} */ function refineNTokenData(data) { diff --git a/test/main.test.js b/test/main.test.js index 483fbcd8..dfb3fd7b 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -13,16 +13,15 @@ describe('YouTube.js Tests', () => { describe('Search', () => { it('Should search on YouTube', async () => { - const search = await this.session.search(Constants.VIDEOS[0].QUERY, { client: 'YOUTUBE' }); + const search = await this.session.search(Constants.VIDEOS[0].QUERY); expect(search.results.length).toBeLessThanOrEqual(30); }); - /* + it('Should search on YouTube Music', async () => { - const search = await this.session.search(Constants.VIDEOS[1].QUERY, { client: 'YTMUSIC' }); - expect(search.results.songs.length).toBeLessThanOrEqual(3); + const search = await this.session.music.search(Constants.VIDEOS[1].QUERY); + expect(search.songs.contents.length).toBeLessThanOrEqual(3); }); - */ it('Should retrieve YouTube search suggestions', async () => { const suggestions = await this.session.getSearchSuggestions(Constants.VIDEOS[0].QUERY, { client: 'YOUTUBE' }); @@ -37,19 +36,22 @@ describe('YouTube.js Tests', () => { describe('Comments', () => { it('Should retrieve comments', async () => { - this.comments = await this.session.getComments(Constants.VIDEOS[1].ID); - expect(this.comments.items.length).toBeLessThanOrEqual(20); + this.threads = await this.session.getComments(Constants.VIDEOS[1].ID); + expect(this.threads.contents.length).toBeGreaterThan(0); }); - it('Should retrieve comment thread continuation', async () => { - const next = await this.comments.getContinuation(); - expect(next.items.length).toBeLessThanOrEqual(20); + it('Should retrieve next batch of comments', async () => { + const next = await this.threads.getContinuation(); + expect(next.contents.length).toBeGreaterThan(0); }); it('Should retrieve comment replies', async () => { - const top_comment = this.comments.items[0]; - const replies = await top_comment.getReplies(); - expect(replies.items.length).toBeLessThanOrEqual(10); + const comment = this.threads.contents[0]; + + const thread = await comment.getReplies(); + + expect(thread.comment_id).toBe(comment.comment_id); + expect(thread.replies.length).toBeLessThanOrEqual(10); }); }); diff --git a/typings/lib/Innertube.d.ts b/typings/lib/Innertube.d.ts index e5ab654c..5f8b1f12 100644 --- a/typings/lib/Innertube.d.ts +++ b/typings/lib/Innertube.d.ts @@ -125,13 +125,9 @@ declare class Innertube { * * @param {string} video_id - the video id. * @param {string} [sort_by] - can be: `TOP_COMMENTS` or `NEWEST_FIRST`. - * @returns {Promise.<{ page_count: number, comment_count: number, items: object[] }>} + * @returns {Promise.} */ - getComments(video_id: string, sort_by?: string): Promise<{ - page_count: number; - comment_count: number; - items: object[]; - }>; + getComments(video_id: string, sort_by?: string): Promise; /** * Retrieves YouTube's home feed (aka recommendations). * @@ -258,6 +254,7 @@ import InteractionManager = require("./core/InteractionManager"); import YTMusic = require("./core/Music"); import VideoInfo = require("./parser/youtube/VideoInfo"); import Search = require("./parser/youtube/Search"); +import Comments = require("./parser/youtube/Comments"); import FilterableFeed = require("./core/FilterableFeed"); import Library = require("./parser/youtube/Library"); import History = require("./parser/youtube/History"); diff --git a/typings/lib/core/Actions.d.ts b/typings/lib/core/Actions.d.ts index 0bd402be..40c81444 100644 --- a/typings/lib/core/Actions.d.ts +++ b/typings/lib/core/Actions.d.ts @@ -318,5 +318,11 @@ declare class Actions { status_code: number; data: object; }>; + /** + * Executes an API call. + * @param {string} action - endpoint + * @param {object} args - call arguments + */ + execute(action: string, args: object): Promise; #private; } diff --git a/typings/lib/parser/contents/classes/Button.d.ts b/typings/lib/parser/contents/classes/Button.d.ts index c01010ed..98d545ad 100644 --- a/typings/lib/parser/contents/classes/Button.d.ts +++ b/typings/lib/parser/contents/classes/Button.d.ts @@ -5,7 +5,7 @@ declare class Button { text: any; label: any; tooltip: any; - icon_type: any; + iconType: any; endpoint: NavigationEndpoint; } import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/classes/Comment.d.ts b/typings/lib/parser/contents/classes/Comment.d.ts new file mode 100644 index 00000000..de69e79f --- /dev/null +++ b/typings/lib/parser/contents/classes/Comment.d.ts @@ -0,0 +1,69 @@ +export = Comment; +declare class Comment { + constructor(data: any); + type: string; + content: Text; + published: Text; + author_is_channel_owner: any; + current_user_reply_thumbnail: Thumbnail[]; + author_badge: any; + author: Author; + action_menu: any; + action_buttons: any; + comment_id: any; + vote_status: any; + vote_count: { + text: any; + short_text: any; + }; + reply_count: any; + is_liked: any; + is_disliked: any; + is_pinned: boolean; + /** + * API response. + * @typedef {{ success: boolean, status_code: number, data: object }} Response + */ + /** + * Likes the comment. + * @returns {Promise.} + */ + like(): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Dislikes the comment. + * @returns {Promise.} + */ + dislike(): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Creates a reply to the comment. + * @param {string} text + * @returns {Promise.} + */ + reply(text: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Translates the comment to the given language. + * @param {string} target_language + */ + translate(target_language: string): Promise; + /** + * @param {import('../../../../core/Actions')} actions + * @private + */ + private setActions; + #private; +} +import Text = require("./Text"); +import Thumbnail = require("./Thumbnail"); +import Author = require("./Author"); diff --git a/typings/lib/parser/contents/classes/CommentReplyDialog.d.ts b/typings/lib/parser/contents/classes/CommentReplyDialog.d.ts new file mode 100644 index 00000000..e3f734c6 --- /dev/null +++ b/typings/lib/parser/contents/classes/CommentReplyDialog.d.ts @@ -0,0 +1,12 @@ +export = CommentReplyDialog; +declare class CommentReplyDialog { + constructor(data: any); + type: string; + reply_button: any; + cancel_button: any; + author_thumbnail: Thumbnail[]; + placeholder: Text; + error_message: Text; +} +import Thumbnail = require("./Thumbnail"); +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/classes/CommentThread.d.ts b/typings/lib/parser/contents/classes/CommentThread.d.ts new file mode 100644 index 00000000..438ab65f --- /dev/null +++ b/typings/lib/parser/contents/classes/CommentThread.d.ts @@ -0,0 +1,26 @@ +export = CommentThread; +declare class CommentThread { + constructor(data: any); + type: string; + /** @type {import('./Comment')} */ + comment: import('./Comment'); + /** @type {boolean} */ + is_moderated_elq_comment: boolean; + /** + * Retrieves replies to this comment thread. + * @returns {Promise.} + */ + getReplies(): Promise; + replies: any; + /** + * Retrieves next batch of replies. + * @returns {Promise.} + */ + getContinuation(): Promise; + /** + * @param {import('../../../core/Actions')} actions + * @private + */ + private setActions; + #private; +} diff --git a/typings/lib/parser/contents/classes/CommentsHeader.d.ts b/typings/lib/parser/contents/classes/CommentsHeader.d.ts new file mode 100644 index 00000000..a1d5e908 --- /dev/null +++ b/typings/lib/parser/contents/classes/CommentsHeader.d.ts @@ -0,0 +1,12 @@ +export = CommentsHeader; +declare class CommentsHeader { + constructor(data: any); + type: string; + title: Text; + count: Text; + comments_count: Text; + create_renderer: any; + sort_menu: any; + custom_emojis: any; +} +import Text = require("./Text"); diff --git a/typings/lib/parser/contents/classes/ContinuationItem.d.ts b/typings/lib/parser/contents/classes/ContinuationItem.d.ts index 1aae3c14..a729d00b 100644 --- a/typings/lib/parser/contents/classes/ContinuationItem.d.ts +++ b/typings/lib/parser/contents/classes/ContinuationItem.d.ts @@ -3,6 +3,7 @@ declare class ContinuationItem { constructor(data: any); type: string; trigger: any; + button: any; endpoint: NavigationEndpoint; } import NavigationEndpoint = require("./NavigationEndpoint"); diff --git a/typings/lib/parser/contents/classes/EndscreenElement.d.ts b/typings/lib/parser/contents/classes/EndscreenElement.d.ts index 1e59df12..af182082 100644 --- a/typings/lib/parser/contents/classes/EndscreenElement.d.ts +++ b/typings/lib/parser/contents/classes/EndscreenElement.d.ts @@ -3,14 +3,14 @@ declare class EndscreenElement { constructor(data: any); type: string; style: any; + title: Text; + endpoint: NavigationEndpoint; image: Thumbnail[]; icon: Thumbnail[]; metadata: Text; call_to_action: Text; hovercard_button: any; is_subscribe: any; - title: Text; - endpoint: NavigationEndpoint; thumbnail_overlays: any; left: any; width: any; @@ -20,6 +20,6 @@ declare class EndscreenElement { end_ms: any; id: any; } -import Thumbnail = require("./Thumbnail"); import Text = require("./Text"); import NavigationEndpoint = require("./NavigationEndpoint"); +import Thumbnail = require("./Thumbnail"); diff --git a/typings/lib/parser/contents/classes/MusicResponsiveListItem.d.ts b/typings/lib/parser/contents/classes/MusicResponsiveListItem.d.ts index ced0584c..a1661500 100644 --- a/typings/lib/parser/contents/classes/MusicResponsiveListItem.d.ts +++ b/typings/lib/parser/contents/classes/MusicResponsiveListItem.d.ts @@ -24,6 +24,9 @@ declare class MusicResponsiveListItem { }; artists: any; views: any; + authors: any; + name: any; + subscribers: any; author: { name: any; channel_id: any; @@ -32,18 +35,7 @@ declare class MusicResponsiveListItem { name: any; channel_id: any; endpoint: any; - } | { - name: any; - channel_id: any; - endpoint: any; }; - authors: { - name: any; - channel_id: any; - endpoint: any; - }[]; - name: any; - subscribers: any; year: any; item_count: number; #private; diff --git a/typings/lib/parser/contents/classes/NavigationEndpoint.d.ts b/typings/lib/parser/contents/classes/NavigationEndpoint.d.ts index 2fdf420b..b921fa60 100644 --- a/typings/lib/parser/contents/classes/NavigationEndpoint.d.ts +++ b/typings/lib/parser/contents/classes/NavigationEndpoint.d.ts @@ -2,6 +2,8 @@ export = NavigationEndpoint; declare class NavigationEndpoint { constructor(data: any); type: string; + payload: any; + dialog: any; metadata: {}; browse: { id: any; @@ -75,5 +77,11 @@ declare class NavigationEndpoint { send_live_chat_vote: { params: any; }; + /** + * Calls the endpoint. (This is an experiment and may replace {@link call()} in the future.). + * @param {import('../../../core/Actions')} actions + * @param {object} args + */ + callTest(actions: import('../../../core/Actions'), args?: object): Promise; call(actions: any, client: any): Promise; } diff --git a/typings/lib/parser/contents/classes/SectionList.d.ts b/typings/lib/parser/contents/classes/SectionList.d.ts index 05b17c5d..6bd03fe6 100644 --- a/typings/lib/parser/contents/classes/SectionList.d.ts +++ b/typings/lib/parser/contents/classes/SectionList.d.ts @@ -5,4 +5,5 @@ declare class SectionList { target_id: any; contents: any; continuation: any; + header: any; } diff --git a/typings/lib/parser/contents/classes/comments/AuthorCommentBadge.d.ts b/typings/lib/parser/contents/classes/comments/AuthorCommentBadge.d.ts new file mode 100644 index 00000000..95580b04 --- /dev/null +++ b/typings/lib/parser/contents/classes/comments/AuthorCommentBadge.d.ts @@ -0,0 +1,10 @@ +export = AuthorCommentBadge; +declare class AuthorCommentBadge { + constructor(data: any); + type: string; + icon_type: any; + tooltip: any; + style: string; + get orig_badge(): any; + #private; +} diff --git a/typings/lib/parser/contents/classes/comments/Comment.d.ts b/typings/lib/parser/contents/classes/comments/Comment.d.ts new file mode 100644 index 00000000..901ce85c --- /dev/null +++ b/typings/lib/parser/contents/classes/comments/Comment.d.ts @@ -0,0 +1,26 @@ +export = Comment; +declare class Comment { + constructor(data: any); + type: string; + content: Text; + published: Text; + author_is_channel_owner: any; + current_user_reply_thumbnail: Thumbnail[]; + author_badge: any; + author: Author; + action_menu: any; + action_buttons: any; + comment_id: any; + vote_status: any; + vote_count: { + text: any; + short_text: any; + }; + reply_count: any; + is_liked: any; + is_disliked: any; + is_pinned: boolean; +} +import Text = require("../Text"); +import Thumbnail = require("../Thumbnail"); +import Author = require("../Author"); diff --git a/typings/lib/parser/contents/classes/comments/CommentActionButtons.d.ts b/typings/lib/parser/contents/classes/comments/CommentActionButtons.d.ts new file mode 100644 index 00000000..5b8c8a04 --- /dev/null +++ b/typings/lib/parser/contents/classes/comments/CommentActionButtons.d.ts @@ -0,0 +1,8 @@ +export = CommentActionButtons; +declare class CommentActionButtons { + constructor(data: any); + type: string; + like_button: any; + dislike_button: any; + reply_button: any; +} diff --git a/typings/lib/parser/contents/classes/comments/CommentReplies.d.ts b/typings/lib/parser/contents/classes/comments/CommentReplies.d.ts new file mode 100644 index 00000000..a81c61a3 --- /dev/null +++ b/typings/lib/parser/contents/classes/comments/CommentReplies.d.ts @@ -0,0 +1,8 @@ +export = CommentReplies; +declare class CommentReplies { + constructor(data: any); + type: string; + contents: any; + view_replies: any; + hide_replies: any; +} diff --git a/typings/lib/parser/contents/classes/comments/CommentSimplebox.d.ts b/typings/lib/parser/contents/classes/comments/CommentSimplebox.d.ts new file mode 100644 index 00000000..4ed72bc9 --- /dev/null +++ b/typings/lib/parser/contents/classes/comments/CommentSimplebox.d.ts @@ -0,0 +1,12 @@ +export = CommentSimplebox; +declare class CommentSimplebox { + constructor(data: any); + type: string; + submit_button: any; + cancel_button: any; + author_thumbnails: Thumbnail[]; + placeholder: Text; + avatar_size: any; +} +import Thumbnail = require("../Thumbnail"); +import Text = require("../Text"); diff --git a/typings/lib/parser/contents/classes/livechat/items/LiveChatPaidSticker.d.ts b/typings/lib/parser/contents/classes/livechat/items/LiveChatPaidSticker.d.ts new file mode 100644 index 00000000..36551ee7 --- /dev/null +++ b/typings/lib/parser/contents/classes/livechat/items/LiveChatPaidSticker.d.ts @@ -0,0 +1,19 @@ +export = LiveChatPaidSticker; +declare class LiveChatPaidSticker { + constructor(data: any); + type: string; + id: any; + author: { + id: any; + name: Text; + thumbnails: Thumbnail[]; + badges: any; + }; + sticker: Thumbnail[]; + purchase_amount: any; + context_menu: NavigationEndpoint; + timestamp: number; +} +import Text = require("../../Text"); +import Thumbnail = require("../../Thumbnail"); +import NavigationEndpoint = require("../../NavigationEndpoint"); diff --git a/typings/lib/parser/contents/index.d.ts b/typings/lib/parser/contents/index.d.ts index 5f17060f..3da54051 100644 --- a/typings/lib/parser/contents/index.d.ts +++ b/typings/lib/parser/contents/index.d.ts @@ -19,8 +19,8 @@ declare class Parser { /** * Parses the `contents` property of the response. * - * @param {object} data - * @param {string} module + * @param {object} data - contents to be parsed. + * @param {string} module - a folder for specific DA classes. * @returns {*} */ static parse(data: object, module: string): any; diff --git a/typings/lib/parser/index.d.ts b/typings/lib/parser/index.d.ts index 6fdbae58..a4f7f178 100644 --- a/typings/lib/parser/index.d.ts +++ b/typings/lib/parser/index.d.ts @@ -4,6 +4,6 @@ declare class Parser { data: any; session: any; args: {}; - parse(): any; + parse(): {}; #private; } diff --git a/typings/lib/parser/youtube/Comments.d.ts b/typings/lib/parser/youtube/Comments.d.ts new file mode 100644 index 00000000..97114d70 --- /dev/null +++ b/typings/lib/parser/youtube/Comments.d.ts @@ -0,0 +1,25 @@ +export = Comments; +declare class Comments { + constructor(actions: any, data: any, already_parsed?: boolean); + /** @type {import('../contents/classes/CommentsHeader')} */ + header: import('../contents/classes/CommentsHeader'); + /** @type {import('../contents/classes/CommentThread')[]} */ + contents: import('../contents/classes/CommentThread')[]; + /** + * Creates a top-level comment. + * @param {string} text + * @returns {Promise.<{ success: boolean, status_code: number, data: object }>} + */ + comment(text: string): Promise<{ + success: boolean; + status_code: number; + data: object; + }>; + /** + * Retrieves next batch of comments. + * @returns {Promise.} + */ + getContinuation(): Promise; + get page(): any; + #private; +} diff --git a/typings/lib/parser/youtube/VideoInfo.d.ts b/typings/lib/parser/youtube/VideoInfo.d.ts index 12ebf247..176f599a 100644 --- a/typings/lib/parser/youtube/VideoInfo.d.ts +++ b/typings/lib/parser/youtube/VideoInfo.d.ts @@ -1,5 +1,5 @@ export = VideoInfo; -/** namespace */ +/** Namespace */ declare class VideoInfo { /** * @param {object} data - API response. @@ -115,7 +115,6 @@ declare class VideoInfo { }>; /** * Retrieves Live Chat if available. - * * @param {string} [mode] - livechat mode * @returns {Promise.} */ diff --git a/typings/lib/proto/index.d.ts b/typings/lib/proto/index.d.ts index b5524d34..a15d7f1c 100644 --- a/typings/lib/proto/index.d.ts +++ b/typings/lib/proto/index.d.ts @@ -63,7 +63,7 @@ declare class Proto { sort_by: string; }): string; /** - * Encodes replies thread parameters. + * Encodes comment replies parameters. * * @param {string} video_id * @param {string} comment_id