From fb68e6bcfedecc67e9131b71292a2dd3a21b7f14 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd <45672955+Wykerd@users.noreply.github.com> Date: Wed, 20 Jul 2022 19:06:12 +0200 Subject: [PATCH] feat!: better cross runtime support (#97) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: remove dependancies removes node-forge and uuid in favor of Web APIs * refactor!: commonjs to es6 To aid with #93 I will make all my changes in TypeScript instead. This is the first step into making that happen. Used: https://github.com/wessberg/cjstoesm * refactor!: NToken and Signature TS files Bring this PR up to speed with #93 * feat: cross platform cache (WIP) this is untested! should remove idb as dependecy. * feat: EventEmitter polyfill * refactor: remove events * feat: HTTPClient based on Fetch API (WIP) * refactor!: parsers refactor (WIP) Initial TS support for parsers as per #93 This adds several type safety checks to the parser which'll help to ensure valid data is returned by the parser. * refactor!: parsers refactor (WIP) Bring more in line with the existing implementations & make less verbose * refactor!: parser refactor I was overcomplicating things, this is much simpler and compatible with the existing JS API * fix: some missed parsers while refactoring * fix: better type inferance for parseResponse * feat(TS): typesafe YTNode casts * feat: more type safety in YTNode and Parser * refactor: VideoInfo download with fetch & TS (WIP) Again, this also does some work for #93 * fix: LiveChat in VideoInfo * refactor!: more typesafety in parser * refactor!: VideoInfo almost completed * refactor!: player and session refactors - Remove the Player class' dependance on Session. - Add additional context to the Session. * refactor!: move auth logic to Session (WIP) * refactor: TS port for Actions and Innertube My fingers hurt from typing out all those types :-P * refactor: NavigationEndpoint TS this is still a WIP and should be improved. NavigationEndpoint should probably be refactored further. * refactor!: VideoInfo compiles without errors * chore: delete old player * fix: import errors It compiles and runs!! * fix: Utils import fixes * fix: several runtime errors * fix: video streaming * chore: remove console.log debugging Whoops, forgot to remove these before I pushed the previous commit * chore: remove old unused dependencies * fix: typescript errors Now emitting declarations and source maps * refactor: TS feed * chore: delete old Feed * refactor: move streamToIterable into Utils * refactor: AccountManager TS * refactor: FilterableFeed to TS * refactor: InteractionManager to TS * refactor: PlaylistManager to TS * refactor: TabbedFeed to TS * refactor: Music to TS (WIP) more work to be done, see TODO comments * fix: getting the tests to pass (6/12) YouTube.js Tests Search ✓ Should search on YouTube (1152 ms) ✕ Should search on YouTube Music (705 ms) ✕ Should retrieve YouTube search suggestions (722 ms) ✓ Should retrieve YouTube Music search suggestions (233 ms) Comments ✓ Should retrieve comments (585 ms) ✕ Should retrieve next batch of comments (221 ms) ✕ Should retrieve comment replies (1 ms) General ✕ Should retrieve playlist with YouTube (732 ms) ✓ Should retrieve home feed (838 ms) ✓ Should retrieve trending content (543 ms) ✓ Should retrieve video info (639 ms) ✕ Should download video (5 ms) * fix: tests (7/12) YouTube.js Tests Search ✓ Should search on YouTube (1984 ms) ✕ Should search on YouTube Music (1139 ms) ✕ Should retrieve YouTube search suggestions (1433 ms) ✓ Should retrieve YouTube Music search suggestions (529 ms) Comments ✓ Should retrieve comments (324 ms) ✓ Should retrieve next batch of comments (395 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (653 ms) ✓ Should retrieve home feed (1085 ms) ✓ Should retrieve trending content (513 ms) ✓ Should retrieve video info (921 ms) ✕ Should download video (3 ms) * fix: download tests (8/12) YouTube.js Tests Search ✓ Should search on YouTube (1293 ms) ✕ Should search on YouTube Music (927 ms) ✕ Should retrieve YouTube search suggestions (1250 ms) ✓ Should retrieve YouTube Music search suggestions (258 ms) Comments ✓ Should retrieve comments (803 ms) ✓ Should retrieve next batch of comments (511 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (528 ms) ✓ Should retrieve home feed (1047 ms) ✓ Should retrieve trending content (548 ms) ✓ Should retrieve video info (825 ms) ✓ Should download video (1779 ms) * fix: tests (9/12) YouTube.js Tests Search ✓ Should search on YouTube (1276 ms) ✕ Should search on YouTube Music (955 ms) ✓ Should retrieve YouTube search suggestions (661 ms) ✓ Should retrieve YouTube Music search suggestions (491 ms) Comments ✓ Should retrieve comments (624 ms) ✓ Should retrieve next batch of comments (353 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (672 ms) ✓ Should retrieve home feed (1277 ms) ✓ Should retrieve trending content (999 ms) ✓ Should retrieve video info (1106 ms) ✓ Should download video (2514 ms) * feat: key based type validation for parsers * fix: comments tests pass (10/12) YouTube.js Tests Search ✓ Should search on YouTube (938 ms) ✕ Should search on YouTube Music (850 ms) ✓ Should retrieve YouTube search suggestions (528 ms) ✓ Should retrieve YouTube Music search suggestions (224 ms) Comments ✓ Should retrieve comments (518 ms) ✓ Should retrieve next batch of comments (337 ms) ✓ Should retrieve comment replies (358 ms) General ✕ Should retrieve playlist with YouTube (466 ms) ✓ Should retrieve home feed (1051 ms) ✓ Should retrieve trending content (623 ms) ✓ Should retrieve video info (863 ms) ✓ Should download video (2656 ms) * refactor: type safety checks removing @ts-ignore * fix: playlist tests pass (11/12) YouTube.js Tests Search ✓ Should search on YouTube (991 ms) ✕ Should search on YouTube Music (924 ms) ✓ Should retrieve YouTube search suggestions (606 ms) ✓ Should retrieve YouTube Music search suggestions (225 ms) Comments ✓ Should retrieve comments (393 ms) ✓ Should retrieve next batch of comments (284 ms) ✓ Should retrieve comment replies (252 ms) General ✓ Should retrieve playlist with YouTube (578 ms) ✓ Should retrieve home feed (1148 ms) ✓ Should retrieve trending content (541 ms) ✓ Should retrieve video info (799 ms) ✓ Should download video (1419 ms) * fix: all tests pass for node 🎉 YouTube.js Tests Search ✓ Should search on YouTube (1053 ms) ✓ Should search on YouTube Music (761 ms) ✓ Should retrieve YouTube search suggestions (453 ms) ✓ Should retrieve YouTube Music search suggestions (221 ms) Comments ✓ Should retrieve comments (627 ms) ✓ Should retrieve next batch of comments (412 ms) ✓ Should retrieve comment replies (268 ms) General ✓ Should retrieve playlist with YouTube (565 ms) ✓ Should retrieve home feed (775 ms) ✓ Should retrieve trending content (498 ms) ✓ Should retrieve video info (875 ms) ✓ Should download video (1364 ms) * build: working Deno bundle Still need to test whether this bundle works in the browser * docs: update deno example to download video * refactor: MusicResponsiveListItem to TS * docs: TSDoc for Parser helpers * docs: Parser documentation for TS * docs: add note about parseItem and parseArray * test: remove browser tests since they're identical * feat: browser support and proxy example * fix: PlaylistManager TS after merge * feat: in-browser video streaming * refactor: cleanup the Dash example * feat: allow custom fetch implementations * feat: fetch debugger * fix: OAuth login * refactor: remove file extensions from imports * refactor: build scripts * fix: CustomEvent on node * fix: LiveChat * fix: linting * fix: liniting in build-parser-json * chore: update test workflow * fix: NToken errors after lint fixes * fix: codacy complaints * docs: update to reflect changes Definitly needs more work but its a start * refactor: cleanup imports/exports * fix: browser example - Remove user-agent before making request. - Fix cache on browsers * fix: cache on node * fix: stupid mistake * refactor: Session#signIn to wait untill success This also splits the 'auth' event up into 3 distinct events: - 'auth' -> fired on success - 'auth-pending' -> fired when pending authentication - 'auth-error' -> fired when an error occurred * refactor: freeze Constants * refactor: cleanup HTTPClient Request * refactor: debugFetch readability * chore: lint * refactor: replace jsdoc with tsdoc eslint plugin remove @param annotations without descriptions * fix: bunch of liniting warnings * refactor: better inference on YTNode#is As suggested by @MasterOfBob777 * fix: linting warnings * revert: undici import * refactor: rename `list_type` to `item_type` --- .eslintrc.yml | 28 +- .github/workflows/node.js.yml | 2 +- .gitignore | 8 + README_v2.0.0WIP.md | 122 +- browser.ts | 12 + build/browser.d.ts | 2 - build/browser.js | 40 - build/browser.js.map | 7 - build/node.d.ts | 2 - build/node.js | 10 - build/node.js.map | 7 - bundle/browser.d.ts | 1 + examples/browser/README.md | 26 + examples/browser/proxy/deno.ts | 82 + examples/browser/web/.gitignore | 24 + examples/browser/web/favicon.svg | 15 + examples/browser/web/index.html | 20 + examples/browser/web/package.json | 18 + examples/browser/web/public/service-worker.js | 3 + .../browser/web/public/service-worker.js.map | 7 + examples/browser/web/public/vite.svg | 1 + examples/browser/web/src/main.ts | 99 + examples/browser/web/src/style.css | 12 + examples/browser/web/src/vite-env.d.ts | 1 + examples/browser/web/tsconfig.json | 20 + examples/deno/README.md | 7 + examples/deno/index.ts | 16 + index.js | 2 - index.ts | 18 + jest.config.js | 15 +- lib/Innertube.js | 376 -- lib/Innertube.ts | 215 + lib/core/AccountManager.js | 221 - lib/core/AccountManager.ts | 151 + lib/core/Actions.js | 670 -- lib/core/Actions.ts | 699 ++ lib/core/Feed.js | 216 - lib/core/Feed.ts | 173 + .../{FilterableFeed.js => FilterableFeed.ts} | 55 +- lib/core/InteractionManager.js | 141 - lib/core/InteractionManager.ts | 93 + lib/core/Music.js | 207 - lib/core/Music.ts | 168 + lib/core/OAuth.js | 230 - lib/core/OAuth.ts | 207 + lib/core/Player.js | 172 - lib/core/Player.ts | 149 + ...{PlaylistManager.js => PlaylistManager.ts} | 107 +- lib/core/Session.ts | 220 + lib/core/SessionBuilder.js | 132 - lib/core/TabbedFeed.js | 42 - lib/core/TabbedFeed.ts | 30 + lib/deciphers/NToken.js | 451 -- lib/deciphers/NToken.ts | 426 ++ lib/deciphers/Signature.js | 157 - lib/deciphers/Signature.ts | 130 + lib/parser/README.md | 197 +- .../classes/AnalyticsMainAppKeyMetrics.js | 15 +- lib/parser/classes/AnalyticsVideo.js | 13 +- .../classes/AnalyticsVodCarouselCard.js | 13 +- lib/parser/classes/Author.js | 40 - lib/parser/classes/BackstageImage.js | 13 +- lib/parser/classes/BackstagePost.js | 19 +- lib/parser/classes/BackstagePostThread.js | 13 +- lib/parser/classes/BrowseFeedActions.js | 13 +- lib/parser/classes/Button.js | 19 +- lib/parser/classes/C4TabbedHeader.js | 19 +- lib/parser/classes/CallToActionButton.js | 13 +- lib/parser/classes/Card.js | 15 +- lib/parser/classes/CardCollection.js | 15 +- lib/parser/classes/Channel.js | 17 +- .../classes/ChannelAboutFullMetadata.js | 19 +- lib/parser/classes/ChannelFeaturedContent.js | 15 +- lib/parser/classes/ChannelHeaderLinks.js | 18 +- lib/parser/classes/ChannelMetadata.js | 13 +- lib/parser/classes/ChannelMobileHeader.js | 11 +- .../classes/ChannelThumbnailWithLink.js | 15 +- lib/parser/classes/ChannelVideoPlayer.js | 13 +- lib/parser/classes/ChildVideo.js | 19 +- lib/parser/classes/ChipCloud.js | 17 +- lib/parser/classes/ChipCloudChip.js | 17 +- lib/parser/classes/CollageHeroImage.js | 15 +- lib/parser/classes/CompactLink.js | 15 +- lib/parser/classes/CompactMix.js | 10 +- lib/parser/classes/CompactPlaylist.js | 10 +- lib/parser/classes/CompactVideo.js | 29 +- lib/parser/classes/ContinuationItem.js | 19 +- lib/parser/classes/CtaGoToCreatorStudio.js | 10 +- lib/parser/classes/DataModelSection.js | 11 +- lib/parser/classes/DidYouMean.js | 15 +- lib/parser/classes/DownloadButton.js | 13 +- lib/parser/classes/Element.js | 13 +- lib/parser/classes/EmergencyOnebox.js | 15 +- lib/parser/classes/EmojiRun.js | 16 +- lib/parser/classes/EndScreenPlaylist.js | 17 +- lib/parser/classes/EndScreenVideo.js | 21 +- lib/parser/classes/Endscreen.js | 13 +- lib/parser/classes/EndscreenElement.js | 26 +- lib/parser/classes/ExpandableTab.js | 15 +- lib/parser/classes/ExpandedShelfContents.js | 14 +- lib/parser/classes/FeedFilterChipBar.js | 13 +- lib/parser/classes/FeedTabbedHeader.js | 11 +- lib/parser/classes/Grid.js | 14 +- lib/parser/classes/GridChannel.js | 19 +- lib/parser/classes/GridPlaylist.js | 25 +- lib/parser/classes/GridVideo.js | 22 +- lib/parser/classes/HistorySuggestion.js | 10 +- lib/parser/classes/HorizontalCardList.js | 13 +- lib/parser/classes/HorizontalList.js | 14 +- lib/parser/classes/ItemSection.js | 16 +- lib/parser/classes/ItemSectionHeader.js | 11 +- lib/parser/classes/LikeButton.js | 15 +- lib/parser/classes/LiveChat.js | 18 +- lib/parser/classes/LiveChatAuthorBadge.js | 10 +- lib/parser/classes/LiveChatHeader.js | 13 +- lib/parser/classes/LiveChatItemList.js | 13 +- lib/parser/classes/LiveChatMessageInput.js | 15 +- lib/parser/classes/LiveChatParticipant.js | 17 +- .../classes/LiveChatParticipantsList.js | 15 +- lib/parser/classes/MerchandiseItem.js | 15 +- lib/parser/classes/MerchandiseShelf.js | 14 +- lib/parser/classes/Message.js | 13 +- lib/parser/classes/MetadataBadge.js | 16 +- lib/parser/classes/MetadataRow.js | 14 - lib/parser/classes/MetadataRow.ts | 15 + lib/parser/classes/MetadataRowContainer.js | 14 - lib/parser/classes/MetadataRowContainer.ts | 15 + lib/parser/classes/MetadataRowHeader.js | 13 +- lib/parser/classes/MicroformatData.js | 13 +- lib/parser/classes/Mix.js | 10 +- lib/parser/classes/Movie.js | 28 +- lib/parser/classes/MovingThumbnail.js | 13 +- lib/parser/classes/MusicCarouselShelf.js | 14 +- .../classes/MusicCarouselShelfBasicHeader.js | 18 +- lib/parser/classes/MusicDescriptionShelf.js | 16 +- lib/parser/classes/MusicDetailHeader.js | 20 +- lib/parser/classes/MusicHeader.js | 13 +- lib/parser/classes/MusicImmersiveHeader.js | 24 +- lib/parser/classes/MusicInlineBadge.js | 10 +- .../classes/MusicItemThumbnailOverlay.js | 13 +- lib/parser/classes/MusicNavigationButton.js | 15 +- lib/parser/classes/MusicPlayButton.js | 16 +- lib/parser/classes/MusicPlaylistShelf.js | 15 +- lib/parser/classes/MusicQueue.js | 13 +- lib/parser/classes/MusicResponsiveListItem.js | 167 - lib/parser/classes/MusicResponsiveListItem.ts | 174 + .../MusicResponsiveListItemFixedColumn.js | 13 +- .../MusicResponsiveListItemFlexColumn.js | 13 +- lib/parser/classes/MusicShelf.js | 22 +- lib/parser/classes/MusicThumbnail.js | 13 +- lib/parser/classes/MusicTwoRowItem.js | 37 +- ...ationEndpoint.js => NavigationEndpoint.ts} | 137 +- lib/parser/classes/Notification.js | 19 +- .../classes/PlayerAnnotationsExpanded.js | 17 +- lib/parser/classes/PlayerCaptionsTracklist.js | 15 +- lib/parser/classes/PlayerErrorMessage.js | 17 +- .../classes/PlayerLiveStoryboardSpec.js | 11 +- lib/parser/classes/PlayerMicroformat.js | 21 +- lib/parser/classes/PlayerOverlay.js | 13 +- lib/parser/classes/PlayerOverlayAutoplay.js | 19 +- lib/parser/classes/PlayerStoryboardSpec.js | 26 +- lib/parser/classes/Playlist.js | 24 +- lib/parser/classes/PlaylistHeader.js | 17 +- lib/parser/classes/PlaylistInfoCardContent.js | 17 +- lib/parser/classes/PlaylistMetadata.js | 10 +- lib/parser/classes/PlaylistPanel.js | 15 +- lib/parser/classes/PlaylistPanelVideo.js | 29 +- lib/parser/classes/PlaylistSidebar.js | 14 +- .../classes/PlaylistSidebarPrimaryInfo.js | 17 +- .../classes/PlaylistSidebarSecondaryInfo.js | 13 +- lib/parser/classes/PlaylistVideo.js | 21 +- lib/parser/classes/PlaylistVideoList.js | 13 +- lib/parser/classes/PlaylistVideoThumbnail.js | 13 +- lib/parser/classes/Poll.js | 20 +- lib/parser/classes/Post.js | 10 +- lib/parser/classes/ProfileColumn.js | 14 +- lib/parser/classes/ProfileColumnStats.js | 14 +- lib/parser/classes/ProfileColumnStatsEntry.js | 13 +- lib/parser/classes/ProfileColumnUserInfo.js | 15 +- lib/parser/classes/ReelItem.js | 17 +- lib/parser/classes/ReelShelf.js | 18 +- lib/parser/classes/RelatedChipCloud.js | 13 +- lib/parser/classes/RichGrid.js | 13 +- lib/parser/classes/RichItem.js | 13 +- lib/parser/classes/RichListHeader.js | 11 +- lib/parser/classes/RichSection.js | 13 +- lib/parser/classes/RichShelf.js | 17 +- lib/parser/classes/SearchBox.js | 17 +- lib/parser/classes/SearchRefinementCard.js | 17 +- lib/parser/classes/SearchSuggestion.js | 16 +- .../classes/SearchSuggestionsSection.js | 13 +- .../classes/SecondarySearchContainer.js | 13 +- lib/parser/classes/SectionList.js | 14 +- lib/parser/classes/Shelf.js | 21 +- lib/parser/classes/ShowingResultsFor.js | 15 +- lib/parser/classes/SimpleCardTeaser.js | 13 +- .../classes/SingleActionEmergencySupport.js | 15 +- .../classes/SingleColumnBrowseResults.js | 13 +- .../SingleColumnMusicWatchNextResults.js | 13 +- lib/parser/classes/SingleHeroImage.js | 13 +- lib/parser/classes/SortFilterSubMenu.js | 15 +- lib/parser/classes/SubFeedOption.js | 15 +- lib/parser/classes/SubFeedSelector.js | 15 +- lib/parser/classes/SubscribeButton.js | 17 +- .../SubscriptionNotificationToggleButton.js | 14 +- lib/parser/classes/Tab.js | 17 - lib/parser/classes/Tab.ts | 20 + lib/parser/classes/Tabbed.js | 13 +- lib/parser/classes/TabbedSearchResults.js | 13 +- lib/parser/classes/Text.js | 27 - lib/parser/classes/TextHeader.js | 13 +- lib/parser/classes/TextRun.js | 12 - .../classes/ThumbnailOverlayBottomPanel.js | 10 +- .../classes/ThumbnailOverlayEndorsement.js | 13 +- .../classes/ThumbnailOverlayHoverText.js | 13 +- .../ThumbnailOverlayInlineUnplayable.js | 13 +- .../classes/ThumbnailOverlayLoadingPreview.js | 13 +- .../classes/ThumbnailOverlayNowPlaying.js | 13 +- lib/parser/classes/ThumbnailOverlayPinking.js | 10 +- .../classes/ThumbnailOverlayPlaybackStatus.js | 13 +- .../classes/ThumbnailOverlayResumePlayback.js | 10 +- .../classes/ThumbnailOverlaySidePanel.js | 13 +- .../classes/ThumbnailOverlayTimeStatus.js | 13 +- .../classes/ThumbnailOverlayToggleButton.js | 16 +- lib/parser/classes/ToggleButton.js | 33 +- lib/parser/classes/ToggleMenuServiceItem.js | 15 +- lib/parser/classes/Tooltip.js | 16 +- lib/parser/classes/TwoColumnBrowseResults.js | 13 +- lib/parser/classes/TwoColumnSearchResults.js | 13 +- .../classes/TwoColumnWatchNextResults.js | 17 +- lib/parser/classes/UniversalWatchCard.js | 13 +- lib/parser/classes/VerticalList.js | 16 +- lib/parser/classes/VerticalWatchCardList.js | 17 +- lib/parser/classes/Video.js | 41 +- lib/parser/classes/VideoInfoCardContent.js | 17 +- lib/parser/classes/VideoOwner.js | 15 +- lib/parser/classes/VideoPrimaryInfo.js | 19 - lib/parser/classes/VideoPrimaryInfo.ts | 25 + lib/parser/classes/VideoSecondaryInfo.js | 18 +- lib/parser/classes/WatchCardCompactVideo.js | 15 +- lib/parser/classes/WatchCardHeroVideo.js | 15 +- lib/parser/classes/WatchCardRichHeader.js | 17 +- .../classes/WatchCardSectionSequence.js | 13 +- lib/parser/classes/WatchNextEndScreen.js | 13 +- lib/parser/classes/WatchNextTabbedResults.js | 10 +- .../actions/AppendContinuationItemsAction.js | 13 +- lib/parser/classes/actions/OpenPopupAction.js | 13 +- .../classes/comments/AuthorCommentBadge.js | 18 +- lib/parser/classes/comments/Comment.js | 59 +- .../classes/comments/CommentActionButtons.js | 13 +- lib/parser/classes/comments/CommentReplies.js | 13 +- .../classes/comments/CommentReplyDialog.js | 17 +- .../classes/comments/CommentSimplebox.js | 17 +- lib/parser/classes/comments/CommentThread.js | 74 - lib/parser/classes/comments/CommentThread.ts | 63 + .../comments/CommentsEntryPointHeader.js | 15 +- lib/parser/classes/comments/CommentsHeader.js | 20 +- .../livechat/AddBannerToLiveChatCommand.js | 11 +- .../classes/livechat/AddChatItemAction.js | 15 +- .../livechat/AddLiveChatTickerItemAction.js | 15 +- .../classes/livechat/LiveChatActionPanel.js | 13 +- .../livechat/MarkChatItemAsDeletedAction.js | 13 +- .../MarkChatItemsByAuthorAsDeletedAction.js | 13 +- .../RemoveBannerForLiveChatCommand.js | 10 +- .../classes/livechat/ReplaceChatItemAction.js | 11 +- .../classes/livechat/ReplayChatItemAction.js | 14 +- .../livechat/ShowLiveChatActionPanelAction.js | 13 +- .../livechat/ShowLiveChatTooltipCommand.js | 13 +- .../classes/livechat/UpdateDateTextAction.js | 13 +- .../livechat/UpdateDescriptionAction.js | 13 +- .../livechat/UpdateLiveChatPollAction.js | 13 +- .../classes/livechat/UpdateTitleAction.js | 13 +- .../livechat/UpdateToggleButtonTextAction.js | 13 +- .../livechat/UpdateViewershipAction.js | 14 +- .../classes/livechat/items/LiveChatBanner.js | 14 +- .../livechat/items/LiveChatBannerHeader.js | 16 +- .../livechat/items/LiveChatBannerPoll.js | 20 +- .../livechat/items/LiveChatMembershipItem.js | 22 +- .../livechat/items/LiveChatPaidMessage.js | 25 +- .../livechat/items/LiveChatPaidSticker.js | 22 +- .../livechat/items/LiveChatPlaceholderItem.js | 11 +- .../livechat/items/LiveChatTextMessage.js | 25 +- .../items/LiveChatTickerPaidMessageItem.js | 25 +- .../items/LiveChatTickerSponsorItem.js | 21 +- .../items/LiveChatViewerEngagementMessage.js | 14 +- .../classes/livechat/items/PollHeader.js | 18 +- lib/parser/classes/menus/Menu.js | 20 - lib/parser/classes/menus/Menu.ts | 21 + .../classes/menus/MenuNavigationItem.js | 10 +- lib/parser/classes/menus/MenuServiceItem.js | 10 +- .../classes/menus/MenuServiceItemDownload.js | 13 +- lib/parser/classes/menus/MultiPageMenu.js | 13 +- .../menus/MultiPageMenuNotificationSection.js | 14 +- lib/parser/classes/menus/SimpleMenuHeader.js | 15 +- lib/parser/classes/misc/Author.js | 32 + lib/parser/classes/{ => misc}/Format.js | 15 +- .../classes/{ => misc}/NavigatableText.js | 13 +- .../classes/{ => misc}/PlaylistAuthor.js | 9 +- lib/parser/classes/misc/Text.ts | 22 + lib/parser/classes/misc/TextRun.js | 9 + lib/parser/classes/{ => misc}/Thumbnail.js | 11 +- lib/parser/classes/{ => misc}/VideoDetails.js | 8 +- lib/parser/helpers.ts | 445 ++ lib/parser/index.js | 308 - lib/parser/index.ts | 303 + lib/parser/map.js | 18 - lib/parser/map.ts | 476 ++ lib/parser/youtube/Analytics.js | 10 +- lib/parser/youtube/Channel.js | 18 +- lib/parser/youtube/Comments.js | 77 - lib/parser/youtube/Comments.ts | 64 + lib/parser/youtube/History.js | 12 +- lib/parser/youtube/Library.js | 33 +- lib/parser/youtube/LiveChat.js | 165 - lib/parser/youtube/LiveChat.ts | 142 + lib/parser/youtube/NotificationsMenu.js | 19 +- lib/parser/youtube/Playlist.js | 44 - lib/parser/youtube/Playlist.ts | 47 + lib/parser/youtube/Search.js | 32 +- lib/parser/youtube/VideoInfo.js | 531 -- lib/parser/youtube/VideoInfo.ts | 527 ++ lib/parser/ytmusic/Album.js | 14 +- lib/parser/ytmusic/Artist.js | 18 +- lib/parser/ytmusic/Explore.js | 13 +- lib/parser/ytmusic/HomeFeed.js | 15 +- lib/parser/ytmusic/Library.js | 9 +- lib/parser/ytmusic/Search.js | 147 - lib/parser/ytmusic/Search.ts | 110 + lib/proto/index.js | 55 +- lib/proto/messages.js | 5694 ++++++++--------- lib/utils/Cache.ts | 241 + lib/utils/Constants.js | 112 - lib/utils/Constants.ts | 122 + lib/utils/EventEmitterLike.ts | 60 + lib/utils/HTTPClient.ts | 134 + lib/utils/Request.js | 154 - lib/utils/Utils.js | 347 - lib/utils/Utils.ts | 341 + lib/utils/index.ts | 6 + lib/utils/wrappers/BrowserCache.js | 50 - lib/utils/wrappers/NodeCache.js | 53 - package-lock.json | 4584 ++++++------- package.json | 57 +- scripts/build-parser-json.js | 50 +- scripts/globals.js | 7 - test/browser/main.test.js | 122 - test/{node => }/main.test.js | 25 +- tsconfig.json | 116 +- typings/index.d.ts | 2 - typings/lib/Innertube.d.ts | 226 - typings/lib/core/AccountManager.d.ts | 168 - typings/lib/core/Actions.d.ts | 331 - typings/lib/core/Feed.d.ts | 89 - typings/lib/core/FilterableFeed.d.ts | 19 - typings/lib/core/InteractionManager.d.ts | 113 - typings/lib/core/Music.d.ts | 101 - typings/lib/core/OAuth.d.ts | 47 - typings/lib/core/Player.d.ts | 31 - typings/lib/core/PlaylistManager.d.ts | 55 - typings/lib/core/SessionBuilder.d.ts | 23 - typings/lib/core/TabbedFeed.d.ts | 12 - typings/lib/deciphers/NToken.d.ts | 81 - typings/lib/deciphers/Signature.d.ts | 24 - .../classes/AnalyticsMainAppKeyMetrics.d.ts | 7 - .../lib/parser/classes/AnalyticsVideo.d.ts | 14 - .../classes/AnalyticsVodCarouselCard.d.ts | 7 - typings/lib/parser/classes/Author.d.ts | 18 - .../lib/parser/classes/BackstageImage.d.ts | 7 - typings/lib/parser/classes/BackstagePost.d.ts | 21 - .../parser/classes/BackstagePostThread.d.ts | 6 - .../lib/parser/classes/BrowseFeedActions.d.ts | 6 - typings/lib/parser/classes/Button.d.ts | 11 - .../lib/parser/classes/C4TabbedHeader.d.ts | 16 - .../parser/classes/CallToActionButton.d.ts | 9 - typings/lib/parser/classes/Card.d.ts | 10 - .../lib/parser/classes/CardCollection.d.ts | 9 - typings/lib/parser/classes/Channel.d.ts | 14 - .../classes/ChannelAboutFullMetadata.d.ts | 19 - .../classes/ChannelFeaturedContent.d.ts | 8 - .../parser/classes/ChannelHeaderLinks.d.ts | 7 - .../lib/parser/classes/ChannelMetadata.d.ts | 19 - .../parser/classes/ChannelMobileHeader.d.ts | 6 - .../classes/ChannelThumbnailWithLink.d.ts | 10 - .../parser/classes/ChannelVideoPlayer.d.ts | 11 - typings/lib/parser/classes/ChildVideo.d.ts | 14 - typings/lib/parser/classes/ChipCloud.d.ts | 9 - typings/lib/parser/classes/ChipCloudChip.d.ts | 9 - .../lib/parser/classes/CollageHeroImage.d.ts | 11 - typings/lib/parser/classes/CompactLink.d.ts | 9 - typings/lib/parser/classes/CompactMix.d.ts | 4 - .../lib/parser/classes/CompactPlaylist.d.ts | 4 - typings/lib/parser/classes/CompactVideo.d.ts | 25 - .../lib/parser/classes/ContinuationItem.d.ts | 9 - .../parser/classes/CtaGoToCreatorStudio.d.ts | 7 - .../lib/parser/classes/DataModelSection.d.ts | 16 - typings/lib/parser/classes/DidYouMean.d.ts | 9 - .../lib/parser/classes/DownloadButton.d.ts | 10 - typings/lib/parser/classes/Element.d.ts | 5 - .../lib/parser/classes/EmergencyOnebox.d.ts | 9 - typings/lib/parser/classes/EmojiRun.d.ts | 12 - .../lib/parser/classes/EndScreenPlaylist.d.ts | 14 - .../lib/parser/classes/EndScreenVideo.d.ts | 21 - typings/lib/parser/classes/Endscreen.d.ts | 7 - .../lib/parser/classes/EndscreenElement.d.ts | 25 - typings/lib/parser/classes/ExpandableTab.d.ts | 10 - .../parser/classes/ExpandedShelfContents.d.ts | 7 - .../lib/parser/classes/FeedFilterChipBar.d.ts | 6 - .../lib/parser/classes/FeedTabbedHeader.d.ts | 6 - typings/lib/parser/classes/Format.d.ts | 40 - typings/lib/parser/classes/Grid.d.ts | 10 - typings/lib/parser/classes/GridChannel.d.ts | 14 - typings/lib/parser/classes/GridPlaylist.d.ts | 21 - typings/lib/parser/classes/GridVideo.d.ts | 21 - .../lib/parser/classes/HistorySuggestion.d.ts | 4 - .../parser/classes/HorizontalCardList.d.ts | 9 - .../lib/parser/classes/HorizontalList.d.ts | 8 - typings/lib/parser/classes/ItemSection.d.ts | 8 - .../lib/parser/classes/ItemSectionHeader.d.ts | 6 - typings/lib/parser/classes/LikeButton.d.ts | 11 - typings/lib/parser/classes/LiveChat.d.ts | 17 - .../parser/classes/LiveChatAuthorBadge.d.ts | 6 - .../lib/parser/classes/LiveChatHeader.d.ts | 8 - .../lib/parser/classes/LiveChatItemList.d.ts | 7 - .../parser/classes/LiveChatMessageInput.d.ts | 10 - .../parser/classes/LiveChatParticipant.d.ts | 10 - .../classes/LiveChatParticipantsList.d.ts | 8 - .../lib/parser/classes/MerchandiseItem.d.ts | 18 - .../lib/parser/classes/MerchandiseShelf.d.ts | 9 - typings/lib/parser/classes/Message.d.ts | 6 - typings/lib/parser/classes/MetadataBadge.d.ts | 7 - typings/lib/parser/classes/MetadataRow.d.ts | 8 - .../parser/classes/MetadataRowContainer.d.ts | 7 - .../lib/parser/classes/MetadataRowHeader.d.ts | 8 - .../lib/parser/classes/MicroformatData.d.ts | 29 - typings/lib/parser/classes/Mix.d.ts | 4 - typings/lib/parser/classes/Movie.d.ts | 25 - .../lib/parser/classes/MovingThumbnail.d.ts | 5 - .../parser/classes/MusicCarouselShelf.d.ts | 8 - .../MusicCarouselShelfBasicHeader.d.ts | 9 - .../parser/classes/MusicDescriptionShelf.d.ts | 10 - .../lib/parser/classes/MusicDetailHeader.d.ts | 22 - typings/lib/parser/classes/MusicHeader.d.ts | 6 - .../parser/classes/MusicImmersiveHeader.d.ts | 9 - .../lib/parser/classes/MusicInlineBadge.d.ts | 7 - .../classes/MusicItemThumbnailOverlay.d.ts | 8 - .../parser/classes/MusicNavigationButton.d.ts | 8 - .../lib/parser/classes/MusicPlayButton.d.ts | 12 - .../parser/classes/MusicPlaylistShelf.d.ts | 10 - typings/lib/parser/classes/MusicQueue.d.ts | 6 - .../classes/MusicResponsiveListItem.d.ts | 45 - .../MusicResponsiveListItemFixedColumn.d.ts | 8 - .../MusicResponsiveListItemFlexColumn.d.ts | 8 - typings/lib/parser/classes/MusicShelf.d.ts | 12 - .../lib/parser/classes/MusicThumbnail.d.ts | 5 - .../lib/parser/classes/MusicTwoRowItem.d.ts | 26 - .../lib/parser/classes/NavigatableText.d.ts | 8 - .../parser/classes/NavigationEndpoint.d.ts | 87 - typings/lib/parser/classes/Notification.d.ts | 17 - .../classes/PlayerAnnotationsExpanded.d.ts | 17 - .../classes/PlayerCaptionsTracklist.d.ts | 8 - .../parser/classes/PlayerErrorMessage.d.ts | 12 - .../classes/PlayerLiveStoryboardSpec.d.ts | 4 - .../lib/parser/classes/PlayerMicroformat.d.ts | 31 - typings/lib/parser/classes/PlayerOverlay.d.ts | 10 - .../parser/classes/PlayerOverlayAutoplay.d.ts | 21 - .../parser/classes/PlayerStoryboardSpec.d.ts | 6 - typings/lib/parser/classes/Playlist.d.ts | 21 - .../lib/parser/classes/PlaylistAuthor.d.ts | 5 - .../lib/parser/classes/PlaylistHeader.d.ts | 22 - .../classes/PlaylistInfoCardContent.d.ts | 13 - .../lib/parser/classes/PlaylistMetadata.d.ts | 7 - typings/lib/parser/classes/PlaylistPanel.d.ts | 15 - .../parser/classes/PlaylistPanelVideo.d.ts | 28 - .../lib/parser/classes/PlaylistSidebar.d.ts | 7 - .../classes/PlaylistSidebarPrimaryInfo.d.ts | 13 - .../classes/PlaylistSidebarSecondaryInfo.d.ts | 7 - typings/lib/parser/classes/PlaylistVideo.d.ts | 23 - .../lib/parser/classes/PlaylistVideoList.d.ts | 9 - .../classes/PlaylistVideoThumbnail.d.ts | 7 - typings/lib/parser/classes/Poll.d.ts | 10 - typings/lib/parser/classes/Post.d.ts | 4 - typings/lib/parser/classes/ProfileColumn.d.ts | 7 - .../parser/classes/ProfileColumnStats.d.ts | 7 - .../classes/ProfileColumnStatsEntry.d.ts | 8 - .../parser/classes/ProfileColumnUserInfo.d.ts | 9 - typings/lib/parser/classes/ReelItem.d.ts | 13 - typings/lib/parser/classes/ReelShelf.d.ts | 11 - .../lib/parser/classes/RelatedChipCloud.d.ts | 6 - typings/lib/parser/classes/RichGrid.d.ts | 7 - typings/lib/parser/classes/RichItem.d.ts | 5 - .../lib/parser/classes/RichListHeader.d.ts | 7 - typings/lib/parser/classes/RichSection.d.ts | 6 - typings/lib/parser/classes/RichShelf.d.ts | 10 - typings/lib/parser/classes/SearchBox.d.ts | 11 - .../parser/classes/SearchRefinementCard.d.ts | 10 - .../lib/parser/classes/SearchSuggestion.d.ts | 11 - .../classes/SearchSuggestionsSection.d.ts | 6 - .../classes/SecondarySearchContainer.d.ts | 6 - typings/lib/parser/classes/SectionList.d.ts | 9 - typings/lib/parser/classes/Shelf.d.ts | 12 - .../lib/parser/classes/ShowingResultsFor.d.ts | 10 - .../lib/parser/classes/SimpleCardTeaser.d.ts | 8 - .../classes/SingleActionEmergencySupport.d.ts | 12 - .../classes/SingleColumnBrowseResults.d.ts | 6 - .../SingleColumnMusicWatchNextResults.d.ts | 5 - .../lib/parser/classes/SingleHeroImage.d.ts | 7 - .../lib/parser/classes/SortFilterSubMenu.d.ts | 7 - typings/lib/parser/classes/SubFeedOption.d.ts | 10 - .../lib/parser/classes/SubFeedSelector.d.ts | 8 - .../lib/parser/classes/SubscribeButton.d.ts | 16 - .../SubscriptionNotificationToggleButton.d.ts | 8 - typings/lib/parser/classes/Tab.d.ts | 10 - typings/lib/parser/classes/Tabbed.d.ts | 5 - .../parser/classes/TabbedSearchResults.d.ts | 6 - typings/lib/parser/classes/Text.d.ts | 7 - typings/lib/parser/classes/TextHeader.d.ts | 8 - typings/lib/parser/classes/TextRun.d.ts | 6 - typings/lib/parser/classes/Thumbnail.d.ts | 27 - .../classes/ThumbnailOverlayBottomPanel.d.ts | 5 - .../classes/ThumbnailOverlayEndorsement.d.ts | 6 - .../classes/ThumbnailOverlayHoverText.d.ts | 7 - .../ThumbnailOverlayInlineUnplayable.d.ts | 7 - .../ThumbnailOverlayLoadingPreview.d.ts | 7 - .../classes/ThumbnailOverlayNowPlaying.d.ts | 6 - .../classes/ThumbnailOverlayPinking.d.ts | 6 - .../ThumbnailOverlayPlaybackStatus.d.ts | 6 - .../ThumbnailOverlayResumePlayback.d.ts | 6 - .../classes/ThumbnailOverlaySidePanel.d.ts | 7 - .../classes/ThumbnailOverlayTimeStatus.d.ts | 6 - .../classes/ThumbnailOverlayToggleButton.d.ts | 17 - typings/lib/parser/classes/ToggleButton.d.ts | 20 - .../parser/classes/ToggleMenuServiceItem.d.ts | 12 - typings/lib/parser/classes/Tooltip.d.ts | 18 - .../classes/TwoColumnBrowseResults.d.ts | 7 - .../classes/TwoColumnSearchResults.d.ts | 7 - .../classes/TwoColumnWatchNextResults.d.ts | 8 - .../parser/classes/UniversalWatchCard.d.ts | 8 - typings/lib/parser/classes/VerticalList.d.ts | 10 - .../parser/classes/VerticalWatchCardList.d.ts | 11 - typings/lib/parser/classes/Video.d.ts | 49 - typings/lib/parser/classes/VideoDetails.d.ts | 37 - .../parser/classes/VideoInfoCardContent.d.ts | 14 - typings/lib/parser/classes/VideoOwner.d.ts | 10 - .../lib/parser/classes/VideoPrimaryInfo.d.ts | 12 - .../parser/classes/VideoSecondaryInfo.d.ts | 14 - .../parser/classes/WatchCardCompactVideo.d.ts | 13 - .../parser/classes/WatchCardHeroVideo.d.ts | 10 - .../parser/classes/WatchCardRichHeader.d.ts | 13 - .../classes/WatchCardSectionSequence.d.ts | 6 - .../parser/classes/WatchNextEndScreen.d.ts | 6 - .../classes/WatchNextTabbedResults.d.ts | 4 - .../AppendContinuationItemsAction.d.ts | 7 - .../classes/actions/OpenPopupAction.d.ts | 7 - .../classes/comments/AuthorCommentBadge.d.ts | 10 - .../lib/parser/classes/comments/Comment.d.ts | 69 - .../comments/CommentActionButtons.d.ts | 8 - .../classes/comments/CommentReplies.d.ts | 8 - .../classes/comments/CommentReplyDialog.d.ts | 12 - .../classes/comments/CommentSimplebox.d.ts | 12 - .../classes/comments/CommentThread.d.ts | 26 - .../comments/CommentsEntryPointHeader.d.ts | 12 - .../classes/comments/CommentsHeader.d.ts | 12 - .../livechat/AddBannerToLiveChatCommand.d.ts | 4 - .../classes/livechat/AddChatItemAction.d.ts | 7 - .../livechat/AddLiveChatTickerItemAction.d.ts | 7 - .../classes/livechat/LiveChatActionPanel.d.ts | 8 - .../livechat/MarkChatItemAsDeletedAction.d.ts | 8 - .../MarkChatItemsByAuthorAsDeletedAction.d.ts | 8 - .../RemoveBannerForLiveChatCommand.d.ts | 6 - .../livechat/ReplaceChatItemAction.d.ts | 6 - .../livechat/ReplayChatItemAction.d.ts | 7 - .../ShowLiveChatActionPanelAction.d.ts | 6 - .../livechat/ShowLiveChatTooltipCommand.d.ts | 6 - .../livechat/UpdateDateTextAction.d.ts | 6 - .../livechat/UpdateDescriptionAction.d.ts | 7 - .../livechat/UpdateLiveChatPollAction.d.ts | 6 - .../classes/livechat/UpdateTitleAction.d.ts | 7 - .../UpdateToggleButtonTextAction.d.ts | 8 - .../livechat/UpdateViewershipAction.d.ts | 9 - .../livechat/items/LiveChatBanner.d.ts | 12 - .../livechat/items/LiveChatBannerHeader.d.ts | 8 - .../livechat/items/LiveChatBannerPoll.d.ts | 13 - .../items/LiveChatMembershipItem.d.ts | 18 - .../livechat/items/LiveChatPaidMessage.d.ts | 20 - .../livechat/items/LiveChatPaidSticker.d.ts | 19 - .../items/LiveChatPlaceholderItem.d.ts | 7 - .../livechat/items/LiveChatTextMessage.d.ts | 17 - .../items/LiveChatTickerPaidMessageItem.d.ts | 19 - .../items/LiveChatTickerSponsorItem.d.ts | 15 - .../LiveChatViewerEngagementMessage.d.ts | 6 - .../classes/livechat/items/PollHeader.d.ts | 12 - typings/lib/parser/classes/menus/Menu.d.ts | 9 - .../classes/menus/MenuNavigationItem.d.ts | 4 - .../parser/classes/menus/MenuServiceItem.d.ts | 4 - .../menus/MenuServiceItemDownload.d.ts | 8 - .../parser/classes/menus/MultiPageMenu.d.ts | 8 - .../MultiPageMenuNotificationSection.d.ts | 7 - .../classes/menus/SimpleMenuHeader.d.ts | 8 - typings/lib/parser/index.d.ts | 58 - typings/lib/parser/map.d.ts | 2 - typings/lib/parser/youtube/Analytics.d.ts | 11 - typings/lib/parser/youtube/Channel.d.ts | 29 - typings/lib/parser/youtube/Comments.d.ts | 25 - typings/lib/parser/youtube/History.d.ts | 21 - typings/lib/parser/youtube/Library.d.ts | 30 - typings/lib/parser/youtube/LiveChat.d.ts | 33 - .../lib/parser/youtube/NotificationsMenu.d.ts | 19 - typings/lib/parser/youtube/Playlist.d.ts | 12 - typings/lib/parser/youtube/Search.d.ts | 44 - typings/lib/parser/youtube/VideoInfo.d.ts | 154 - typings/lib/parser/ytmusic/Album.d.ts | 18 - typings/lib/parser/ytmusic/Artist.d.ts | 15 - typings/lib/parser/ytmusic/Explore.d.ts | 12 - typings/lib/parser/ytmusic/HomeFeed.d.ts | 19 - typings/lib/parser/ytmusic/Library.d.ts | 10 - typings/lib/parser/ytmusic/Search.d.ts | 59 - typings/lib/proto/index.d.ts | 120 - typings/lib/proto/messages.d.ts | 116 - typings/lib/utils/Constants.d.ts | 98 - typings/lib/utils/Request.d.ts | 13 - typings/lib/utils/Utils.d.ts | 107 - typings/lib/utils/wrappers/BrowserCache.d.ts | 26 - typings/lib/utils/wrappers/NodeCache.d.ts | 25 - 623 files changed, 13531 insertions(+), 18560 deletions(-) create mode 100644 browser.ts delete mode 100644 build/browser.d.ts delete mode 100644 build/browser.js delete mode 100644 build/browser.js.map delete mode 100644 build/node.d.ts delete mode 100644 build/node.js delete mode 100644 build/node.js.map create mode 100644 bundle/browser.d.ts create mode 100644 examples/browser/README.md create mode 100644 examples/browser/proxy/deno.ts create mode 100644 examples/browser/web/.gitignore create mode 100644 examples/browser/web/favicon.svg create mode 100644 examples/browser/web/index.html create mode 100644 examples/browser/web/package.json create mode 100644 examples/browser/web/public/service-worker.js create mode 100644 examples/browser/web/public/service-worker.js.map create mode 100644 examples/browser/web/public/vite.svg create mode 100644 examples/browser/web/src/main.ts create mode 100644 examples/browser/web/src/style.css create mode 100644 examples/browser/web/src/vite-env.d.ts create mode 100644 examples/browser/web/tsconfig.json create mode 100644 examples/deno/README.md create mode 100644 examples/deno/index.ts delete mode 100644 index.js create mode 100644 index.ts delete mode 100644 lib/Innertube.js create mode 100644 lib/Innertube.ts delete mode 100644 lib/core/AccountManager.js create mode 100644 lib/core/AccountManager.ts delete mode 100644 lib/core/Actions.js create mode 100644 lib/core/Actions.ts delete mode 100644 lib/core/Feed.js create mode 100644 lib/core/Feed.ts rename lib/core/{FilterableFeed.js => FilterableFeed.ts} (51%) delete mode 100644 lib/core/InteractionManager.js create mode 100644 lib/core/InteractionManager.ts delete mode 100644 lib/core/Music.js create mode 100644 lib/core/Music.ts delete mode 100644 lib/core/OAuth.js create mode 100644 lib/core/OAuth.ts delete mode 100644 lib/core/Player.js create mode 100644 lib/core/Player.ts rename lib/core/{PlaylistManager.js => PlaylistManager.ts} (65%) create mode 100644 lib/core/Session.ts delete mode 100644 lib/core/SessionBuilder.js delete mode 100644 lib/core/TabbedFeed.js create mode 100644 lib/core/TabbedFeed.ts delete mode 100644 lib/deciphers/NToken.js create mode 100644 lib/deciphers/NToken.ts delete mode 100644 lib/deciphers/Signature.js create mode 100644 lib/deciphers/Signature.ts delete mode 100644 lib/parser/classes/Author.js delete mode 100644 lib/parser/classes/MetadataRow.js create mode 100644 lib/parser/classes/MetadataRow.ts delete mode 100644 lib/parser/classes/MetadataRowContainer.js create mode 100644 lib/parser/classes/MetadataRowContainer.ts delete mode 100644 lib/parser/classes/MusicResponsiveListItem.js create mode 100644 lib/parser/classes/MusicResponsiveListItem.ts rename lib/parser/classes/{NavigationEndpoint.js => NavigationEndpoint.ts} (68%) delete mode 100644 lib/parser/classes/Tab.js create mode 100644 lib/parser/classes/Tab.ts delete mode 100644 lib/parser/classes/Text.js delete mode 100644 lib/parser/classes/TextRun.js delete mode 100644 lib/parser/classes/VideoPrimaryInfo.js create mode 100644 lib/parser/classes/VideoPrimaryInfo.ts delete mode 100644 lib/parser/classes/comments/CommentThread.js create mode 100644 lib/parser/classes/comments/CommentThread.ts delete mode 100644 lib/parser/classes/menus/Menu.js create mode 100644 lib/parser/classes/menus/Menu.ts create mode 100644 lib/parser/classes/misc/Author.js rename lib/parser/classes/{ => misc}/Format.js (87%) rename lib/parser/classes/{ => misc}/NavigatableText.js (76%) rename lib/parser/classes/{ => misc}/PlaylistAuthor.js (66%) create mode 100644 lib/parser/classes/misc/Text.ts create mode 100644 lib/parser/classes/misc/TextRun.js rename lib/parser/classes/{ => misc}/Thumbnail.js (79%) rename lib/parser/classes/{ => misc}/VideoDetails.js (91%) create mode 100644 lib/parser/helpers.ts delete mode 100644 lib/parser/index.js create mode 100644 lib/parser/index.ts delete mode 100644 lib/parser/map.js create mode 100644 lib/parser/map.ts delete mode 100644 lib/parser/youtube/Comments.js create mode 100644 lib/parser/youtube/Comments.ts delete mode 100644 lib/parser/youtube/LiveChat.js create mode 100644 lib/parser/youtube/LiveChat.ts delete mode 100644 lib/parser/youtube/Playlist.js create mode 100644 lib/parser/youtube/Playlist.ts delete mode 100644 lib/parser/youtube/VideoInfo.js create mode 100644 lib/parser/youtube/VideoInfo.ts delete mode 100644 lib/parser/ytmusic/Search.js create mode 100644 lib/parser/ytmusic/Search.ts create mode 100644 lib/utils/Cache.ts delete mode 100644 lib/utils/Constants.js create mode 100644 lib/utils/Constants.ts create mode 100644 lib/utils/EventEmitterLike.ts create mode 100644 lib/utils/HTTPClient.ts delete mode 100644 lib/utils/Request.js delete mode 100644 lib/utils/Utils.js create mode 100644 lib/utils/Utils.ts create mode 100644 lib/utils/index.ts delete mode 100644 lib/utils/wrappers/BrowserCache.js delete mode 100644 lib/utils/wrappers/NodeCache.js delete mode 100644 scripts/globals.js delete mode 100644 test/browser/main.test.js rename test/{node => }/main.test.js (86%) delete mode 100644 typings/index.d.ts delete mode 100644 typings/lib/Innertube.d.ts delete mode 100644 typings/lib/core/AccountManager.d.ts delete mode 100644 typings/lib/core/Actions.d.ts delete mode 100644 typings/lib/core/Feed.d.ts delete mode 100644 typings/lib/core/FilterableFeed.d.ts delete mode 100644 typings/lib/core/InteractionManager.d.ts delete mode 100644 typings/lib/core/Music.d.ts delete mode 100644 typings/lib/core/OAuth.d.ts delete mode 100644 typings/lib/core/Player.d.ts delete mode 100644 typings/lib/core/PlaylistManager.d.ts delete mode 100644 typings/lib/core/SessionBuilder.d.ts delete mode 100644 typings/lib/core/TabbedFeed.d.ts delete mode 100644 typings/lib/deciphers/NToken.d.ts delete mode 100644 typings/lib/deciphers/Signature.d.ts delete mode 100644 typings/lib/parser/classes/AnalyticsMainAppKeyMetrics.d.ts delete mode 100644 typings/lib/parser/classes/AnalyticsVideo.d.ts delete mode 100644 typings/lib/parser/classes/AnalyticsVodCarouselCard.d.ts delete mode 100644 typings/lib/parser/classes/Author.d.ts delete mode 100644 typings/lib/parser/classes/BackstageImage.d.ts delete mode 100644 typings/lib/parser/classes/BackstagePost.d.ts delete mode 100644 typings/lib/parser/classes/BackstagePostThread.d.ts delete mode 100644 typings/lib/parser/classes/BrowseFeedActions.d.ts delete mode 100644 typings/lib/parser/classes/Button.d.ts delete mode 100644 typings/lib/parser/classes/C4TabbedHeader.d.ts delete mode 100644 typings/lib/parser/classes/CallToActionButton.d.ts delete mode 100644 typings/lib/parser/classes/Card.d.ts delete mode 100644 typings/lib/parser/classes/CardCollection.d.ts delete mode 100644 typings/lib/parser/classes/Channel.d.ts delete mode 100644 typings/lib/parser/classes/ChannelAboutFullMetadata.d.ts delete mode 100644 typings/lib/parser/classes/ChannelFeaturedContent.d.ts delete mode 100644 typings/lib/parser/classes/ChannelHeaderLinks.d.ts delete mode 100644 typings/lib/parser/classes/ChannelMetadata.d.ts delete mode 100644 typings/lib/parser/classes/ChannelMobileHeader.d.ts delete mode 100644 typings/lib/parser/classes/ChannelThumbnailWithLink.d.ts delete mode 100644 typings/lib/parser/classes/ChannelVideoPlayer.d.ts delete mode 100644 typings/lib/parser/classes/ChildVideo.d.ts delete mode 100644 typings/lib/parser/classes/ChipCloud.d.ts delete mode 100644 typings/lib/parser/classes/ChipCloudChip.d.ts delete mode 100644 typings/lib/parser/classes/CollageHeroImage.d.ts delete mode 100644 typings/lib/parser/classes/CompactLink.d.ts delete mode 100644 typings/lib/parser/classes/CompactMix.d.ts delete mode 100644 typings/lib/parser/classes/CompactPlaylist.d.ts delete mode 100644 typings/lib/parser/classes/CompactVideo.d.ts delete mode 100644 typings/lib/parser/classes/ContinuationItem.d.ts delete mode 100644 typings/lib/parser/classes/CtaGoToCreatorStudio.d.ts delete mode 100644 typings/lib/parser/classes/DataModelSection.d.ts delete mode 100644 typings/lib/parser/classes/DidYouMean.d.ts delete mode 100644 typings/lib/parser/classes/DownloadButton.d.ts delete mode 100644 typings/lib/parser/classes/Element.d.ts delete mode 100644 typings/lib/parser/classes/EmergencyOnebox.d.ts delete mode 100644 typings/lib/parser/classes/EmojiRun.d.ts delete mode 100644 typings/lib/parser/classes/EndScreenPlaylist.d.ts delete mode 100644 typings/lib/parser/classes/EndScreenVideo.d.ts delete mode 100644 typings/lib/parser/classes/Endscreen.d.ts delete mode 100644 typings/lib/parser/classes/EndscreenElement.d.ts delete mode 100644 typings/lib/parser/classes/ExpandableTab.d.ts delete mode 100644 typings/lib/parser/classes/ExpandedShelfContents.d.ts delete mode 100644 typings/lib/parser/classes/FeedFilterChipBar.d.ts delete mode 100644 typings/lib/parser/classes/FeedTabbedHeader.d.ts delete mode 100644 typings/lib/parser/classes/Format.d.ts delete mode 100644 typings/lib/parser/classes/Grid.d.ts delete mode 100644 typings/lib/parser/classes/GridChannel.d.ts delete mode 100644 typings/lib/parser/classes/GridPlaylist.d.ts delete mode 100644 typings/lib/parser/classes/GridVideo.d.ts delete mode 100644 typings/lib/parser/classes/HistorySuggestion.d.ts delete mode 100644 typings/lib/parser/classes/HorizontalCardList.d.ts delete mode 100644 typings/lib/parser/classes/HorizontalList.d.ts delete mode 100644 typings/lib/parser/classes/ItemSection.d.ts delete mode 100644 typings/lib/parser/classes/ItemSectionHeader.d.ts delete mode 100644 typings/lib/parser/classes/LikeButton.d.ts delete mode 100644 typings/lib/parser/classes/LiveChat.d.ts delete mode 100644 typings/lib/parser/classes/LiveChatAuthorBadge.d.ts delete mode 100644 typings/lib/parser/classes/LiveChatHeader.d.ts delete mode 100644 typings/lib/parser/classes/LiveChatItemList.d.ts delete mode 100644 typings/lib/parser/classes/LiveChatMessageInput.d.ts delete mode 100644 typings/lib/parser/classes/LiveChatParticipant.d.ts delete mode 100644 typings/lib/parser/classes/LiveChatParticipantsList.d.ts delete mode 100644 typings/lib/parser/classes/MerchandiseItem.d.ts delete mode 100644 typings/lib/parser/classes/MerchandiseShelf.d.ts delete mode 100644 typings/lib/parser/classes/Message.d.ts delete mode 100644 typings/lib/parser/classes/MetadataBadge.d.ts delete mode 100644 typings/lib/parser/classes/MetadataRow.d.ts delete mode 100644 typings/lib/parser/classes/MetadataRowContainer.d.ts delete mode 100644 typings/lib/parser/classes/MetadataRowHeader.d.ts delete mode 100644 typings/lib/parser/classes/MicroformatData.d.ts delete mode 100644 typings/lib/parser/classes/Mix.d.ts delete mode 100644 typings/lib/parser/classes/Movie.d.ts delete mode 100644 typings/lib/parser/classes/MovingThumbnail.d.ts delete mode 100644 typings/lib/parser/classes/MusicCarouselShelf.d.ts delete mode 100644 typings/lib/parser/classes/MusicCarouselShelfBasicHeader.d.ts delete mode 100644 typings/lib/parser/classes/MusicDescriptionShelf.d.ts delete mode 100644 typings/lib/parser/classes/MusicDetailHeader.d.ts delete mode 100644 typings/lib/parser/classes/MusicHeader.d.ts delete mode 100644 typings/lib/parser/classes/MusicImmersiveHeader.d.ts delete mode 100644 typings/lib/parser/classes/MusicInlineBadge.d.ts delete mode 100644 typings/lib/parser/classes/MusicItemThumbnailOverlay.d.ts delete mode 100644 typings/lib/parser/classes/MusicNavigationButton.d.ts delete mode 100644 typings/lib/parser/classes/MusicPlayButton.d.ts delete mode 100644 typings/lib/parser/classes/MusicPlaylistShelf.d.ts delete mode 100644 typings/lib/parser/classes/MusicQueue.d.ts delete mode 100644 typings/lib/parser/classes/MusicResponsiveListItem.d.ts delete mode 100644 typings/lib/parser/classes/MusicResponsiveListItemFixedColumn.d.ts delete mode 100644 typings/lib/parser/classes/MusicResponsiveListItemFlexColumn.d.ts delete mode 100644 typings/lib/parser/classes/MusicShelf.d.ts delete mode 100644 typings/lib/parser/classes/MusicThumbnail.d.ts delete mode 100644 typings/lib/parser/classes/MusicTwoRowItem.d.ts delete mode 100644 typings/lib/parser/classes/NavigatableText.d.ts delete mode 100644 typings/lib/parser/classes/NavigationEndpoint.d.ts delete mode 100644 typings/lib/parser/classes/Notification.d.ts delete mode 100644 typings/lib/parser/classes/PlayerAnnotationsExpanded.d.ts delete mode 100644 typings/lib/parser/classes/PlayerCaptionsTracklist.d.ts delete mode 100644 typings/lib/parser/classes/PlayerErrorMessage.d.ts delete mode 100644 typings/lib/parser/classes/PlayerLiveStoryboardSpec.d.ts delete mode 100644 typings/lib/parser/classes/PlayerMicroformat.d.ts delete mode 100644 typings/lib/parser/classes/PlayerOverlay.d.ts delete mode 100644 typings/lib/parser/classes/PlayerOverlayAutoplay.d.ts delete mode 100644 typings/lib/parser/classes/PlayerStoryboardSpec.d.ts delete mode 100644 typings/lib/parser/classes/Playlist.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistAuthor.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistHeader.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistInfoCardContent.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistMetadata.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistPanel.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistPanelVideo.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistSidebar.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistSidebarPrimaryInfo.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistSidebarSecondaryInfo.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistVideo.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistVideoList.d.ts delete mode 100644 typings/lib/parser/classes/PlaylistVideoThumbnail.d.ts delete mode 100644 typings/lib/parser/classes/Poll.d.ts delete mode 100644 typings/lib/parser/classes/Post.d.ts delete mode 100644 typings/lib/parser/classes/ProfileColumn.d.ts delete mode 100644 typings/lib/parser/classes/ProfileColumnStats.d.ts delete mode 100644 typings/lib/parser/classes/ProfileColumnStatsEntry.d.ts delete mode 100644 typings/lib/parser/classes/ProfileColumnUserInfo.d.ts delete mode 100644 typings/lib/parser/classes/ReelItem.d.ts delete mode 100644 typings/lib/parser/classes/ReelShelf.d.ts delete mode 100644 typings/lib/parser/classes/RelatedChipCloud.d.ts delete mode 100644 typings/lib/parser/classes/RichGrid.d.ts delete mode 100644 typings/lib/parser/classes/RichItem.d.ts delete mode 100644 typings/lib/parser/classes/RichListHeader.d.ts delete mode 100644 typings/lib/parser/classes/RichSection.d.ts delete mode 100644 typings/lib/parser/classes/RichShelf.d.ts delete mode 100644 typings/lib/parser/classes/SearchBox.d.ts delete mode 100644 typings/lib/parser/classes/SearchRefinementCard.d.ts delete mode 100644 typings/lib/parser/classes/SearchSuggestion.d.ts delete mode 100644 typings/lib/parser/classes/SearchSuggestionsSection.d.ts delete mode 100644 typings/lib/parser/classes/SecondarySearchContainer.d.ts delete mode 100644 typings/lib/parser/classes/SectionList.d.ts delete mode 100644 typings/lib/parser/classes/Shelf.d.ts delete mode 100644 typings/lib/parser/classes/ShowingResultsFor.d.ts delete mode 100644 typings/lib/parser/classes/SimpleCardTeaser.d.ts delete mode 100644 typings/lib/parser/classes/SingleActionEmergencySupport.d.ts delete mode 100644 typings/lib/parser/classes/SingleColumnBrowseResults.d.ts delete mode 100644 typings/lib/parser/classes/SingleColumnMusicWatchNextResults.d.ts delete mode 100644 typings/lib/parser/classes/SingleHeroImage.d.ts delete mode 100644 typings/lib/parser/classes/SortFilterSubMenu.d.ts delete mode 100644 typings/lib/parser/classes/SubFeedOption.d.ts delete mode 100644 typings/lib/parser/classes/SubFeedSelector.d.ts delete mode 100644 typings/lib/parser/classes/SubscribeButton.d.ts delete mode 100644 typings/lib/parser/classes/SubscriptionNotificationToggleButton.d.ts delete mode 100644 typings/lib/parser/classes/Tab.d.ts delete mode 100644 typings/lib/parser/classes/Tabbed.d.ts delete mode 100644 typings/lib/parser/classes/TabbedSearchResults.d.ts delete mode 100644 typings/lib/parser/classes/Text.d.ts delete mode 100644 typings/lib/parser/classes/TextHeader.d.ts delete mode 100644 typings/lib/parser/classes/TextRun.d.ts delete mode 100644 typings/lib/parser/classes/Thumbnail.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayBottomPanel.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayEndorsement.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayHoverText.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayInlineUnplayable.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayLoadingPreview.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayNowPlaying.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayPinking.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayPlaybackStatus.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayResumePlayback.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlaySidePanel.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayTimeStatus.d.ts delete mode 100644 typings/lib/parser/classes/ThumbnailOverlayToggleButton.d.ts delete mode 100644 typings/lib/parser/classes/ToggleButton.d.ts delete mode 100644 typings/lib/parser/classes/ToggleMenuServiceItem.d.ts delete mode 100644 typings/lib/parser/classes/Tooltip.d.ts delete mode 100644 typings/lib/parser/classes/TwoColumnBrowseResults.d.ts delete mode 100644 typings/lib/parser/classes/TwoColumnSearchResults.d.ts delete mode 100644 typings/lib/parser/classes/TwoColumnWatchNextResults.d.ts delete mode 100644 typings/lib/parser/classes/UniversalWatchCard.d.ts delete mode 100644 typings/lib/parser/classes/VerticalList.d.ts delete mode 100644 typings/lib/parser/classes/VerticalWatchCardList.d.ts delete mode 100644 typings/lib/parser/classes/Video.d.ts delete mode 100644 typings/lib/parser/classes/VideoDetails.d.ts delete mode 100644 typings/lib/parser/classes/VideoInfoCardContent.d.ts delete mode 100644 typings/lib/parser/classes/VideoOwner.d.ts delete mode 100644 typings/lib/parser/classes/VideoPrimaryInfo.d.ts delete mode 100644 typings/lib/parser/classes/VideoSecondaryInfo.d.ts delete mode 100644 typings/lib/parser/classes/WatchCardCompactVideo.d.ts delete mode 100644 typings/lib/parser/classes/WatchCardHeroVideo.d.ts delete mode 100644 typings/lib/parser/classes/WatchCardRichHeader.d.ts delete mode 100644 typings/lib/parser/classes/WatchCardSectionSequence.d.ts delete mode 100644 typings/lib/parser/classes/WatchNextEndScreen.d.ts delete mode 100644 typings/lib/parser/classes/WatchNextTabbedResults.d.ts delete mode 100644 typings/lib/parser/classes/actions/AppendContinuationItemsAction.d.ts delete mode 100644 typings/lib/parser/classes/actions/OpenPopupAction.d.ts delete mode 100644 typings/lib/parser/classes/comments/AuthorCommentBadge.d.ts delete mode 100644 typings/lib/parser/classes/comments/Comment.d.ts delete mode 100644 typings/lib/parser/classes/comments/CommentActionButtons.d.ts delete mode 100644 typings/lib/parser/classes/comments/CommentReplies.d.ts delete mode 100644 typings/lib/parser/classes/comments/CommentReplyDialog.d.ts delete mode 100644 typings/lib/parser/classes/comments/CommentSimplebox.d.ts delete mode 100644 typings/lib/parser/classes/comments/CommentThread.d.ts delete mode 100644 typings/lib/parser/classes/comments/CommentsEntryPointHeader.d.ts delete mode 100644 typings/lib/parser/classes/comments/CommentsHeader.d.ts delete mode 100644 typings/lib/parser/classes/livechat/AddBannerToLiveChatCommand.d.ts delete mode 100644 typings/lib/parser/classes/livechat/AddChatItemAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/AddLiveChatTickerItemAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/LiveChatActionPanel.d.ts delete mode 100644 typings/lib/parser/classes/livechat/MarkChatItemAsDeletedAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/MarkChatItemsByAuthorAsDeletedAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/RemoveBannerForLiveChatCommand.d.ts delete mode 100644 typings/lib/parser/classes/livechat/ReplaceChatItemAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/ReplayChatItemAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/ShowLiveChatActionPanelAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/ShowLiveChatTooltipCommand.d.ts delete mode 100644 typings/lib/parser/classes/livechat/UpdateDateTextAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/UpdateDescriptionAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/UpdateLiveChatPollAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/UpdateTitleAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/UpdateToggleButtonTextAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/UpdateViewershipAction.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatBanner.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatBannerHeader.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatBannerPoll.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatMembershipItem.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatPaidMessage.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatPaidSticker.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatPlaceholderItem.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatTextMessage.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatTickerPaidMessageItem.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatTickerSponsorItem.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/LiveChatViewerEngagementMessage.d.ts delete mode 100644 typings/lib/parser/classes/livechat/items/PollHeader.d.ts delete mode 100644 typings/lib/parser/classes/menus/Menu.d.ts delete mode 100644 typings/lib/parser/classes/menus/MenuNavigationItem.d.ts delete mode 100644 typings/lib/parser/classes/menus/MenuServiceItem.d.ts delete mode 100644 typings/lib/parser/classes/menus/MenuServiceItemDownload.d.ts delete mode 100644 typings/lib/parser/classes/menus/MultiPageMenu.d.ts delete mode 100644 typings/lib/parser/classes/menus/MultiPageMenuNotificationSection.d.ts delete mode 100644 typings/lib/parser/classes/menus/SimpleMenuHeader.d.ts delete mode 100644 typings/lib/parser/index.d.ts delete mode 100644 typings/lib/parser/map.d.ts delete mode 100644 typings/lib/parser/youtube/Analytics.d.ts delete mode 100644 typings/lib/parser/youtube/Channel.d.ts delete mode 100644 typings/lib/parser/youtube/Comments.d.ts delete mode 100644 typings/lib/parser/youtube/History.d.ts delete mode 100644 typings/lib/parser/youtube/Library.d.ts delete mode 100644 typings/lib/parser/youtube/LiveChat.d.ts delete mode 100644 typings/lib/parser/youtube/NotificationsMenu.d.ts delete mode 100644 typings/lib/parser/youtube/Playlist.d.ts delete mode 100644 typings/lib/parser/youtube/Search.d.ts delete mode 100644 typings/lib/parser/youtube/VideoInfo.d.ts delete mode 100644 typings/lib/parser/ytmusic/Album.d.ts delete mode 100644 typings/lib/parser/ytmusic/Artist.d.ts delete mode 100644 typings/lib/parser/ytmusic/Explore.d.ts delete mode 100644 typings/lib/parser/ytmusic/HomeFeed.d.ts delete mode 100644 typings/lib/parser/ytmusic/Library.d.ts delete mode 100644 typings/lib/parser/ytmusic/Search.d.ts delete mode 100644 typings/lib/proto/index.d.ts delete mode 100644 typings/lib/proto/messages.d.ts delete mode 100644 typings/lib/utils/Constants.d.ts delete mode 100644 typings/lib/utils/Request.d.ts delete mode 100644 typings/lib/utils/Utils.d.ts delete mode 100644 typings/lib/utils/wrappers/BrowserCache.d.ts delete mode 100644 typings/lib/utils/wrappers/NodeCache.d.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 3c00c6d9..76a51765 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,17 +1,19 @@ plugins: - [ jsdoc ] + [ '@typescript-eslint', 'eslint-plugin-tsdoc' ] env: commonjs: true es2021: true node: true -extends: [ eslint:recommended, plugin:jsdoc/recommended ] -globals: - BROWSER: readonly -settings: - jsdoc: - mode: 'typescript' +extends: [ eslint:recommended, 'plugin:@typescript-eslint/recommended' ] +parser: '@typescript-eslint/parser' parserOptions: ecmaVersion: latest +overrides: + - + files: + - '**/*.js' + rules: + 'tsdoc/syntax': 'off' rules: max-len: - error @@ -24,12 +26,10 @@ rules: ignoreRegExpLiterals: true quotes: [error, single] - - jsdoc/newline-after-description: 'off' - jsdoc/require-returns-description: 'off' - jsdoc/require-param-description: 'off' - jsdoc/no-undefined-types: 'off' - jsdoc/require-returns: 'off' + + '@typescript-eslint/ban-types': 'off' + 'tsdoc/syntax': 'warn' + '@typescript-eslint/no-explicit-any': 'off' no-template-curly-in-string: error no-unreachable-loop: error @@ -42,7 +42,7 @@ rules: no-implied-eval: error arrow-spacing: error no-invalid-this: error - no-lone-blocks: error + no-lone-blocks: 'off' no-new-func: error no-new-wrappers: error no-new: error diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 0a6a0947..7040873a 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [ 12.x, 14.x, 16.x ] + node-version: [ 16.x, 18.x ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/.gitignore b/.gitignore index dfdfb958..a8975723 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,11 @@ pnpm-lock.yaml # Temporary files for testing tmp/ + +# Build output +dist/ +bundle/*.js.* +bundle/*.js + +# MacOS +.DS_Store diff --git a/README_v2.0.0WIP.md b/README_v2.0.0WIP.md index 1b0917f4..d809fe37 100644 --- a/README_v2.0.0WIP.md +++ b/README_v2.0.0WIP.md @@ -88,17 +88,19 @@ Innertube is an API used across all YouTube clients, it was created to simplify[ And huge thanks to [@gatecrasher777][gatecrasher] for his research on the workings of the Innertube API! +If you have any questions or need help, feel free to contact us on our chat server [here](https://discord.gg/syDu7Yks54). + ## Getting Started ### Prerequisites -- [NodeJS][nodejs] v14 or greater +YouTube.js runs on Node.js, Deno and in modern browsers. -To verify things are set up -properly, run this: -```bash -node --version -``` +It requires a runtime with the following features: +- [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) + - On Node we use [undici]()'s fetch implementation which requires Node.js 16.8+. You may provide your own fetch implementation if you need to use an older version. See [providing your own fetch implementation](#custom-fetch) for more information. + - The `Response` object returned by fetch must thus be spec compliant and return a `ReadableStream` object if you want to use the `VideoInfo#download` method. (Implementations like `node-fetch` returns a non-standard `Readable` object.) +- [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) and [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) required. ### Installation - NPM: @@ -114,15 +116,113 @@ yarn add youtubei.js@latest npm install git+https://github.com/LuanRT/YouTube.js.git ``` +**TODO: Deno install instructions (esm.sh possibly?)** + ## Usage - Create an Innertube instance (or session): -```js -// const Innertube = require('youtubei.js'); -import Innertube from 'youtubei.js'; -const youtube = await new Innertube({ gl: 'US' }); +```ts +// const { Innertube } = require('youtubei.js'); +import { Innertube } from 'youtubei.js'; +const youtube = await Innertube.create(); ``` + +## Browser Usage +To use YouTube.js in the browser you must proxy requests through your own server. You can see our simple reference implementation in Deno in [`examples/browser/proxy/deno.ts`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/proxy/deno.ts). + +You may provide your own fetch implementation to be used by YouTube.js. Which we will use here to modify and send the requests to through our proxy. See [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/web) for an simple example using [Vite](https://vitejs.dev/). + +```ts +// Pre-bundled version for the web +import { Innertube } from 'youtubei.js/bundle/browser'; +await Innertube.create({ + fetch: async (input: RequestInfo | URL, init?: RequestInit) => { + // Modify the request + // and send it to the proxy + + // fetch the url + return fetch(request, init); + } +}); +``` + +### Streaming +YouTube.js supports streaming of videos in the browser by converting YouTube's streaming data into a MPEG-DASH manifest. + +The example below uses [`dash.js`](https://github.com/Dash-Industry-Forum/dash.js) to play the video. + +```ts +import { Innertube } from 'youtubei.js'; +import dashjs from 'dashjs'; + +const youtube = await Innertube.create({ /* setup - see above */ }); + +// get the video info +const videoInfo = await youtube.getInfo('videoId'); + +// now convert to a dash manifest +// again - to be able to stream the video in the browser - we must proxy the requests through our own server +// to do this, we provide a method to transform the urls before writing them to the manifest +const manifest = videoInfo.toDash(url => { + // modify the url + // and return it + return url; +}); + +const uri = "data:application/dash+xml;charset=utf-8;base64," + btoa(manifest); + +const videoElement = document.getElementById('video_player'); + +const player = dashjs.MediaPlayer().create(); +player.initialize(videoElement, uri, true); +``` + +Our browser example in [`examples/browser/web`]() provides a full working example. + + + + +## Providing your own fetch implementation +You may provide your own fetch implementation to be used by YouTube.js. This may be useful in some cases to modify the requests before they are sent and transform the responses before they are returned (eg. for proxies). + +```ts +// provide a fetch implementation +const yt = await Innertube.create({ + fetch: async (input: RequestInfo | URL, init?: RequestInit) => { + // make the request with your own fetch implementation + // and return the response + return new Response( + /* ... */ + ); + } +}); +``` + + + +## Caching +To improve performance, you may wish to cache the transformed player instance which we use to decode the streaming urls. + +Our cache uses the `node:fs` module in Node-like environments, `Deno.writeFile` in Deno and `indexedDB` in browsers. + +```ts +import { Innertube, UniversalCache } from 'youtubei.js'; +// By default, cache stores files in the OS temp directory (or indexedDB in browsers). +const yt = await Innertube.create({ + cache: new UniversalCache() +}); + +// You may wish to make the cache persistent (on Node and Deno) +const yt = await Innertube.create({ + cache: new UniversalCache( + // Enables persistent caching + true, + // Path to the cache directory, will create the directory if it doesn't exist + './.cache' + ) +}); +``` + ## API ## Innertube : `object` diff --git a/browser.ts b/browser.ts new file mode 100644 index 00000000..31d626d7 --- /dev/null +++ b/browser.ts @@ -0,0 +1,12 @@ +// Deno and browser runtimes + +// Polyfill buffer +import { Buffer } from 'buffer'; +if (!Reflect.has(globalThis, 'Buffer')) { + Reflect.set(globalThis, 'Buffer', Buffer); +} + +import Innertube from './lib/Innertube'; +export { default as Innertube } from './lib/Innertube.js'; +export * from './lib/utils'; +export default Innertube; diff --git a/build/browser.d.ts b/build/browser.d.ts deleted file mode 100644 index ae3e583d..00000000 --- a/build/browser.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import Innertube from ".."; -export default Innertube; \ No newline at end of file diff --git a/build/browser.js b/build/browser.js deleted file mode 100644 index 898b0827..00000000 --- a/build/browser.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict";/* eslint-disable */ -var or=Object.defineProperty;var JC=Object.getOwnPropertyDescriptor;var ZC=Object.getOwnPropertyNames;var eL=Object.prototype.hasOwnProperty;var tL=(t,e,i)=>e in t?or(t,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[e]=i;var p=(t,e)=>or(t,"name",{value:e,configurable:!0});var Ut=(t,e)=>()=>(t&&(e=t(t=0)),e);var u=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),iL=(t,e)=>{for(var i in e)or(t,i,{get:e[i],enumerable:!0})},nL=(t,e,i,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of ZC(e))!eL.call(t,o)&&o!==i&&or(t,o,{get:()=>e[o],enumerable:!(n=JC(e,o))||n.enumerable});return t};var fv=t=>nL(or({},"__esModule",{value:!0}),t);var Qt=(t,e,i)=>(tL(t,typeof e!="symbol"?e+"":e,i),i),nd=(t,e,i)=>{if(!e.has(t))throw TypeError("Cannot "+i)};var M=(t,e,i)=>(nd(t,e,"read from private field"),i?i.call(t):e.get(t)),z=(t,e,i)=>{if(e.has(t))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(t):e.set(t,i)},X=(t,e,i,n)=>(nd(t,e,"write to private field"),n?n.call(t,i):e.set(t,i),i);var oe=(t,e,i)=>(nd(t,e,"access private method"),i);function rr(){if(!Aa&&(Aa=typeof crypto<"u"&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||typeof msCrypto<"u"&&typeof msCrypto.getRandomValues=="function"&&msCrypto.getRandomValues.bind(msCrypto),!Aa))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return Aa(oL)}var Aa,oL,od=Ut(()=>{oL=new Uint8Array(16);p(rr,"rng")});var wv,uv=Ut(()=>{wv=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i});function rL(t){return typeof t=="string"&&wv.test(t)}var W0,ar=Ut(()=>{uv();p(rL,"validate");W0=rL});function aL(t){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,i=(At[t[e+0]]+At[t[e+1]]+At[t[e+2]]+At[t[e+3]]+"-"+At[t[e+4]]+At[t[e+5]]+"-"+At[t[e+6]]+At[t[e+7]]+"-"+At[t[e+8]]+At[t[e+9]]+"-"+At[t[e+10]]+At[t[e+11]]+At[t[e+12]]+At[t[e+13]]+At[t[e+14]]+At[t[e+15]]).toLowerCase();if(!W0(i))throw TypeError("Stringified UUID is invalid");return i}var At,Ia,k0,pr=Ut(()=>{ar();At=[];for(Ia=0;Ia<256;++Ia)At.push((Ia+256).toString(16).substr(1));p(aL,"stringify");k0=aL});function pL(t,e,i){var n=e&&i||0,o=e||new Array(16);t=t||{};var r=t.node||Wv,a=t.clockseq!==void 0?t.clockseq:rd;if(r==null||a==null){var c=t.random||(t.rng||rr)();r==null&&(r=Wv=[c[0]|1,c[1],c[2],c[3],c[4],c[5]]),a==null&&(a=rd=(c[6]<<8|c[7])&16383)}var l=t.msecs!==void 0?t.msecs:Date.now(),s=t.nsecs!==void 0?t.nsecs:pd+1,g=l-ad+(s-pd)/1e4;if(g<0&&t.clockseq===void 0&&(a=a+1&16383),(g<0||l>ad)&&t.nsecs===void 0&&(s=0),s>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");ad=l,pd=s,rd=a,l+=122192928e5;var h=((l&268435455)*1e4+s)%4294967296;o[n++]=h>>>24&255,o[n++]=h>>>16&255,o[n++]=h>>>8&255,o[n++]=h&255;var d=l/4294967296*1e4&268435455;o[n++]=d>>>8&255,o[n++]=d&255,o[n++]=d>>>24&15|16,o[n++]=d>>>16&255,o[n++]=a>>>8|128,o[n++]=a&255;for(var f=0;f<6;++f)o[n+f]=r[f];return e||k0(o)}var Wv,rd,ad,pd,kv,mv=Ut(()=>{od();pr();ad=0,pd=0;p(pL,"v1");kv=pL});function cL(t){if(!W0(t))throw TypeError("Invalid UUID");var e,i=new Uint8Array(16);return i[0]=(e=parseInt(t.slice(0,8),16))>>>24,i[1]=e>>>16&255,i[2]=e>>>8&255,i[3]=e&255,i[4]=(e=parseInt(t.slice(9,13),16))>>>8,i[5]=e&255,i[6]=(e=parseInt(t.slice(14,18),16))>>>8,i[7]=e&255,i[8]=(e=parseInt(t.slice(19,23),16))>>>8,i[9]=e&255,i[10]=(e=parseInt(t.slice(24,36),16))/1099511627776&255,i[11]=e/4294967296&255,i[12]=e>>>24&255,i[13]=e>>>16&255,i[14]=e>>>8&255,i[15]=e&255,i}var Ka,cd=Ut(()=>{ar();p(cL,"parse");Ka=cL});function lL(t){t=unescape(encodeURIComponent(t));for(var e=[],i=0;i{pr();cd();p(lL,"stringToBytes");sL="6ba7b810-9dad-11d1-80b4-00c04fd430c8",gL="6ba7b811-9dad-11d1-80b4-00c04fd430c8";p(cr,"default")});function hL(t){if(typeof t=="string"){var e=unescape(encodeURIComponent(t));t=new Uint8Array(e.length);for(var i=0;i>5]>>>o%32&255,a=parseInt(n.charAt(r>>>4&15)+n.charAt(r&15),16);e.push(a)}return e}function Mv(t){return(t+64>>>9<<4)+14+1}function vL(t,e){t[e>>5]|=128<>5]|=(t[n/8]&255)<>16)+(e>>16)+(i>>16);return n<<16|i&65535}function wL(t,e){return t<>>32-e}function Sa(t,e,i,n,o,r){return m0(wL(m0(m0(e,t),m0(n,r)),o),i)}function _t(t,e,i,n,o,r,a){return Sa(e&i|~e&n,t,e,o,r,a)}function zt(t,e,i,n,o,r,a){return Sa(e&n|i&~n,t,e,o,r,a)}function Et(t,e,i,n,o,r,a){return Sa(e^i^n,t,e,o,r,a)}function Ot(t,e,i,n,o,r,a){return Sa(i^(e|~n),t,e,o,r,a)}var yv,Hv=Ut(()=>{p(hL,"md5");p(dL,"md5ToHexEncodedArray");p(Mv,"getOutputLength");p(vL,"wordsToMd5");p(fL,"bytesToWords");p(m0,"safeAdd");p(wL,"bitRotateLeft");p(Sa,"md5cmn");p(_t,"md5ff");p(zt,"md5gg");p(Et,"md5hh");p(Ot,"md5ii");yv=hL});var uL,Tv,Nv=Ut(()=>{ld();Hv();uL=cr("v3",48,yv),Tv=uL});function WL(t,e,i){t=t||{};var n=t.random||(t.rng||rr)();if(n[6]=n[6]&15|64,n[8]=n[8]&63|128,e){i=i||0;for(var o=0;o<16;++o)e[i+o]=n[o];return e}return k0(n)}var Cv,Lv=Ut(()=>{od();pr();p(WL,"v4");Cv=WL});function kL(t,e,i,n){switch(t){case 0:return e&i^~e&n;case 1:return e^i^n;case 2:return e&i^e&n^i&n;case 3:return e^i^n}}function sd(t,e){return t<>>32-e}function mL(t){var e=[1518500249,1859775393,2400959708,3395469782],i=[1732584193,4023233417,2562383102,271733878,3285377520];if(typeof t=="string"){var n=unescape(encodeURIComponent(t));t=[];for(var o=0;o>>0;N=C,C=T,T=sd(y,30)>>>0,y=w,w=O}i[0]=i[0]+w>>>0,i[1]=i[1]+y>>>0,i[2]=i[2]+T>>>0,i[3]=i[3]+C>>>0,i[4]=i[4]+N>>>0}return[i[0]>>24&255,i[0]>>16&255,i[0]>>8&255,i[0]&255,i[1]>>24&255,i[1]>>16&255,i[1]>>8&255,i[1]&255,i[2]>>24&255,i[2]>>16&255,i[2]>>8&255,i[2]&255,i[3]>>24&255,i[3]>>16&255,i[3]>>8&255,i[3]&255,i[4]>>24&255,i[4]>>16&255,i[4]>>8&255,i[4]&255]}var Av,Iv=Ut(()=>{p(kL,"f");p(sd,"ROTL");p(mL,"sha1");Av=mL});var ML,Kv,Sv=Ut(()=>{ld();Iv();ML=cr("v5",80,Av),Kv=ML});var Gv,bv=Ut(()=>{Gv="00000000-0000-0000-0000-000000000000"});function yL(t){if(!W0(t))throw TypeError("Invalid UUID");return parseInt(t.substr(14,1),16)}var xv,_v=Ut(()=>{ar();p(yL,"version");xv=yL});var gd={};iL(gd,{NIL:()=>Gv,parse:()=>Ka,stringify:()=>k0,v1:()=>kv,v3:()=>Tv,v4:()=>Cv,v5:()=>Kv,validate:()=>W0,version:()=>xv});var hd=Ut(()=>{mv();Nv();Lv();Sv();bv();_v();ar();pr();cd()});var gi=u((tU,zv)=>{"use strict";zv.exports={URLS:{YT_BASE:"https://www.youtube.com",YT_MUSIC_BASE:"https://music.youtube.com",YT_SUGGESTIONS:"https://suggestqueries.google.com/complete/",API:{BASE:"https://youtubei.googleapis.com",PRODUCTION:"https://youtubei.googleapis.com/youtubei/",STAGING:"https://green-youtubei.sandbox.googleapis.com/youtubei/",RELEASE:"https://release-youtubei.sandbox.googleapis.com/youtubei/",TEST:"https://test-youtubei.sandbox.googleapis.com/youtubei/",CAMI:"http://cami-youtubei.sandbox.googleapis.com/youtubei/",UYTFE:"https://uytfe.sandbox.google.com/youtubei/"}},OAUTH:{SCOPE:"http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content",GRANT_TYPE:"http://oauth.net/grant_type/device/1.0",MODEL_NAME:"ytlr::",HEADERS:{accept:"*/*",origin:"https://www.youtube.com","user-agent":"Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version","content-type":"application/json",referer:"https://www.youtube.com/tv","accept-language":"en-US"},REGEX:{AUTH_SCRIPT:/ + + diff --git a/examples/browser/web/package.json b/examples/browser/web/package.json new file mode 100644 index 00000000..284684c0 --- /dev/null +++ b/examples/browser/web/package.json @@ -0,0 +1,18 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^4.6.4", + "vite": "^3.0.0" + }, + "dependencies": { + "dashjs": "^4.4.0" + } +} \ No newline at end of file diff --git a/examples/browser/web/public/service-worker.js b/examples/browser/web/public/service-worker.js new file mode 100644 index 00000000..c594f739 --- /dev/null +++ b/examples/browser/web/public/service-worker.js @@ -0,0 +1,3 @@ +/* eslint-disable */ +var u=new Set(["www.youtube.com","music.youtube.com","suggestqueries.google.com","youtubei.googleapis.com","youtubei.googleapis.com","green-youtubei.sandbox.googleapis.com","release-youtubei.sandbox.googleapis.com","test-youtubei.sandbox.googleapis.com","cami-youtubei.sandbox.googleapis.com","uytfe.sandbox.google.com"]);self.addEventListener("fetch",o=>{try{let s=new URL(o.request.url).hostname;if(!u.has(s))return}catch(s){return}let e=new URL(o.request.url);e.searchParams.set("__host",e.host),e.host=e.searchParams.get("__proxy");let t=new Request(e,o.request);o.respondWith(fetch(t))}); +//# sourceMappingURL=service-worker.js.map diff --git a/examples/browser/web/public/service-worker.js.map b/examples/browser/web/public/service-worker.js.map new file mode 100644 index 00000000..576d7538 --- /dev/null +++ b/examples/browser/web/public/service-worker.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../browser/client/service-worker.ts"], + "sourcesContent": ["// We need to proxy requests to youtube to our own server to avoid CORS issues\n\n/// \n\n// export empty type because of tsc --isolatedModules flag\nexport type {};\ndeclare const self: ServiceWorkerGlobalScope;\n\nconst hosts = new Set([\n \"www.youtube.com\",\n \"music.youtube.com\",\n \"suggestqueries.google.com\",\n \"youtubei.googleapis.com\",\n \"youtubei.googleapis.com\",\n \"green-youtubei.sandbox.googleapis.com\",\n \"release-youtubei.sandbox.googleapis.com\",\n \"test-youtubei.sandbox.googleapis.com\",\n \"cami-youtubei.sandbox.googleapis.com\",\n \"uytfe.sandbox.google.com\"\n]);\n\nself.addEventListener('fetch', event => {\n try {\n const host = new URL(event.request.url).hostname;\n if (!hosts.has(host))\n return;\n } catch {\n return;\n }\n const url = new URL(event.request.url);\n url.searchParams.set('__host', url.host);\n url.host = url.searchParams.get('__proxy')!;\n\n // we should proxy this to our own server\n const request = new Request(url, event.request);\n\n event.respondWith(fetch(request));\n});\n"], + "mappings": ";AAQA,GAAM,GAAQ,GAAI,KAAI,CAClB,kBACA,oBACA,4BACA,0BACA,0BACA,wCACA,0CACA,uCACA,uCACA,0BACJ,CAAC,EAED,KAAK,iBAAiB,QAAS,GAAS,CACpC,GAAI,CACA,GAAM,GAAO,GAAI,KAAI,EAAM,QAAQ,GAAG,EAAE,SACxC,GAAI,CAAC,EAAM,IAAI,CAAI,EACf,MACR,OAAQ,EAAN,CACE,MACJ,CACA,GAAM,GAAO,GAAI,KAAI,EAAM,QAAQ,GAAG,EACtC,EAAI,aAAa,IAAI,SAAU,EAAI,IAAI,EACvC,EAAI,KAAO,EAAI,aAAa,IAAI,SAAS,EAGzC,GAAM,GAAU,GAAI,SAAQ,EAAK,EAAM,OAAO,EAE9C,EAAM,YAAY,MAAM,CAAO,CAAC,CACpC,CAAC", + "names": [] +} diff --git a/examples/browser/web/public/vite.svg b/examples/browser/web/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/browser/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/browser/web/src/main.ts b/examples/browser/web/src/main.ts new file mode 100644 index 00000000..f3de15d1 --- /dev/null +++ b/examples/browser/web/src/main.ts @@ -0,0 +1,99 @@ +import './style.css'; +import { Innertube, UniversalCache } from '../../../../bundle/browser'; +import dashjs from 'dashjs'; + +async function main() { + const yt = await Innertube.create({ + fetch: async (input: RequestInfo | URL, init?: RequestInit) => { + // url + const url = typeof input === 'string' + ? new URL(input) + : input instanceof URL + ? input + : new URL(input.url); + + // transform the url for use with our proxy + url.searchParams.set('__host', url.host); + url.host = 'localhost:8080'; + url.protocol = 'http'; + + const headers = init?.headers + ? new Headers(init.headers) + : input instanceof Request + ? input.headers + : new Headers(); + + // now serialize the headers + url.searchParams.set('__headers', JSON.stringify([...headers])); + + // copy over the request + const request = new Request( + url, + input instanceof Request ? input : undefined, + ); + + headers.delete('user-agent'); + + // fetch the url + return fetch(request, init ? { + ...init, + headers + } : { + headers + }); + }, + cache: new UniversalCache(), + }); + + const span = document.getElementById('video_name') as HTMLSpanElement; + const form = document.querySelector('form') as HTMLFormElement; + + span.textContent = 'Library ready'; + + let player: dashjs.MediaPlayerClass | undefined; + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + + span.textContent = 'Loading...'; + + const video_id = document.querySelector( + 'input[type=text]', + )?.value; + if (!video_id) { + span.textContent = 'No video id'; + return; + } + try { + const video = await yt.getInfo(video_id); + + console.log(video); + span.textContent = video.basic_info.title || null; + + const dash = video.toDash((url) => { + url.searchParams.set('__host', url.host); + url.host = 'localhost:8080'; + url.protocol = 'http'; + return url; + }); + + const uri = 'data:application/dash+xml;charset=utf-8;base64,' + + btoa(dash); + + // create and append video element + const video_element = document.querySelector('video') as HTMLVideoElement; + video_element.setAttribute('controls', 'true'); + // use dash.js to parse the manifest + if (player) { + player.destroy(); + } + player = dashjs.MediaPlayer().create(); + player.initialize(video_element, uri, true); + } catch (error) { + span.textContent = 'An error occurred (see console)'; + console.error(error); + } + }); +} + +main(); diff --git a/examples/browser/web/src/style.css b/examples/browser/web/src/style.css new file mode 100644 index 00000000..49c2fa06 --- /dev/null +++ b/examples/browser/web/src/style.css @@ -0,0 +1,12 @@ +body { + display: flex; + flex-direction: column; + align-items: center; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +video { + max-width: calc(100vw - 1rem); + width: fit-content; + max-height: calc(90vh - 12rem); +} \ No newline at end of file diff --git a/examples/browser/web/src/vite-env.d.ts b/examples/browser/web/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/browser/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/browser/web/tsconfig.json b/examples/browser/web/tsconfig.json new file mode 100644 index 00000000..3cacf7ee --- /dev/null +++ b/examples/browser/web/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/examples/deno/README.md b/examples/deno/README.md new file mode 100644 index 00000000..902c42ed --- /dev/null +++ b/examples/deno/README.md @@ -0,0 +1,7 @@ +# Deno example + +Run this example with: + +``` +deno run --allow-net --allow-write index.ts +``` diff --git a/examples/deno/index.ts b/examples/deno/index.ts new file mode 100644 index 00000000..38605ed0 --- /dev/null +++ b/examples/deno/index.ts @@ -0,0 +1,16 @@ +import { Innertube } from '../../bundle/browser.js'; + +const yt = await Innertube.create(); + +const video = await yt.getInfo('dQw4w9WgXcQ'); + +console.log('Video title is', video.basic_info.title); + +const file = await Deno.open('test.mp4', { + write: true, + create: true, +}); + +const stream = await video.download(); + +stream.pipeTo(file.writable); diff --git a/index.js b/index.js deleted file mode 100644 index 6e5d01b9..00000000 --- a/index.js +++ /dev/null @@ -1,2 +0,0 @@ -'use strict'; -module.exports = require('./lib/Innertube.js'); \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 00000000..b5ecbce9 --- /dev/null +++ b/index.ts @@ -0,0 +1,18 @@ +import { getRuntime } from './lib/utils/Utils'; + +// Polyfill fetch for node +if (getRuntime() === 'node') { + // eslint-disable-next-line + const undici = require('undici'); + Reflect.set(globalThis, 'fetch', undici.fetch); + Reflect.set(globalThis, 'Headers', undici.Headers); + Reflect.set(globalThis, 'Request', undici.Request); + Reflect.set(globalThis, 'Response', undici.Response); + Reflect.set(globalThis, 'FormData', undici.FormData); + Reflect.set(globalThis, 'File', undici.File); +} + +import Innertube from './lib/Innertube'; +export { default as Innertube } from './lib/Innertube.js'; +export * from './lib/utils'; +export default Innertube; diff --git a/jest.config.js b/jest.config.js index a459d6f0..6903e9c4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,24 +1,13 @@ -'use strict'; + module.exports = { projects: [ { displayName: 'node', - roots: [ '/test/node' ], + roots: [ '/test' ], testMatch: [ '**/*.test.js' ], testTimeout: 10000, setupFiles: [] - }, - { - displayName: 'browser', - roots: [ '/test/browser' ], - testMatch: [ '**/*.test.js' ], - testTimeout: 10000, - setupFiles: [ - 'fake-indexeddb/auto', - './scripts/globals.js', - 'fake-dom' - ] } ] }; \ No newline at end of file diff --git a/lib/Innertube.js b/lib/Innertube.js deleted file mode 100644 index f652a4d0..00000000 --- a/lib/Innertube.js +++ /dev/null @@ -1,376 +0,0 @@ -'use strict'; - -const OAuth = require('./core/OAuth'); -const Actions = require('./core/Actions'); -const SessionBuilder = require('./core/SessionBuilder'); -const AccountManager = require('./core/AccountManager'); -const PlaylistManager = require('./core/PlaylistManager'); -const InteractionManager = require('./core/InteractionManager'); - -const Search = require('./parser/youtube/Search'); -const VideoInfo = require('./parser/youtube/VideoInfo'); -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 NotificationsMenu = require('./parser/youtube/NotificationsMenu'); - -const YTMusic = require('./core/Music'); -const FilterableFeed = require('./core/FilterableFeed'); -const TabbedFeed = require('./core/TabbedFeed'); -const Feed = require('./core/Feed'); - -const EventEmitter = require('events'); -const { PassThrough } = BROWSER ? require('stream-browserify') : require('stream'); - -const Request = require('./utils/Request'); -const Constants = require('./utils/Constants'); - -const { - InnertubeError, - throwIfMissing, - generateRandomString -} = require('./utils/Utils'); - -const Proto = require('./proto'); - -/** @namespace */ -class Innertube { - #player; - #request; - - /** - * @example - * ```js - * const Innertube = require('youtubei.js'); - * const youtube = await new Innertube(); - * ``` - * @param {object} [config] - * @param {string} [config.gl] - * @param {string} [config.cookie] - * @param {boolean} [config.debug] - * @param {object} [config.proxy] - * @param {object} [config.http_agent] - * @param {object} [config.https_agent] - */ - constructor(config) { - this.config = config || {}; - return this.#init(); - } - - async #init() { - const request = new Request(this.config); - const session = await new SessionBuilder(this.config, request.instance).build(); - - /** @type {string} */ - this.key = session.key; - - /** @type {string} */ - this.version = session.api_version; - - /** @type {object} */ - this.context = session.context; - - /** @type {boolean} */ - this.logged_in = !!this.config.cookie; - - /** @type {number} */ - this.sts = session.player.sts; - - /** @type {string} */ - this.player_url = session.player.url; - - /** @type {import('./core/Player')} */ - this.#player = session.player; - - request.setSession(this); - - this.#request = request.instance; - - /** - * @fires Innertube#auth - fired when signing in to an account. - * @fires Innertube#update-credentials - fired when the access token is no longer valid. - * @type {EventEmitter} - */ - this.ev = new EventEmitter(); - this.oauth = new OAuth(this.ev, request.instance); - - 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; - } - - /** - * Signs in to a google account. - * @param {object} credentials - * @param {string} credentials.access_token - Token used to sign in. - * @param {string} credentials.refresh_token - Token used to get a new access token. - * @param {Date} credentials.expires - Access token's expiration date, which is usually 24hrs-ish. - * @returns {Promise.} - */ - signIn(credentials = {}) { - return new Promise(async (resolve) => { - this.oauth.init(credentials); - - if (this.oauth.validateCredentials()) { - await this.oauth.checkAccessTokenValidity(); - this.logged_in = true; - resolve(); - } - - this.ev.on('auth', (data) => { - this.logged_in = true; - if (data.status === 'SUCCESS') resolve(); - }); - }); - } - - /** - * Signs out of an account. - * @returns {Promise.<{ success: boolean, status_code: number }>} - */ - async signOut() { - if (!this.logged_in) throw new InnertubeError('You are not signed in'); - - const response = await this.oauth.revokeAccessToken(); - - this.logged_in = false; - - return response; - } - - /** - * Retrieves video info. - * @param {string} video_id - * @returns {Promise.} - */ - async getInfo(video_id) { - throwIfMissing({ video_id }); - const cpn = 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. - * @param {string} video_id - * @returns {Promise.} - */ - async getBasicInfo(video_id) { - throwIfMissing({ video_id }); - const cpn = generateRandomString(16); - - const response = await this.actions.getVideoInfo(video_id, cpn); - return new VideoInfo([ response, {} ], this.actions, this.#player, cpn); - } - - /** - * 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.sort_by] - filter video results by order, can be: relevance | rating | upload_date | view_count - * @returns {Promise.} - */ - async search(query, filters = {}) { - 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. - */ - async getSearchSuggestions(query) { - throwIfMissing({ query }); - - const response = await this.#request({ - url: 'search', - baseURL: Constants.URLS.YT_SUGGESTIONS, - params: { - q: query, - ds: 'yt', - client: 'youtube', - xssi: 't', - oe: 'UTF', - gl: this.context.client.gl, - hl: this.context.client.hl - } - }); - - const data = JSON.parse(response.data.replace(')]}\'', '')); - const suggestions = data[1].map((suggestion) => suggestion[0]); - - return suggestions; - } - - /** - * Retrieves comments for a video. - * @param {string} video_id - the video id. - * @param {string} [sort_by] - can be: `TOP_COMMENTS` or `NEWEST_FIRST`. - * @returns {Promise.} - */ - async getComments(video_id, sort_by) { - throwIfMissing({ video_id }); - - const payload = Proto.encodeCommentsSectionParams(video_id, { - sort_by: sort_by || 'TOP_COMMENTS' - }); - - const response = await this.actions.next({ ctoken: payload }); - - return new Comments(this.actions, response.data); - } - - /** - * Retrieves YouTube's home feed (aka recommendations). - * @returns {Promise} - */ - async getHomeFeed() { - const response = await this.actions.browse('FEwhat_to_watch'); - return new FilterableFeed(this.actions, response.data); - } - - /** - * Returns the account's library. - * @returns {Promise.} - */ - async getLibrary() { - const response = await this.actions.browse('FElibrary'); - return new Library(response.data, this.actions); - } - - /** - * Retrieves watch history. - * Which can also be achieved with {@link getLibrary()}. - * @returns {Promise.} - */ - async getHistory() { - const response = await this.actions.browse('FEhistory'); - return new History(this.actions, response.data); - } - - /** - * Retrieves trending content. - * @returns {Promise} - */ - async getTrending() { - const response = await this.actions.browse('FEtrending'); - return new TabbedFeed(this.actions, response.data); - } - - /** - * Retrieves subscriptions feed. - * @returns {Promise.} - */ - async getSubscriptionsFeed() { - const response = await this.actions.browse('FEsubscriptions'); - return new Feed(this.actions, response.data); - } - - /** - * Retrieves contents for a given channel. - * @param {string} id - channel id - * @returns {Promise} - */ - async getChannel(id) { - throwIfMissing({ id }); - const response = await this.actions.browse(id); - return new Channel(this.actions, response.data); - } - - /** - * Retrieves notifications. - * @returns {Promise.} - */ - async getNotifications() { - const response = await this.actions.notifications('get_notification_menu'); - return new NotificationsMenu(this.actions, response.data); - } - - /** - * Retrieves unseen notifications count. - * @returns {Promise.} - */ - async getUnseenNotificationsCount() { - const response = await this.actions.notifications('get_unseen_count'); - return response.data.unseenCount; - } - - /** - * Retrieves the contents of a given playlist. - * @param {string} playlist_id - the id of the playlist. - * @returns {Promise.} - */ - async getPlaylist(playlist_id) { - throwIfMissing({ playlist_id }); - const response = await this.actions.browse(`VL${playlist_id.replace(/VL/g, '')}`); - return new Playlist(this.actions, response.data); - } - - /** - * 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... - * @param {string} options.type - download type, can be: video, audio or videoandaudio - * @param {string} options.format - file format - * @returns {Promise.} - */ - async getStreamingData(video_id, options = {}) { - const info = await this.getBasicInfo(video_id); - return info.chooseFormat(options); - } - - /** - * 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... - * @param {string} [options.type] - download type, can be: video, audio or videoandaudio - * @param {string} [options.format] - file format - * @param {object} [options.range] - download range, indicates which bytes should be downloaded. - * @param {number} options.range.start - the beginning of the range. - * @param {number} options.range.end - the end of the range. - * @returns {PassThrough} - */ - download(video_id, options = {}) { - throwIfMissing({ video_id }); - const stream = new PassThrough(); - - (async () => { - const info = await this.getBasicInfo(video_id); - stream.emit('info', info); - info.download(options, stream); - })(); - - return stream; - } - - getPlayer() { - return this.#player; - } - - /** @readonly */ - get request() { - return this.#request; - } -} - -module.exports = Innertube; \ No newline at end of file diff --git a/lib/Innertube.ts b/lib/Innertube.ts new file mode 100644 index 00000000..6bd7dffe --- /dev/null +++ b/lib/Innertube.ts @@ -0,0 +1,215 @@ +import Session, { SessionOptions } from './core/Session'; +import AccountManager from './core/AccountManager'; +import PlaylistManager from './core/PlaylistManager'; +import InteractionManager from './core/InteractionManager'; +import Search from './parser/youtube/Search'; +import VideoInfo, { DownloadOptions, FormatOptions } from './parser/youtube/VideoInfo'; +import Channel from './parser/youtube/Channel'; +import Playlist from './parser/youtube/Playlist'; +import Library from './parser/youtube/Library'; +import History from './parser/youtube/History'; +import Comments from './parser/youtube/Comments'; +import NotificationsMenu from './parser/youtube/NotificationsMenu'; +import YTMusic from './core/Music'; +import FilterableFeed from './core/FilterableFeed'; +import TabbedFeed from './core/TabbedFeed'; +import Feed from './core/Feed'; +import Constants from './utils/Constants'; +import { throwIfMissing, generateRandomString } from './utils/Utils'; +import Proto from './proto/index'; + +export type InnertubeConfig = SessionOptions + +export interface SearchFilters { + /** + * Filter videos by upload date, can be: any | last_hour | today | this_week | this_month | this_year + */ + upload_date?: 'any' | 'last_hour' | 'today' | 'this_week' | 'this_month' | 'this_year'; + /** + * Filter results by type, can be: any | video | channel | playlist | movie + */ + type?: 'any' | 'video' | 'channel' | 'playlist' | 'movie'; + /** + * Filter videos by duration, can be: any | short | medium | long + */ + duration?: 'any' | 'short' | 'medium' | 'long'; + /** + * Filter video results by order, can be: relevance | rating | upload_date | view_count + */ + sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count'; + } + +class Innertube { + session; + account; + playlist; + interact; + music; + actions; + constructor(session: Session) { + this.session = session; + this.account = new AccountManager(this.session.actions); + this.playlist = new PlaylistManager(this.session.actions); + this.interact = new InteractionManager(this.session.actions); + this.music = new YTMusic(this.session); + this.actions = this.session.actions; + } + static async create(config: InnertubeConfig = {}) { + return new Innertube(await Session.create(config)); + } + /** + * Retrieves video info. + */ + async getInfo(video_id: string) { + throwIfMissing({ video_id }); + const cpn = 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.session.player, cpn); + } + /** + * Retrieves basic video info. + */ + async getBasicInfo(video_id: string) { + throwIfMissing({ video_id }); + const cpn = generateRandomString(16); + const response = await this.actions.getVideoInfo(video_id, cpn); + return new VideoInfo([ response ], this.actions, this.session.player, cpn); + } + /** + * Searches a given query. + * @param query - search query. + * @param filters - search filters. + */ + async search(query: string, filters: SearchFilters = {}) { + 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 query - the search query. + */ + async getSearchSuggestions(query: string): Promise { + throwIfMissing({ query }); + const url = new URL(`${Constants.URLS.YT_SUGGESTIONS}search`); + url.searchParams.set('q', query); + url.searchParams.set('hl', this.session.context.client.hl); + url.searchParams.set('gl', this.session.context.client.gl); + url.searchParams.set('ds', 'yt'); + url.searchParams.set('client', 'youtube'); + url.searchParams.set('xssi', 't'); + url.searchParams.set('oe', 'UTF'); + + const response = await this.session.http.fetch(url); + + const response_data = await response.text(); + + const data = JSON.parse(response_data.replace(')]}\'', '')); + const suggestions = data[1].map((suggestion: any) => suggestion[0]); + return suggestions; + } + /** + * Retrieves comments for a video. + * @param video_id - the video id. + * @param sort_by - can be: `TOP_COMMENTS` or `NEWEST_FIRST`. + */ + async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST') { + throwIfMissing({ video_id }); + const payload = Proto.encodeCommentsSectionParams(video_id, { + sort_by: sort_by || 'TOP_COMMENTS' + }); + const response = await this.actions.next({ ctoken: payload }); + return new Comments(this.actions, response.data); + } + /** + * Retrieves YouTube's home feed (aka recommendations). + */ + async getHomeFeed() { + const response = await this.actions.browse('FEwhat_to_watch'); + return new FilterableFeed(this.actions, response.data); + } + /** + * Returns the account's library. + */ + async getLibrary() { + const response = await this.actions.browse('FElibrary'); + return new Library(response.data, this.actions); + } + /** + * Retrieves watch history. + * Which can also be achieved with {@link getLibrary}. + */ + async getHistory() { + const response = await this.actions.browse('FEhistory'); + return new History(this.actions, response.data); + } + /** + * Retrieves trending content. + */ + async getTrending() { + const response = await this.actions.browse('FEtrending'); + return new TabbedFeed(this.actions, response.data); + } + /** + * Retrieves subscriptions feed. + */ + async getSubscriptionsFeed() { + const response = await this.actions.browse('FEsubscriptions'); + return new Feed(this.actions, response.data); + } + /** + * Retrieves contents for a given channel. + * @param id - channel id + */ + async getChannel(id: string) { + throwIfMissing({ id }); + const response = await this.actions.browse(id); + return new Channel(this.actions, response.data); + } + /** + * Retrieves notifications. + */ + async getNotifications() { + const response = await this.actions.notifications('get_notification_menu'); + return new NotificationsMenu(this.actions, response.data); + } + /** + * Retrieves unseen notifications count. + */ + async getUnseenNotificationsCount() { + const response = await this.actions.notifications('get_unseen_count'); + return response.data.unseenCount; + } + /** + * Retrieves the contents of a given playlist. + * @param playlist_id - the id of the playlist. + */ + async getPlaylist(playlist_id: string) { + throwIfMissing({ playlist_id }); + const response = await this.actions.browse(`VL${playlist_id.replace(/VL/g, '')}`); + return new Playlist(this.actions, response.data); + } + /** + * An alternative to {@link download}. + * Returns deciphered streaming data. + * + * If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}. + */ + async getStreamingData(video_id: string, options: FormatOptions = {}) { + const info = await this.getBasicInfo(video_id); + return info.chooseFormat(options); + } + /** + * Downloads a given video. If you only need the direct download link take a look at {@link getStreamingData}. + * + * If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}. + */ + async download(video_id: string, options?: DownloadOptions) { + throwIfMissing({ video_id }); + const info = await this.getBasicInfo(video_id); + return info.download(options); + } +} +export default Innertube; diff --git a/lib/core/AccountManager.js b/lib/core/AccountManager.js deleted file mode 100644 index 27eef650..00000000 --- a/lib/core/AccountManager.js +++ /dev/null @@ -1,221 +0,0 @@ -'use strict'; - -const Utils = require('../utils/Utils'); -const Constants = require('../utils/Constants'); -const Analytics = require('../parser/youtube/Analytics'); -const Proto = require('../proto'); - -/** @namespace */ -class AccountManager { - #actions; - - /** - * @param {import('./Actions')} actions - */ - constructor (actions) { - this.#actions = actions; - - /** - * API response. - * - * @typedef {{ success: boolean, status_code: number, data: object }} Response - */ - - /** @namespace */ - this.channel = { - /** - * Edits channel name. - * - * @param {string} new_name - * @returns {Promise.} - */ - editName: (new_name) => this.#actions.channel('channel/edit_name', { new_name }), - - /** - * Edits channel description. - * - * @param {string} new_description - * @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 - * @returns {Promise.} - */ - setSubscriptions: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'SPaccount_notifications', option), - - /** - * Recommended content notifications. - * - * @param {boolean} option - ON | OFF - * @returns {Promise.} - */ - setRecommendedVideos: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'SPaccount_notifications', option), - - /** - * Notify about activity on your channel. - * - * @param {boolean} option - ON | OFF - * @returns {Promise.} - */ - setChannelActivity: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'SPaccount_notifications', option), - - /** - * Notify about replies to your comments. - * - * @param {boolean} option - ON | OFF - * @returns {Promise.} - */ - setCommentReplies: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'SPaccount_notifications', option), - - /** - * Notify when others mention your channel. - * - * @param {boolean} option - ON | OFF - * @returns {Promise.} - */ - setMentions: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'SPaccount_notifications', option), - - /** - * Notify when others share your content on their channels. - * - * @param {boolean} option - ON | OFF - * @returns {Promise.} - */ - setSharedContent: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'SPaccount_notifications', option) - }, - privacy: { - /** - * If set to true, your subscriptions won't be visible to others. - * - * @param {boolean} option - ON | OFF - * @returns {Promise.} - */ - setSubscriptionsPrivate: (option) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'SPaccount_privacy', option), - - /** - * If set to true, saved playlists won't appear on your channel. - * - * @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 - * @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)) - 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 option = contents.find((option) => option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemIdForClient == setting_id); - - const setting_item_id = option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemId; - const set_setting = await this.#actions.account('account/set_setting', { - new_value: type == 'SPaccount_privacy' ? !values[new_value] : values[new_value], - setting_item_id - }); - - return set_setting; - } - - /** - * Retrieves channel info. - * - * @returns {Promise.<{ name: string, email: string, channel_id: string, subscriber_count: string, photo: object[] }>} - */ - 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. - * - * @returns {Promise.>} - */ - 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. - * - * @returns {Promise.} - */ - 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); - } -} - -module.exports = AccountManager; \ No newline at end of file diff --git a/lib/core/AccountManager.ts b/lib/core/AccountManager.ts new file mode 100644 index 00000000..d9717c57 --- /dev/null +++ b/lib/core/AccountManager.ts @@ -0,0 +1,151 @@ +import { throwIfMissing, findNode } from '../utils/Utils'; +import Constants from '../utils/Constants'; +import Analytics from '../parser/youtube/Analytics'; +import Proto from '../proto/index'; +import Actions from './Actions'; + +class AccountManager { + #actions; + channel; + settings; + + constructor(actions: Actions) { + this.#actions = actions; + this.channel = { + /** + * Edits channel name. + */ + editName: (new_name: string) => this.#actions.channel('channel/edit_name', { new_name }), + /** + * Edits channel description. + * + */ + editDescription: (new_description: string) => this.#actions.channel('channel/edit_description', { new_description }), + /** + * Retrieves basic channel analytics. + */ + getBasicAnalytics: () => this.getAnalytics() + }; + this.settings = { + notifications: { + /** + * Notify about activity from the channels you're subscribed to. + * + * @param option - ON | OFF + */ + setSubscriptions: (option: boolean) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS, 'SPaccount_notifications', option), + /** + * Recommended content notifications. + * + * @param option - ON | OFF + */ + setRecommendedVideos: (option: boolean) => this.#setSetting(Constants.ACCOUNT_SETTINGS.RECOMMENDED_VIDEOS, 'SPaccount_notifications', option), + /** + * Notify about activity on your channel. + * + * @param option - ON | OFF + */ + setChannelActivity: (option: boolean) => this.#setSetting(Constants.ACCOUNT_SETTINGS.CHANNEL_ACTIVITY, 'SPaccount_notifications', option), + /** + * Notify about replies to your comments. + * + * @param option - ON | OFF + */ + setCommentReplies: (option: boolean) => this.#setSetting(Constants.ACCOUNT_SETTINGS.COMMENT_REPLIES, 'SPaccount_notifications', option), + /** + * Notify when others mention your channel. + * + * @param option - ON | OFF + */ + setMentions: (option: boolean) => this.#setSetting(Constants.ACCOUNT_SETTINGS.USER_MENTION, 'SPaccount_notifications', option), + /** + * Notify when others share your content on their channels. + * + * @param option - ON | OFF + */ + setSharedContent: (option: boolean) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SHARED_CONTENT, 'SPaccount_notifications', option) + }, + privacy: { + /** + * If set to true, your subscriptions won't be visible to others. + * + * @param option - ON | OFF + */ + setSubscriptionsPrivate: (option: boolean) => this.#setSetting(Constants.ACCOUNT_SETTINGS.SUBSCRIPTIONS_PRIVACY, 'SPaccount_privacy', option), + /** + * If set to true, saved playlists won't appear on your channel. + * + * @param option - ON | OFF + */ + setSavedPlaylistsPrivate: (option: boolean) => this.#setSetting(Constants.ACCOUNT_SETTINGS.PLAYLISTS_PRIVACY, 'SPaccount_privacy', option) + } + }; + } + /** + * Internal method to perform changes on an account's settings. + */ + async #setSetting(setting_id: string, type: string, new_value: boolean) { + throwIfMissing({ setting_id, type, new_value }); + const response = await this.#actions.browse(type); + const contents = (() => { + switch (type.trim()) { + case 'SPaccount_notifications': + return findNode(response.data, 'contents', 'Your preferences', 13, false).options; + case 'SPaccount_privacy': + return 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: any) => option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemIdForClient == setting_id); + const setting_item_id = option.settingsSwitchRenderer.enableServiceEndpoint.setSettingEndpoint.settingItemId; + const set_setting = await this.#actions.account('account/set_setting', { + new_value: type == 'SPaccount_privacy' ? !new_value : new_value, + setting_item_id + }); + return set_setting; + } + /** + * Retrieves channel info. + */ + async getInfo() { + const response = await this.#actions.account('account/accounts_list', { client: 'ANDROID' }); + const account_item_section_renderer = 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: any) => 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. + */ + async getTimeWatched() { + const response = await this.#actions.browse('SPtime_watched', { client: 'ANDROID' }); + const rows: any[] = findNode(response.data, 'contents', 'statRowRenderer', 11, false); + const stats = rows.map((row: any) => { + const renderer = row.statRowRenderer; + if (renderer) { + return { + title: renderer.title.runs.map((run: any) => run.text).join(''), + time: renderer.contents.runs.map((run: any) => run.text).join('') + }; + } + }).filter((stat: any) => stat); + return stats; + } + /** + * Retrieves basic channel analytics. + * + */ + 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); + } +} +export default AccountManager; diff --git a/lib/core/Actions.js b/lib/core/Actions.js deleted file mode 100644 index 1de857fc..00000000 --- a/lib/core/Actions.js +++ /dev/null @@ -1,670 +0,0 @@ -'use strict'; - -const Uuid = require('uuid'); -const Proto = require('../proto'); -const Utils = require('../utils/Utils'); -const Constants = require('../utils/Constants'); -const Parser = require('../parser'); - -/** @namespace */ -class Actions { - #session; - #request; - - /** - * @param {import('../Innertube')} session - */ - constructor(session) { - 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 - * and sometimes to retrieve continuations. - * - * @param {string} id - browseId or a continuation token - * @param {object} args - additional arguments - * @param {string} [args.params] - * @param {boolean} [args.is_ytm] - * @param {boolean} [args.is_ctoken] - * @param {string} [args.client] - * @returns {Promise.} - */ - 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; - } - - 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] - * @param {string} [args.channel_id] - * @param {string} [args.comment_id] - * @param {string} [args.comment_action] - * @returns {Promise.} - */ - 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.target.videoId = args.video_id; - - if (args.params) { - data.params = args.params; - } - break; - case 'subscription/subscribe': - case 'subscription/unsubscribe': - data.channelIds = [ args.channel_id ]; - data.params = action === 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA'; - break; - case 'comment/create_comment': - data.commentText = args.text; - data.createCommentParams = Proto.encodeCommentParams(args.video_id); - break; - case 'comment/create_comment_reply': - data.createReplyParams = Proto.encodeCommentReplyParams(args.comment_id, args.video_id); - 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: - 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] - * @param {string} [args.setting_item_id] - * @returns {Promise.} - */ - async account(action, args = {}) { - if (!this.#session.logged_in) - throw new Utils.InnertubeError('You are not signed in'); - - const data = { - client: args.client - }; - - switch (action) { - case 'account/set_setting': - data.newValue = { - boolValue: args.new_value - }; - data.settingItemId = args.setting_item_id; - break; - case 'account/accounts_list': - break; - 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] - * @param {string} [args.options.period] - * @param {string} [args.options.duration] - * @param {string} [args.options.order] - * @param {string} [args.client] - * @param {string} [args.ctoken] - * @returns {Promise.} - */ - async search(args = {}) { - const data = { client: args.client }; - - if (args.query) { - data.query = args.query; - } - - if (args.ctoken) { - data.continuation = args.ctoken; - } - - if (args.params) { - data.params = args.params; - } - - if (args.filters) { - if (args.client == 'YTMUSIC') { - data.params = Proto.encodeMusicSearchFilters(args.filters); - } else { - data.params = 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.} - */ - async searchSound(args = {}) { - const data = { - query: args.query, - 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] - * @param {string} [args.new_description] - * @returns {Promise.} - */ - async channel(action, args = {}) { - if (!this.#session.logged_in) - throw new Utils.InnertubeError('You are not signed in'); - - const data = { - client: args.client || 'ANDROID' - }; - - switch (action) { - case 'channel/edit_name': - data.givenName = args.new_name; - break; - case 'channel/edit_description': - data.description = args.new_description; - break; - case 'channel/get_profile_editor': - break; - 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] - * @param {string} [args.ids] - * @param {string} [args.playlist_id] - * @param {string} [args.action] - * @returns {Promise.} - */ - async playlist(action, args = {}) { - 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; - data.videoIds = args.ids; - break; - case 'playlist/delete': - data.playlistId = args.playlist_id; - break; - 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: - 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] - * @param {string} [args.channel_id] - * @param {string} [args.ctoken] - * @returns {Promise.} - */ - async notifications(action, args = {}) { - 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 - }; - data.params = Proto.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase()]); - break; - case 'get_notification_menu': - data.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX'; - if (args.ctoken) data.ctoken = args.ctoken; - break; - case 'record_interactions': - data.serializedRecordNotificationInteractionsRequest = args.params; - break; - case 'get_unseen_count': - break; - 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] - * @param {string} [args.video_id] - * @param {string} [args.channel_id] - * @param {string} [args.ctoken] - * @param {string} [args.params] - * @returns {Promise.} - */ - async livechat(action, args = {}) { - const data = { client: args.client }; - - switch (action) { - case 'live_chat/get_live_chat': - case 'live_chat/get_live_chat_replay': - data.continuation = args.ctoken; - break; - case 'live_chat/send_message': - data.params = Proto.encodeMessageParams(args.channel_id, args.video_id); - data.clientMessageId = Uuid.v4(); - data.richMessage = { - textSegments: [ { - text: args.text - } ] - }; - break; - case 'live_chat/get_item_context_menu': - // Note: this is currently broken due to a recent refactor - break; - case 'live_chat/moderate': - data.params = args.params; - break; - case 'updated_metadata': - data.videoId = args.video_id; - if (args.ctoken) data.continuation = args.ctoken; - break; - 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.} - */ - async thumbnails(args = {}) { - const data = { - 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 - * @returns {Promise.} - */ - 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] - * @param {string} [args.params] - * @returns {Promise.} - */ - 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; - break; - case 'flag/get_form': - data.params = args.params; - break; - 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] - * @returns {Promise.} - */ - async music(action, args) { - const data = { - 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] - * @param {string} [args.client] - * @returns {Promise.} - */ - async next(args = {}) { - const data = { client: args.client }; - - if (args.ctoken) { - data.continuation = args.ctoken; - } - - if (args.video_id) { - 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] - * @returns {Promise.} - */ - async getVideoInfo(id, cpn, client) { - const data = { - playbackContext: { - contentPlaybackContext: { - vis: 0, - splay: false, - referer: 'https://www.youtube.com', - currentUrl: `/watch?v=${id}`, - autonavState: 'STATE_OFF', - signatureTimestamp: this.#session.sts, - autoCaptionsDefaultOn: false, - html5Preference: 'HTML5_PREF_WANTS', - lactMilliseconds: '-1' - } - }, - attestationRequest: { - omitBotguardData: true - }, - videoId: id - }; - - 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)) - throw new Utils.InnertubeError('Invalid client', client); - - const response = await ({ - YOUTUBE: () => this.#request({ - url: 'search', - baseURL: Constants.URLS.YT_SUGGESTIONS, - params: { - q: query, - ds: 'yt', - client: 'youtube', - xssi: 't', - oe: 'UTF', - gl: this.#session.context.client.gl, - hl: this.#session.context.client.hl - } - }), - 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.} - */ - 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 - * @param {boolean} [args.parse] - */ - async execute(action, args) { - const data = { ...args }; - - if (Reflect.has(data, 'parse')) - delete data.parse; - - 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; - } - - const response = await this.#request.post(action, data); - - if (args.parse) { - return Parser.parseResponse(response.data); - } - - return response; - } - - #needsLogin(id) { - return [ - 'FElibrary', - 'FEhistory', - 'FEsubscriptions', - 'SPaccount_notifications', - 'SPaccount_privacy', - 'SPtime_watched' - ].includes(id); - } -} - -module.exports = Actions; \ No newline at end of file diff --git a/lib/core/Actions.ts b/lib/core/Actions.ts new file mode 100644 index 00000000..0a804586 --- /dev/null +++ b/lib/core/Actions.ts @@ -0,0 +1,699 @@ +import Proto from '../proto/index'; +import { hasKeys, InnertubeError, MissingParamError, uuidv4 } from '../utils/Utils'; +import Constants from '../utils/Constants'; +import Parser, { ParsedResponse } from '../parser/index'; +import Session from './Session'; + + +export interface BrowseArgs { + params?: string; + is_ytm?: boolean; + is_ctoken?: boolean; + client?: string; +} + +export interface EngageArgs { + video_id?: string; + channel_id?: string; + comment_id?: string; + comment_action?: string; + params?: string; + text?: string; + target_language?: string; +} + +export interface AccountArgs { + new_value?: string | boolean; // TODO: is this correct? + setting_item_id?: string; + client?: string; +} + +export interface SearchArgs { + query?: string, + options?: { + period?: string, + duration?: string, + order?: string + }, + client?: string, + ctoken?: string, + params?: string + filters?: any // TODO: what is this type?? +} + +export interface AxioslikeResponse { + success: boolean; + status_code: number; + data: any; +} + +export type ActionsResponse = Promise; + +class Actions { + #session; + constructor(session: Session) { + this.#session = session; + } + get session() { + return this.#session; + } + + /** + * Mimmics the Axios API using Fetch's Response object. + */ + async #wrap(response: Response) { + return { + success: response.ok, + status_code: response.status, + data: await response.json() + }; + } + /** + * Covers `/browse` endpoint, mostly used to access + * YouTube's sections such as the home feed, etc + * and sometimes to retrieve continuations. + * + * @param id - browseId or a continuation token + * @param args - additional arguments + */ + async browse(id: string, args: BrowseArgs = {}) { + if (this.#needsLogin(id) && !this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + const data: Record = {}; + 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.#session.http.fetch('/browse', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Covers endpoints used to perform direct interactions + * on YouTube. + */ + async engage(action: string, args: EngageArgs = {}) { + if (!this.#session.logged_in && !args.hasOwnProperty('text')) + throw new InnertubeError('You are not signed in'); + const data: Record = {}; + switch (action) { + case 'like/like': + case 'like/dislike': + case 'like/removelike': + if (!hasKeys(args, 'video_id')) + throw new MissingParamError('Arguments lacks video_id'); + data.target = {}; + data.target.videoId = args.video_id; + if (args.params) { + data.params = args.params; + } + break; + case 'subscription/subscribe': + case 'subscription/unsubscribe': + if (!hasKeys(args, 'channel_id')) + throw new MissingParamError('Arguments lacks channel_id'); + data.channelIds = [ args.channel_id ]; + data.params = action === 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA'; + break; + case 'comment/create_comment': + data.commentText = args.text; + if (!hasKeys(args, 'video_id')) + throw new MissingParamError('Arguments lacks video_id'); + data.createCommentParams = Proto.encodeCommentParams(args.video_id); + break; + case 'comment/create_comment_reply': + if (!hasKeys(args, 'comment_id', 'video_id', 'text')) + throw new MissingParamError('Arguments lacks comment_id, video_id or text'); + data.createReplyParams = Proto.encodeCommentReplyParams(args.comment_id, args.video_id); + 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: + break; + } + })(); + data.actions = [ target_action ]; + break; + default: + throw new InnertubeError('Action not implemented', action); + } + const response = await this.#session.http.fetch(`/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Covers endpoints related to account management. + */ + async account(action: string, args: AccountArgs = {}) { + if (!this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + const data: Record = { + client: args.client + }; + switch (action) { + case 'account/set_setting': + data.newValue = { + boolValue: args.new_value + }; + data.settingItemId = args.setting_item_id; + break; + case 'account/accounts_list': + break; + default: + throw new InnertubeError('Action not implemented', action); + } + const response = await this.#session.http.fetch(`/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Endpoint used for search. + */ + async search(args: SearchArgs = {}) { + const data: Record = { client: args.client }; + if (args.query) { + data.query = args.query; + } + if (args.ctoken) { + data.continuation = args.ctoken; + } + if (args.params) { + data.params = args.params; + } + if (args.filters) { + if (args.client == 'YTMUSIC') { + data.params = Proto.encodeMusicSearchFilters(args.filters); + } else { + data.params = Proto.encodeSearchFilters(args.filters); + } + } + const response = await this.#session.http.fetch('/search', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Endpoint used fo Shorts' sound search. + */ + async searchSound(args: { + query: string; + }) { + const data = { + query: args.query, + client: 'ANDROID' + }; + const response = await this.#session.http.fetch('/sfv/search', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Channel management endpoints. + * + */ + async channel(action: string, args: { + new_name?: string; + new_description?: string; + client?: string; + } = {}) { + if (!this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + const data: Record = { + client: args.client || 'ANDROID' + }; + switch (action) { + case 'channel/edit_name': + data.givenName = args.new_name; + break; + case 'channel/edit_description': + data.description = args.new_description; + break; + case 'channel/get_profile_editor': + break; + default: + throw new InnertubeError('Action not implemented', action); + } + const response = await this.#session.http.fetch(`/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Covers endpoints used for playlist management. + * + */ + async playlist(action: string, args: { + title?: string; + ids?: string[]; // TODO: this was a string before, but I made it an array, is this correct? + playlist_id?: string; + action?: string; + } = {}) { + if (!this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + const data: Record = {}; + switch (action) { + case 'playlist/create': + data.title = args.title; + data.videoIds = args.ids; + break; + case 'playlist/delete': + data.playlistId = args.playlist_id; + break; + case 'browse/edit_playlist': + if (!hasKeys(args, 'ids')) + throw new MissingParamError('Arguments lacks ids'); + 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: + break; + } + }); + break; + default: + throw new InnertubeError('Action not implemented', action); + } + const response = await this.#session.http.fetch(`/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Covers endpoints used for notifications management. + */ + async notifications(action: string, args: { + pref?: string; + channel_id?: string; + ctoken?: string; + params?: string + } = {}) { + if (!this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + const data: Record = {}; + switch (action) { + case 'modify_channel_preference': + if (!hasKeys(args, 'channel_id', 'pref')) + throw new MissingParamError('Arguments lacks channel_id or pref'); + const pref_types = { + PERSONALIZED: 1, + ALL: 2, + NONE: 3 + }; + if (!Object.keys(pref_types).includes(args.pref.toUpperCase())) + throw new InnertubeError('Invalid preference type', args.pref); + data.params = Proto.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase() as keyof typeof pref_types]); + break; + case 'get_notification_menu': + data.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX'; + if (args.ctoken) + data.ctoken = args.ctoken; + break; + case 'record_interactions': + data.serializedRecordNotificationInteractionsRequest = args.params; + break; + case 'get_unseen_count': + break; + default: + throw new InnertubeError('Action not implemented', action); + } + const response = await this.#session.http.fetch(`/notification/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Covers livechat endpoints. + */ + async livechat(action: string, args: { + text?: string; + video_id?: string; + channel_id?: string; + ctoken?: string; + params?: string; + client?: string; + } = {}) { + // TODO: should client be required? + const data: Record = { client: args.client }; + switch (action) { + case 'live_chat/get_live_chat': + case 'live_chat/get_live_chat_replay': + data.continuation = args.ctoken; + break; + case 'live_chat/send_message': + if (!hasKeys(args, 'channel_id', 'video_id', 'text')) + throw new MissingParamError('Arguments lacks channel_id, video_id or text'); + data.params = Proto.encodeMessageParams(args.channel_id, args.video_id); + data.clientMessageId = uuidv4(); + data.richMessage = { + textSegments: [ { + text: args.text + } ] + }; + break; + case 'live_chat/get_item_context_menu': + // Note: this is currently broken due to a recent refactor + // TODO: this should be implemented + break; + case 'live_chat/moderate': + data.params = args.params; + break; + case 'updated_metadata': + data.videoId = args.video_id; + if (args.ctoken) + data.continuation = args.ctoken; + break; + default: + throw new InnertubeError('Action not implemented', action); + } + const response = await this.#session.http.fetch(`/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Endpoint used to retrieve video thumbnails. + */ + async thumbnails(args: { + video_id: string; + }) { + const data = { + client: 'ANDROID', + videoId: args.video_id + }; + const response = await this.#session.http.fetch('/thumbnails', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(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); + * ``` + */ + async geo(action: string, args: { + input: string; + }) { + if (!this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + const data = { + input: args.input, + client: 'ANDROID' + }; + const response = await this.#session.http.fetch(`/geo/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Covers endpoints used to report content. + */ + async flag(action: string, args: { + action: string; + params?: string; + }) { + if (!this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + const data: Record = {}; + switch (action) { + case 'flag/flag': + data.action = args.action; + break; + case 'flag/get_form': + data.params = args.params; + break; + default: + throw new InnertubeError('Action not implemented', action); + } + const response = await this.#session.http.fetch(`/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Covers specific YouTube Music endpoints. + */ + async music(action: string, args: { + input?: string; + }) { + const data = { + input: args.input || '', + client: 'YTMUSIC' + }; + const response = await this.#session.http.fetch(`/music/${action}`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Mostly used for pagination and specific operations. + */ + async next(args: { + video_id?: string; + ctoken?: string; + client?: string; + } = {}) { + const data: Record = { client: args.client }; + if (args.ctoken) { + data.continuation = args.ctoken; + } + if (args.video_id) { + data.videoId = args.video_id; + } + const response = await this.#session.http.fetch('/next', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Used to retrieve video info. + */ + async getVideoInfo(id: string, cpn?: string, client?: string) { + const data: Record = { + playbackContext: { + contentPlaybackContext: { + vis: 0, + splay: false, + referer: 'https://www.youtube.com', + currentUrl: `/watch?v=${id}`, + autonavState: 'STATE_OFF', + signatureTimestamp: this.#session.player.sts, + autoCaptionsDefaultOn: false, + html5Preference: 'HTML5_PREF_WANTS', + lactMilliseconds: '-1' + } + }, + attestationRequest: { + omitBotguardData: true + }, + videoId: id + }; + if (client) { + data.client = client; + } + if (cpn) { + data.cpn = cpn; + } + const response = await this.#session.http.fetch('/player', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Covers search suggestion endpoints. + */ + async getSearchSuggestions(client: 'YOUTUBE' | 'YTMUSIC', query: string) { + if (![ 'YOUTUBE', 'YTMUSIC' ].includes(client)) + throw new InnertubeError('Invalid client', client); + const response = await ({ + YOUTUBE: async () => { + const params = new URLSearchParams({ + q: query, + ds: 'yt', + client: 'youtube', + xssi: 't', + oe: 'UTF', + gl: this.#session.context.client.gl, + hl: this.#session.context.client.hl + }); + const response = await this.#session.http.fetch(`search?${params.toString()}`, { + baseURL: Constants.URLS.YT_SUGGESTIONS, + method: 'GET' + }); + return this.#wrap(response); + }, + YTMUSIC: () => this.music('get_search_suggestions', { + input: query + }) + }[client])(); + return response; + } + /** + * Endpoint used to retrieve user mention suggestions. + */ + async getUserMentionSuggestions(args: { + input: string; + }) { + if (!this.#session.logged_in) + throw new InnertubeError('You are not signed in'); + const data = { + input: args.input, + client: 'ANDROID' + }; + const response = await this.#session.http.fetch('/get_user_mention_suggestions', { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + return this.#wrap(response); + } + /** + * Executes an API call. + * @param action - endpoint + * @param args - call arguments + */ + async execute(action: string, args: { + [key: string]: any; + parse: true; + }) : Promise; + async execute(action: string, args: { + [key: string]: any; + parse?: false; + }) : Promise; + async execute(action: string, args: { + [key: string]: any; + parse?: boolean; + }): Promise { + const data = { ...args }; + if (Reflect.has(data, 'parse')) + delete data.parse; + 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; + } + const response = await this.#session.http.fetch(action, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }); + if (args.parse) { + return Parser.parseResponse(await response.json()); + } + return this.#wrap(response); + } + #needsLogin(id: string) { + return [ + 'FElibrary', + 'FEhistory', + 'FEsubscriptions', + 'SPaccount_notifications', + 'SPaccount_privacy', + 'SPtime_watched' + ].includes(id); + } +} +// TODO: maybe do this inferrance in a more elegant way +export default Actions; diff --git a/lib/core/Feed.js b/lib/core/Feed.js deleted file mode 100644 index 303fe191..00000000 --- a/lib/core/Feed.js +++ /dev/null @@ -1,216 +0,0 @@ -'use strict'; - -const Parser = require('../parser'); -const { InnertubeError } = require('../utils/Utils'); - -// TODO: add a way subdivide into sections and return subfeeds? - -class Feed { - #page; - - /** @type {import('../parser/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; - } else { - this.#page = Parser.parseResponse(data); - } - - 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.#actions = actions; - } - - /** - * Get all videos on a given page via memo - * - * @param {Map} memo - * @returns {Array} - */ - static getVideosFromMemo(memo) { - const videos = memo.get('Video') || []; - const grid_videos = memo.get('GridVideo') || []; - const compact_videos = memo.get('CompactVideo') || []; - 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, - ...compact_videos, - ...playlist_videos, - ...playlist_panel_videos, - ...watch_card_compact_videos - ]; - } - - /** - * Get all playlists on a given page via memo - * - * @param {Map} memo - * @returns {Array} - */ - static getPlaylistsFromMemo(memo) { - const playlists = memo.get('Playlist') || []; - const grid_playlists = memo.get('GridPlaylist') || []; - return [ ...playlists, ...grid_playlists ]; - } - - /** - * Get all the videos in the feed - */ - get videos() { - return Feed.getVideosFromMemo(this.#memo); - } - - /** - * Get all the community posts in the feed - * - * @returns {import('../parser/classes/BackstagePost')[] | import('../parser/classes/Post')[]} - */ - get posts() { - return this.#memo.get('BackstagePost') || this.#memo.get('Post') || []; - } - - /** - * Get all the channels in the feed - * - * @returns {Array} - */ - get channels() { - const channels = this.#memo.get('Channel') || []; - const grid_channels = this.#memo.get('GridChannel') || []; - return [ ...channels, ...grid_channels ]; - } - - /** - * Get all playlists in the feed - * - * @returns {Array} - */ - get playlists() { - return Feed.getPlaylistsFromMemo(this.#memo); - } - - get memo() { - return this.#memo; - } - - /** - * Returns contents from the page. - * - * @returns {*} - */ - get contents() { - 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')[]} - */ - get shelves() { - const shelf = this.#memo.get('Shelf') || []; - const rich_shelf = this.#memo.get('RichShelf') || []; - const reel_shelf = this.#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); - } - - /** - * Returns secondary contents from the page. - * - * @returns {*} - */ - 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. - * - * @returns {boolean} - */ - get has_continuation() { - return (this.#memo.get('ContinuationItem') || []).length > 0; - } - - /** - * Retrieves continuation data as it is. - * - * @returns {Promise.} - */ - async getContinuationData() { - if (this.#continuation) { - if (this.#continuation.length > 1) - throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page'); - if (this.#continuation.length === 0) - throw new InnertubeError('There are no continuations'); - - const response = await this.#continuation[0].endpoint.call(this.#actions); - - return response; - } - - this.#continuation = this.#memo.get('ContinuationItem'); - - if (this.#continuation) - return this.getContinuationData(); - - return null; - } - - /** - * Retrieves next batch of contents and returns a new {@link Feed} object. - * - * @returns {Promise.} - */ - async getContinuation() { - const continuation_data = await this.getContinuationData(); - return new Feed(this.actions, continuation_data, true); - } -} - -module.exports = Feed; \ No newline at end of file diff --git a/lib/core/Feed.ts b/lib/core/Feed.ts new file mode 100644 index 00000000..79de94bd --- /dev/null +++ b/lib/core/Feed.ts @@ -0,0 +1,173 @@ +import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction'; +import BackstagePost from '../parser/classes/BackstagePost'; +import Channel from '../parser/classes/Channel'; +import CompactVideo from '../parser/classes/CompactVideo'; +import ContinuationItem from '../parser/classes/ContinuationItem'; +import GridChannel from '../parser/classes/GridChannel'; +import GridPlaylist from '../parser/classes/GridPlaylist'; +import GridVideo from '../parser/classes/GridVideo'; +import Playlist from '../parser/classes/Playlist'; +import PlaylistPanelVideo from '../parser/classes/PlaylistPanelVideo'; +import PlaylistVideo from '../parser/classes/PlaylistVideo'; +import Post from '../parser/classes/Post'; +import ReelShelf from '../parser/classes/ReelShelf'; +import RichShelf from '../parser/classes/RichShelf'; +import Shelf from '../parser/classes/Shelf'; +import Tab from '../parser/classes/Tab'; +import TwoColumnBrowseResults from '../parser/classes/TwoColumnBrowseResults'; +import TwoColumnSearchResults from '../parser/classes/TwoColumnSearchResults'; +import Video from '../parser/classes/Video'; +import WatchCardCompactVideo from '../parser/classes/WatchCardCompactVideo'; +import { Memo, ObservedArray } from '../parser/helpers'; +import Parser, { ParsedResponse, ReloadContinuationItemsCommand } from '../parser/index'; +import { InnertubeError } from '../utils/Utils'; +import Actions from './Actions'; + +// TODO: add a way subdivide into sections and return subfeeds? +class Feed { + #page: ParsedResponse; + #continuation?: ObservedArray; + #actions; + #memo; + constructor(actions: Actions, data: any, already_parsed = false) { + if (data.on_response_received_actions || data.on_response_received_endpoints || already_parsed) { + this.#page = data; + } else { + this.#page = Parser.parseResponse(data); + } + const 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 : undefined; + if (!memo) + throw new InnertubeError('No memo found in feed'); + this.#memo = memo; + this.#actions = actions; + } + /** + * Get all videos on a given page via memo + */ + static getVideosFromMemo(memo: Memo) { + return memo.getType