You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
β² How do i fetch youtube caption with this package?
You can fetch YouTube captions using the VideoInfo#getTranscript() method. Here's an example:
import{Innertube}from'youtubei.js';(async()=>{constyt=awaitInnertube.create();// Get the video infoconstvideoInfo=awaityt.getInfo('videoId');// Get the transcriptconsttranscript=awaitvideoInfo.getTranscript();// Print the transcriptconsole.log(transcript);})();
This code will first create an Innertube instance and then fetch the video info for the video with the ID videoId. Then, it will call getTranscript() on the videoInfo object to retrieve the transcript. Finally, it will print the transcript to the console.
Note:
The getTranscript() method returns a TranscriptInfo object, which contains the transcript data.
The TranscriptInfo object has a languages property, which contains a list of available languages for the transcript.
You can use the selectLanguage() method on the TranscriptInfo object to select a specific language for the transcript.
# Changelog## [10.3.0](https://github.com/LuanRT/YouTube.js/compare/v10.2.0...v10.3.0) (2024-08-01)### Features***parser:** Add `EomSettingsDisclaimer` node ([#703](https://github.com/LuanRT/YouTube.js/issues/703)) ([a9bf225](https://github.com/LuanRT/YouTube.js/commit/a9bf225a62108e47a50316235a83a814c682d745))
***PlaylistManager:** Add ability to remove videos by set ID ([#715](https://github.com/LuanRT/YouTube.js/issues/715)) ([d85fbc5](https://github.com/LuanRT/YouTube.js/commit/d85fbc56cf0fd7367b182ae36e65c1701bc5e62d))
### Bug Fixes***HTTPClient:** Adjust more context fields for the iOS client ([#705](https://github.com/LuanRT/YouTube.js/issues/705)) ([3153375](https://github.com/LuanRT/YouTube.js/commit/3153375bcaa6c03afba9da8474e6a9d37471ed29))
## [10.2.0](https://github.com/LuanRT/YouTube.js/compare/v10.1.0...v10.2.0) (2024-07-25)### Features***Format:** Add `is_secondary` for detecting secondary audio tracks ([#697](https://github.com/LuanRT/YouTube.js/issues/697)) ([a352dde](https://github.com/LuanRT/YouTube.js/commit/a352ddeb9db001e99f49025048ad0942d84f1b3e))
***parser:** add classdata to unhandled parse errors ([#691](https://github.com/LuanRT/YouTube.js/issues/691)) ([090539b](https://github.com/LuanRT/YouTube.js/commit/090539b28f9bc3387d01e37325ba5741b33b1765))
***proto:** Add `comment_id` to commentSectionParams ([#693](https://github.com/LuanRT/YouTube.js/issues/693)) ([a5f6209](https://github.com/LuanRT/YouTube.js/commit/a5f62093a18705fc822abd86beaa81788b6535ce))
### Bug Fixes***parser:** ignore MiniGameCardView node ([#692](https://github.com/LuanRT/YouTube.js/issues/692)) ([6d0bc89](https://github.com/LuanRT/YouTube.js/commit/6d0bc89be18f27a8ce74517f5cab5020d6790328))
***parser:** ThumbnailView background color ([#686](https://github.com/LuanRT/YouTube.js/issues/686)) ([0f8f92a](https://github.com/LuanRT/YouTube.js/commit/0f8f92a28a5b6143e890626b22ce570730a0cf09))
***Player:** Bump cache version ([#702](https://github.com/LuanRT/YouTube.js/issues/702)) ([6765f4e](https://github.com/LuanRT/YouTube.js/commit/6765f4e0d791c657fc7411e9cdd2c0f9284e9982))
***Player:** Fix extracting the n-token decipher algorithm again ([#701](https://github.com/LuanRT/YouTube.js/issues/701)) ([3048f70](https://github.com/LuanRT/YouTube.js/commit/3048f70f60756884bd7b591d770f7b6343cfa259))
## [10.1.0](https://github.com/LuanRT/YouTube.js/compare/v10.0.0...v10.1.0) (2024-07-10)### Features***Session:** Add `configInfo` to InnerTube context ([5a8fd3a](https://github.com/LuanRT/YouTube.js/commit/5a8fd3ad37bce1decad28ec3727453ddd430a561))
***toDash:** Add option to include WebVTT or TTML captions ([#673](https://github.com/LuanRT/YouTube.js/issues/673)) ([bd9f6ac](https://github.com/LuanRT/YouTube.js/commit/bd9f6ac64ca9ba96e856aabe5fcc175fd9c294dc))
***toDash:** Add the "dub" role to translated captions ([#677](https://github.com/LuanRT/YouTube.js/issues/677)) ([858cdd1](https://github.com/LuanRT/YouTube.js/commit/858cdd197cb2bb1e1d7a7285966cb56043ad8961))
### Bug Fixes***FormatUtils:** Throw an error if download requests fails ([a19511d](https://github.com/LuanRT/YouTube.js/commit/a19511de24bb82007aab072844efe64bbb8698da))
***InfoPanelContent:** Update InfoPanelContent node to also support `paragraphs` ([4cbaa79](https://github.com/LuanRT/YouTube.js/commit/4cbaa7983f35a82b9907197769672ac3b300bfbe))
***Player:** Fix extracting the n-token decipher algorithm ([#682](https://github.com/LuanRT/YouTube.js/issues/682)) ([142a7d0](https://github.com/LuanRT/YouTube.js/commit/142a7d042885188605bdc0655d3733502d1e20fa))
***proto:** Update `Context` message ([62ac2f6](https://github.com/LuanRT/YouTube.js/commit/62ac2f6f32d35fec3c31b5f5d556bd4569fa49f9)), closes [#681](https://github.com/LuanRT/YouTube.js/issues/681)***Session:** Round UTC offset minutes ([84f90aa](https://github.com/LuanRT/YouTube.js/commit/84f90aaf2908ecacb9dfb6ce5497c4c4d14a72c3))
***toDash:** Fix image representations not being spec compliant ([#672](https://github.com/LuanRT/YouTube.js/issues/672)) ([e5aab9a](https://github.com/LuanRT/YouTube.js/commit/e5aab9a9b35f0752cd5ca50bfa25936dce4718c6))
***YTMusic:** Add support for new header layouts ([14c3a06](https://github.com/LuanRT/YouTube.js/commit/14c3a06d402989e98a9d32c79b2dc26f74fb0219))
## [10.0.0](https://github.com/LuanRT/YouTube.js/compare/v9.4.0...v10.0.0) (2024-06-09)### β BREAKING CHANGES***Innertube#getPlaylists:** Return a `Feed` instance instead of items
***OAuth2:** Rewrite auth module ([#661](https://github.com/LuanRT/YouTube.js/issues/661))
### Features***Format:** Add `is_drc` ([#656](https://github.com/LuanRT/YouTube.js/issues/656)) ([6bb2086](https://github.com/LuanRT/YouTube.js/commit/6bb2086875d089f47c5f86ce94db9e32cb051319))
***Platform:** Add support for `react-native` platform ([#593](https://github.com/LuanRT/YouTube.js/issues/593)) ([2980a60](https://github.com/LuanRT/YouTube.js/commit/2980a608b67f18416d7f73f1bdbcf4b897307b26))
***Session:** Add `enable_session_cache` option ([#664](https://github.com/LuanRT/YouTube.js/issues/664)) ([7953296](https://github.com/LuanRT/YouTube.js/commit/795329658033652625d2d61b275ccf703573a437))
***toDash:** Add support for stable volume/DRC ([#662](https://github.com/LuanRT/YouTube.js/issues/662)) ([031ffb6](https://github.com/LuanRT/YouTube.js/commit/031ffb696e3b7e160779e8b55a49b0cfa9f95620))
### Bug Fixes***ButtonView:** Rename `type` property to `button_type` ([15f3b5f](https://github.com/LuanRT/YouTube.js/commit/15f3b5fdba17f11cddada168de268546875e48f9))
***Cache:** Use `TextEncoder` to encode compressed data ([384b80e](https://github.com/LuanRT/YouTube.js/commit/384b80ee41d7547a00d8dc17c50c8542629264b5))
***FlexibleActionsView:** Update actions array type to include `ToggleButtonView` ([040a091](https://github.com/LuanRT/YouTube.js/commit/040a09163903b914f546d5083dbfdeab7175b24c))
***InfoPanelContainer:** Use new attributed text prop ([5cdb9e1](https://github.com/LuanRT/YouTube.js/commit/5cdb9e1e2fa4ad5abdb3659bb35d0b3edc60123c))
***ItemSection:** Fix `target_id` not being set because of a typo. ([#655](https://github.com/LuanRT/YouTube.js/issues/655)) ([8106654](https://github.com/LuanRT/YouTube.js/commit/810665407e91b2890a8e555fd759d67ccd800379))
***MusicResponsiveHeader:** Add `Text` import ([583fd9f](https://github.com/LuanRT/YouTube.js/commit/583fd9f8d70735d071b34bd1d68faa62eeac593a))
### Performance Improvements***general:** Add session cache and LZW compression ([#663](https://github.com/LuanRT/YouTube.js/issues/663)) ([cf29664](https://github.com/LuanRT/YouTube.js/commit/cf29664d376ff792602400ef9a4ac301c676735c))
### Code Refactoring***Innertube#getPlaylists:** Return a `Feed` instance instead of items ([7660450](https://github.com/LuanRT/YouTube.js/commit/766045049d7154866e6fe32f6d965025d736d77d))
***OAuth2:** Rewrite auth module ([#661](https://github.com/LuanRT/YouTube.js/issues/661)) ([b6ce5f9](https://github.com/LuanRT/YouTube.js/commit/b6ce5f903fa2285cb381d73aedf02cc5e2712478))
## [9.4.0](https://github.com/LuanRT/YouTube.js/compare/v9.3.0...v9.4.0) (2024-04-29)### Features***Format:** Add `projection_type` and `stereo_layout` ([#643](https://github.com/LuanRT/YouTube.js/issues/643)) ([064436c](https://github.com/LuanRT/YouTube.js/commit/064436cef30e892d8f569d4f7b146557fd72b09f))
***Format:** Add `spatial_audio_type` ([#647](https://github.com/LuanRT/YouTube.js/issues/647)) ([0ba8c54](https://github.com/LuanRT/YouTube.js/commit/0ba8c54257b068d7e4518c982396acb42f1dd41d))
***Parser:** Add `MusicResponsiveHeader` node ([ea82bea](https://github.com/LuanRT/YouTube.js/commit/ea82beaa10f6c877d6dd3102e10f6ae382560e0f))
## [9.3.0](https://github.com/LuanRT/YouTube.js/compare/v9.2.1...v9.3.0) (2024-04-11)### Features***CommentView:** Implement comment interaction methods ([1c08bfe](https://github.com/LuanRT/YouTube.js/commit/1c08bfe113804c69fbc4e49b42442d9a63d73be6))
### Bug Fixes***CommentThread:** Replies not being parsed correctly ([66e34f9](https://github.com/LuanRT/YouTube.js/commit/66e34f9388429a2088d5c5835d19eebdc881c957))
## [9.2.1](https://github.com/LuanRT/YouTube.js/compare/v9.2.0...v9.2.1) (2024-04-09)### Bug Fixes***toDash:** Add missing transfer characteristics for h264 streams ([#631](https://github.com/LuanRT/YouTube.js/issues/631)) ([0107049](https://github.com/LuanRT/YouTube.js/commit/010704929fa4b737f68a5a1f10bf0b166cfbf905))
## [9.2.0](https://github.com/LuanRT/YouTube.js/compare/v9.1.0...v9.2.0) (2024-03-31)### Features* add support of cloudflare workers ([#596](https://github.com/LuanRT/YouTube.js/issues/596)) ([2029aec](https://github.com/LuanRT/YouTube.js/commit/2029aec90de3c0fdb022094d7b704a2feed4133b))
***parser:** Support CommentView nodes ([#614](https://github.com/LuanRT/YouTube.js/issues/614)) ([900f557](https://github.com/LuanRT/YouTube.js/commit/900f5572021d348e7012909f2080e52eac06adae))
***parser:** Support LockupView and it's child nodes ([#609](https://github.com/LuanRT/YouTube.js/issues/609)) ([7ca2a0c](https://github.com/LuanRT/YouTube.js/commit/7ca2a0c3e43ebd4b9443e69b7432f302b09e9c7a))
***Text:** Support formatting and emojis in `fromAttributed` ([#615](https://github.com/LuanRT/YouTube.js/issues/615)) ([e6f1f07](https://github.com/LuanRT/YouTube.js/commit/e6f1f078a828f8ea5db1fe7aec9f677bc53694e3))
### Bug Fixes***Cache:** handle the value read from the db correctly according to its type ([#620](https://github.com/LuanRT/YouTube.js/issues/620)) ([3170659](https://github.com/LuanRT/YouTube.js/commit/317065988007c860bf6173b0ac3c3d685ed81d20))
***PlayerEndpoint:** Workaround for "The following content is not available on this app" (Android) ([#624](https://github.com/LuanRT/YouTube.js/issues/624)) ([d589365](https://github.com/LuanRT/YouTube.js/commit/d589365ea27f540ff36e33a65362c932cd28c274))
## [9.1.0](https://github.com/LuanRT/YouTube.js/compare/v9.0.2...v9.1.0) (2024-02-23)### Features***Format:** Support caption tracks in adaptive formats ([#598](https://github.com/LuanRT/YouTube.js/issues/598)) ([bff65f8](https://github.com/LuanRT/YouTube.js/commit/bff65f8889c32813ec05bd187f3a4386fc6127c0))
### Bug Fixes***Playlist:**`items` getter failing if a playlist contains Shorts ([89fa3b2](https://github.com/LuanRT/YouTube.js/commit/89fa3b27a839d98aaf8bd70dd75220ee309c2bea))
***Session:** Don't try to extract api version from service worker ([2068dfb](https://github.com/LuanRT/YouTube.js/commit/2068dfb73eefc0e40157421d4e5b4096c0c8429c))
## [9.0.2](https://github.com/LuanRT/YouTube.js/compare/v9.0.1...v9.0.2) (2024-01-31)### Bug Fixes***VideoInfo:** Fix error because of typo in getWatchNextContinuation ([#590](https://github.com/LuanRT/YouTube.js/issues/590)) ([b21eb9f](https://github.com/LuanRT/YouTube.js/commit/b21eb9f33d956e130bac98712384125ae04ace47))
## [9.0.1](https://github.com/LuanRT/YouTube.js/compare/v9.0.0...v9.0.1) (2024-01-26)### Bug Fixes***build:** Circular imports causing issues with webpack ([81dd5d3](https://github.com/LuanRT/YouTube.js/commit/81dd5d3288771132e7fb904b620e58277f639ccc))
## [9.0.0](https://github.com/LuanRT/YouTube.js/compare/v8.2.0...v9.0.0) (2024-01-25)### β BREAKING CHANGES***toDash:** Add support for generating manifests for Post Live DVR videos ([#580](https://github.com/LuanRT/YouTube.js/issues/580))
### Features***Channel:** Support getting about with PageHeader ([#581](https://github.com/LuanRT/YouTube.js/issues/581)) ([2e710dc](https://github.com/LuanRT/YouTube.js/commit/2e710dc9f7e206627f189df19be17009b270bc8b))
***Channel:** Support PageHeader being used on user channels ([#577](https://github.com/LuanRT/YouTube.js/issues/577)) ([6082b4a](https://github.com/LuanRT/YouTube.js/commit/6082b4a52ee07a622735e6e9128a0531a5ae3bfb))
***Format:** Add `max_dvr_duration_sec` and `target_duration_dec` ([#570](https://github.com/LuanRT/YouTube.js/issues/570)) ([586bb5f](https://github.com/LuanRT/YouTube.js/commit/586bb5f1398d68bfabfb9449f527e53c398c3767))
***parser:** Add `ImageBannerView` ([#583](https://github.com/LuanRT/YouTube.js/issues/583)) ([2073aa9](https://github.com/LuanRT/YouTube.js/commit/2073aa910a0e441a8ec1a9ba0434051ec0e2e6a9))
***toDash:** Add support for generating manifests for Post Live DVR videos ([#580](https://github.com/LuanRT/YouTube.js/issues/580)) ([6dd03e1](https://github.com/LuanRT/YouTube.js/commit/6dd03e1658036c2fba0696de81033b5e16abb379))
***VideoDetails:** Add `is_live_dvr_enabled`, `is_low_latency_live_stream` and `live_chunk_readahead` ([#569](https://github.com/LuanRT/YouTube.js/issues/569)) ([254f779](https://github.com/LuanRT/YouTube.js/commit/254f77944fcd398cc19cb62b82b0fdfbe6ed70ed))
***VideoInfo:** Add live stream `end_timestamp` ([#571](https://github.com/LuanRT/YouTube.js/issues/571)) ([562e6a2](https://github.com/LuanRT/YouTube.js/commit/562e6a20f06ef5302af427861355215630d91edc))
### Bug Fixes***DecoratedAvatarView:** Fix parsing and optional properties ([#584](https://github.com/LuanRT/YouTube.js/issues/584)) ([fed3512](https://github.com/LuanRT/YouTube.js/commit/fed3512461277b7fc41e26c770e2bd3d4a7d5eb5))
***PlayerCaptionTracklist:** Fix `captions_tracks[].kind` type ([#586](https://github.com/LuanRT/YouTube.js/issues/586)) ([7fbc37f](https://github.com/LuanRT/YouTube.js/commit/7fbc37f9d1c109e448085d5736326dce82ca2c9a))
***proto:** Fix visitor data base64url decoding ([#576](https://github.com/LuanRT/YouTube.js/issues/576)) ([3980f97](https://github.com/LuanRT/YouTube.js/commit/3980f97b8fca05f95cda1ab347db9402c55b8b3c))
***toDash:** Add missing transfer characteristics for h264 streams ([#573](https://github.com/LuanRT/YouTube.js/issues/573)) ([59f4cfb](https://github.com/LuanRT/YouTube.js/commit/59f4cfb4db6184d47f0a6634832055e9ce71f644))
## [8.2.0](https://github.com/LuanRT/YouTube.js/compare/v8.1.0...v8.2.0) (2024-01-08)### Features***OAuth:** Allow passing custom client identity ([#566](https://github.com/LuanRT/YouTube.js/issues/566)) ([7ffd0fc](https://github.com/LuanRT/YouTube.js/commit/7ffd0fc25edef99a938e7986b1c74af05b8f954e))
### Bug Fixes***Parser:** Add `SortFilterHeader` ([#563](https://github.com/LuanRT/YouTube.js/issues/563)) ([8f07e49](https://github.com/LuanRT/YouTube.js/commit/8f07e49512c59eb72debc80a9d9623ca62330858))
## [8.1.0](https://github.com/LuanRT/YouTube.js/compare/v8.0.0...v8.1.0) (2023-12-27)### Features***generator:** add support for arrays ([#556](https://github.com/LuanRT/YouTube.js/issues/556)) ([e4f2a00](https://github.com/LuanRT/YouTube.js/commit/e4f2a00c843fe453cc7904f79e35597cc6e2e619))
***generator:** Add support for generating view models ([#550](https://github.com/LuanRT/YouTube.js/issues/550)) ([f938c34](https://github.com/LuanRT/YouTube.js/commit/f938c34ee81186774096b3d24d06250211ce2851))
***MediaInfo:** Parse player config ([5c9c231](https://github.com/LuanRT/YouTube.js/commit/5c9c231cc2f17c49da03daa8262043b985320e9a))
***parser:** Support new like and dislike nodes ([#557](https://github.com/LuanRT/YouTube.js/issues/557)) ([fcd3044](https://github.com/LuanRT/YouTube.js/commit/fcd30449821763e9b5b57718dd02eff15d964d2b))
***Thumbnail:** Support `sources` in `Thumbnail.fromResponse` ([#552](https://github.com/LuanRT/YouTube.js/issues/552)) ([48a5d4e](https://github.com/LuanRT/YouTube.js/commit/48a5d4e7c37b76f8980f9b68e8815aef7a6d91ab))
***YouTube:** Add FEchannels feed ([#560](https://github.com/LuanRT/YouTube.js/issues/560)) ([14578ac](https://github.com/LuanRT/YouTube.js/commit/14578ac96af4b8bee652cce87d043173de964113))
### Bug Fixes***Format:** Extract correct audio language from captions ([#553](https://github.com/LuanRT/YouTube.js/issues/553)) ([5c83e99](https://github.com/LuanRT/YouTube.js/commit/5c83e999dfa00386d18369f42aa9aa10123ba578))
***generator:** Output Parser.parseItem() calls with one valid type, without the array ([#551](https://github.com/LuanRT/YouTube.js/issues/551)) ([bd487f8](https://github.com/LuanRT/YouTube.js/commit/bd487f8befe7f62022c61ff3aae7f487104e81eb))
***VideoInfo:** Restore `like`, `dislike` & `removeRating` methods ([9c503f4](https://github.com/LuanRT/YouTube.js/commit/9c503f4fa8a750558cedbeca974faf36e304147e))
## [8.0.0](https://github.com/LuanRT/YouTube.js/compare/v7.0.0...v8.0.0) (2023-12-01)### β BREAKING CHANGES***Library:** Add support for the new layout and remove profile & stats info
***Channel:** YouTube removed the "Channels" tab on channels, so this pull request removes the `getChannels()` method and `has_channels` getter from the `YT.Channel` class, as they are no longer useful. The featured channels are now shown on the channel home tab. To get them you can use the `channels` getter on the home tab of the channel. Please note that some channel owners might not have added that section to their home page yet, so you won't be able to get the featured channels for those channels. The home tab is the default tab that is returned when you call `InnerTube#getChannel()`, you can also access that tab by calling `getHome()` on a `YT.Channel` object.
### Features* add `FeedNudge` ([#533](https://github.com/LuanRT/YouTube.js/issues/533)) ([e021395](https://github.com/LuanRT/YouTube.js/commit/e02139532b2c07aaf72dd1bd8610f63b6780001d))
* add `VideoAttributeView` ([#531](https://github.com/LuanRT/YouTube.js/issues/531)) ([ff4ab16](https://github.com/LuanRT/YouTube.js/commit/ff4ab1680e110fc32e09d09215fd3e05dbde2c85))
* Add Shorts endpoint ([#512](https://github.com/LuanRT/YouTube.js/issues/512)) ([a32aa8c](https://github.com/LuanRT/YouTube.js/commit/a32aa8c633b6f3c3bb0695ad1878cbb313867346))
***Channel:** Support new about popup ([#537](https://github.com/LuanRT/YouTube.js/issues/537)) ([c66eb1f](https://github.com/LuanRT/YouTube.js/commit/c66eb1fecf0e66d9eca841be0ca56b39ad4466eb))
***parser:** Add `ChannelOwnerEmptyState` ([#541](https://github.com/LuanRT/YouTube.js/issues/541)) ([b60930a](https://github.com/LuanRT/YouTube.js/commit/b60930a0c1ce419dddb753846c84d4e46ddf04e1))
***Parser:** Add `ClipSection` ([#532](https://github.com/LuanRT/YouTube.js/issues/532)) ([9007b65](https://github.com/LuanRT/YouTube.js/commit/9007b652375e1ca3c3844bdf091fe3670f98dc2c))
***toDash:** Add `contentType` to audio and video adaption sets ([#539](https://github.com/LuanRT/YouTube.js/issues/539)) ([4806fc6](https://github.com/LuanRT/YouTube.js/commit/4806fc6c112cb3cf0584f7d253f3c4aeaffa9927))
* Use `overrides` instead of `--legacy-peer-deps` ([#529](https://github.com/LuanRT/YouTube.js/issues/529)) ([db7f620](https://github.com/LuanRT/YouTube.js/commit/db7f6209b2329bf18b8b35aababfdb9b750c3b0f))
### Bug Fixes***Channel:** Remove `getChannels()` and `has_channels`, as YouTube removed the tab ([#542](https://github.com/LuanRT/YouTube.js/issues/542)) ([6a5a579](https://github.com/LuanRT/YouTube.js/commit/6a5a579e3947109af0e7c2a318aef40edb8484f8))
***Library:** Add support for the new layout and remove profile & stats info ([4261915](https://github.com/LuanRT/YouTube.js/commit/4261915fd4aa84f7619a45d678910be0ae30e13e))
***StructuredDescriptionContent:** Add `ReelShelf` to list of possible nodes ([f74ed5a](https://github.com/LuanRT/YouTube.js/commit/f74ed5a1cf352a7b57fa84b9373f9ed9ba1911fc))
***VideoAttributeView:** Fix `image` and `overflow_menu_on_tap` props ([5ae15be](https://github.com/LuanRT/YouTube.js/commit/5ae15be63dee2a2393a1aa2a308ca5378140760a))
### Performance Improvements* Use named Parser import, to allow bundlers to create direct function references ([#535](https://github.com/LuanRT/YouTube.js/issues/535)) ([95ed602](https://github.com/LuanRT/YouTube.js/commit/95ed60207a1219f4891f28d2b2b90cf816f11831))
## [7.0.0](https://github.com/LuanRT/YouTube.js/compare/v6.4.1...v7.0.0) (2023-10-28)### β BREAKING CHANGES***music#getSearchSuggestions:** Return array of `SearchSuggestionsSection` instead of a single node
### Features***Kids:** Add `blockChannel` command to easily block channels ([#503](https://github.com/LuanRT/YouTube.js/issues/503)) ([9ab528e](https://github.com/LuanRT/YouTube.js/commit/9ab528ec823dcd527a97150009eed632c6d3eb6a))
***music#getSearchSuggestions:** Return array of `SearchSuggestionsSection` instead of a single node ([beaa28f](https://github.com/LuanRT/YouTube.js/commit/beaa28f4c68de8366caa84ce5a026bf9e12e1b9d))
***parser:** Add `PlayerOverflow` and `PlayerControlsOverlay` ([a45273f](https://github.com/LuanRT/YouTube.js/commit/a45273fec498df87eecd364ffb708c9f787793d5))
***UpdateViewerShipAction:** Add `original_view_count` and `unlabeled_view_count_value` ([#527](https://github.com/LuanRT/YouTube.js/issues/527)) ([bc97e07](https://github.com/LuanRT/YouTube.js/commit/bc97e07ac6d1cdc45194e214c6001cf92190e1d5))
### Bug Fixes***build:** Inline package.json import to avoid runtime erros ([#509](https://github.com/LuanRT/YouTube.js/issues/509)) ([4c0de19](https://github.com/LuanRT/YouTube.js/commit/4c0de199e85dd5cc8b3719920b24dec9613acaab))
## [6.4.1](https://github.com/LuanRT/YouTube.js/compare/v6.4.0...v6.4.1) (2023-10-02)### Bug Fixes***Feed:** Do not throw when multiple continuations are present ([8e372d5](https://github.com/LuanRT/YouTube.js/commit/8e372d5c67f148be288bb0485f2c70ec43fbecd0))
***Playlist:** Throw a more helpful error when parsing empty responses ([987f506](https://github.com/LuanRT/YouTube.js/commit/987f50604a0163f9a07091ce787995c6f6fddb75))
### Performance Improvements* Cache deciphered n-params by info response ([#505](https://github.com/LuanRT/YouTube.js/issues/505)) ([d2959b3](https://github.com/LuanRT/YouTube.js/commit/d2959b3a55a5081295da4754627913933bbaf1e7))
***generator:** Remove duplicate checks in `isMiscType` ([#506](https://github.com/LuanRT/YouTube.js/issues/506)) ([68df321](https://github.com/LuanRT/YouTube.js/commit/68df3218580db10c9a0932c93ff2ce487526ff1e))
## [6.4.0](https://github.com/LuanRT/YouTube.js/compare/v6.3.0...v6.4.0) (2023-09-10)### Features* Add support for retrieving transcripts ([#500](https://github.com/LuanRT/YouTube.js/issues/500)) ([f94ea6c](https://github.com/LuanRT/YouTube.js/commit/f94ea6cf917f63f30dd66514b22a4cf43b948f07))
***PlaylistManager:** add .setName() and .setDescription() functions for editing playlists ([#498](https://github.com/LuanRT/YouTube.js/issues/498)) ([86fb33e](https://github.com/LuanRT/YouTube.js/commit/86fb33ed03a127d9fd4caa695ca97642bffe61bd))
### Bug Fixes***BackstagePost:**`vote_button` type mismatch ([fba3fc9](https://github.com/LuanRT/YouTube.js/commit/fba3fc971454d66d80d4920fbd60889a221de381))
## [6.3.0](https://github.com/LuanRT/YouTube.js/compare/v6.2.0...v6.3.0) (2023-08-31)### Features***ChannelMetadata:** Add `music_artist_name` ([#497](https://github.com/LuanRT/YouTube.js/issues/497)) ([91de6e5](https://github.com/LuanRT/YouTube.js/commit/91de6e5c0e5b27e6d12ce5db2f500c5ff78b9830))
***Session:** Add on_behalf_of_user session option. ([#494](https://github.com/LuanRT/YouTube.js/issues/494)) ([8bc2aaa](https://github.com/LuanRT/YouTube.js/commit/8bc2aaa3587fcf79f69eedbc2bf422a4c6fa7eb1))
### Bug Fixes***CompactMovie:** Add missing import and remove unnecessary console.log ([#496](https://github.com/LuanRT/YouTube.js/issues/496)) ([c26972c](https://github.com/LuanRT/YouTube.js/commit/c26972c42a6368822ac254c00f1bbee5a1542486))
## [6.2.0](https://github.com/LuanRT/YouTube.js/compare/v6.1.0...v6.2.0) (2023-08-29)### Features***Session:** Add fallback for session data retrieval ([#490](https://github.com/LuanRT/YouTube.js/issues/490)) ([10c15bf](https://github.com/LuanRT/YouTube.js/commit/10c15bfb9f131a2acea2f26ff3328993d8d8f4aa))
### Bug Fixes***Format:** Fix `is_original` always being `true` ([#492](https://github.com/LuanRT/YouTube.js/issues/492)) ([0412fa0](https://github.com/LuanRT/YouTube.js/commit/0412fa05ff1f00960b398c2f18d5ce39ce0cb864))
## [6.1.0](https://github.com/LuanRT/YouTube.js/compare/v6.0.2...v6.1.0) (2023-08-27)### Features***parser:** Add `AlertWithButton` ([#486](https://github.com/LuanRT/YouTube.js/issues/486)) ([8b69587](https://github.com/LuanRT/YouTube.js/commit/8b6958778721ba274283f641779fb60bc6f42cd2))
***parser:** Add `ChannelHeaderLinksView` ([#484](https://github.com/LuanRT/YouTube.js/issues/484)) ([ed7be2a](https://github.com/LuanRT/YouTube.js/commit/ed7be2a675cf1ec663e743e90db6260c97546739))
***parser:** Add `CompactMovie` ([#487](https://github.com/LuanRT/YouTube.js/issues/487)) ([2eed172](https://github.com/LuanRT/YouTube.js/commit/2eed1726d5bde7648af09273cc14ab4a315cb23e))
## [6.0.2](https://github.com/LuanRT/YouTube.js/compare/v6.0.1...v6.0.2) (2023-08-24)### Bug Fixes* invalid set ids in dash manifest ([#480](https://github.com/LuanRT/YouTube.js/issues/480)) ([1c3ea2a](https://github.com/LuanRT/YouTube.js/commit/1c3ea2acd38652c6b40a0817a7836c672a776c4e))
## [6.0.1](https://github.com/LuanRT/YouTube.js/compare/v6.0.0...v6.0.1) (2023-08-22)### Bug Fixes***SearchSubMenu:** Groups not being parsed due to a typo ([90be877](https://github.com/LuanRT/YouTube.js/commit/90be877d28e0ef013056eaeaa4f2765c91addd61))
## [6.0.0](https://github.com/LuanRT/YouTube.js/compare/v5.8.0...v6.0.0) (2023-08-18)### β BREAKING CHANGES* replace unnecessary classes with pure functions ([#468](https://github.com/LuanRT/YouTube.js/issues/468))
### Features***MusicResponsiveListItem:** Detect non music tracks properly ([815e54b](https://github.com/LuanRT/YouTube.js/commit/815e54b854fcda3f5423231c8495ce1fb69d8237))
***parser:** add `MusicMultiRowListItem` ([494ee87](https://github.com/LuanRT/YouTube.js/commit/494ee8776af0839d3ee2cca3d2fd836680cfdb9e))
***Session:** Add `IOS` to `ClientType` enum ([22a38c0](https://github.com/LuanRT/YouTube.js/commit/22a38c0762499de74f0aeb3ef01332f893518b08))
***VideoInfo:** support iOS client ([#467](https://github.com/LuanRT/YouTube.js/issues/467)) ([46fe18b](https://github.com/LuanRT/YouTube.js/commit/46fe18b763e0c943b24ea10fdf25456ab9ade709))
### Bug Fixes***Format:** Extracting audio language from captions ([#470](https://github.com/LuanRT/YouTube.js/issues/470)) ([31d27b1](https://github.com/LuanRT/YouTube.js/commit/31d27b1bca489ee0053d2783f1a956609845a901))
***parser:** Allow any property in the `RawResponse` interface ([3bc53a8](https://github.com/LuanRT/YouTube.js/commit/3bc53a8c12e65b22f19a3e337641196b692a94db))
***parser:** Logger logging `classdata` as `[Object object]` ([bf1510b](https://github.com/LuanRT/YouTube.js/commit/bf1510b235e3ee7d13d51f092babd1105c3d6b9f))
***Playlist:** Only try extracting the subtitle for the first page ([#465](https://github.com/LuanRT/YouTube.js/issues/465)) ([e370116](https://github.com/LuanRT/YouTube.js/commit/e3701160928e9e959b88ca215c6b0a44c70ca6e6))
***toDash:** Format grouping into AdaptationSets ([#462](https://github.com/LuanRT/YouTube.js/issues/462)) ([1ff3e1a](https://github.com/LuanRT/YouTube.js/commit/1ff3e1a440389e71055d4b201c29021ca5b39254))
### Performance Improvements* Cleanup some unnecessary uses of `YTNode#key` and `Maybe` ([#463](https://github.com/LuanRT/YouTube.js/issues/463)) ([0dda97e](https://github.com/LuanRT/YouTube.js/commit/0dda97e0b03171de52d7f11a5abf78911e74cead))
### Code Refactoring* replace unnecessary classes with pure functions ([#468](https://github.com/LuanRT/YouTube.js/issues/468)) ([87ed396](https://github.com/LuanRT/YouTube.js/commit/87ed3960ffa1c738b6f3b5acaf423647db4d367e))
## [5.8.0](https://github.com/LuanRT/YouTube.js/compare/v5.7.1...v5.8.0) (2023-07-30)### Features***YouTube Playlist:** Add subtitle and fix author optionality ([#458](https://github.com/LuanRT/YouTube.js/issues/458)) ([0fa5a85](https://github.com/LuanRT/YouTube.js/commit/0fa5a859ae15a35266297079e3e34fd9f3a5ebf4))
## [5.7.1](https://github.com/LuanRT/YouTube.js/compare/v5.7.0...v5.7.1) (2023-07-25)### Bug Fixes***SearchHeader:** remove console.log ([d91695a](https://github.com/LuanRT/YouTube.js/commit/d91695a9ec6c55445cbeedba4ace4ac1e0a72eee))
## [5.7.0](https://github.com/LuanRT/YouTube.js/compare/v5.6.0...v5.7.0) (2023-07-24)### Features***parser:** Add `PageHeader` ([#450](https://github.com/LuanRT/YouTube.js/issues/450)) ([18cbc8c](https://github.com/LuanRT/YouTube.js/commit/18cbc8c038ddddffa1ba1519e56a8054b2996e42))
***parser:** Add `SearchHeader` ([6997982](https://github.com/LuanRT/YouTube.js/commit/6997982cf2db87edf4929e9a77e2690e7b630d3d)), closes [#452](https://github.com/LuanRT/YouTube.js/issues/452)## [5.6.0](https://github.com/LuanRT/YouTube.js/compare/v5.5.0...v5.6.0) (2023-07-18)### Features***parser:** Add `IncludingResultsFor` ([#447](https://github.com/LuanRT/YouTube.js/issues/447)) ([c477b82](https://github.com/LuanRT/YouTube.js/commit/c477b824c084552169062f72cde8890e77b31f59))
***toDash:** Add option to include thumbnails in the manifest ([#446](https://github.com/LuanRT/YouTube.js/issues/446)) ([1a03473](https://github.com/LuanRT/YouTube.js/commit/1a034733f6bb641e2d97df12de81ae3516c1f703))
## [5.5.0](https://github.com/LuanRT/YouTube.js/compare/v5.4.0...v5.5.0) (2023-07-16)### Features***Format:** Populate audio language from captions when available ([#445](https://github.com/LuanRT/YouTube.js/issues/445)) ([bdd98a3](https://github.com/LuanRT/YouTube.js/commit/bdd98a3b9be39c11942043a300a6ebce9a15efc6))
***parser:** Add `CommentsSimplebox` parser ([#442](https://github.com/LuanRT/YouTube.js/issues/442)) ([555d257](https://github.com/LuanRT/YouTube.js/commit/555d257459b76d7c0158e9c6b189a75a82b10faf))
***parser:** Add `HashtagTile` ([#440](https://github.com/LuanRT/YouTube.js/issues/440)) ([ae2557d](https://github.com/LuanRT/YouTube.js/commit/ae2557d15c9df09bb92e0dc6191670d72b36631a))
***parser:** add `MacroMarkersList` ([#444](https://github.com/LuanRT/YouTube.js/issues/444)) ([708c5f7](https://github.com/LuanRT/YouTube.js/commit/708c5f7394b4ea140836b9483848cb61b97ea1af))
***parser:** Add `ShowMiniplayerCommand` ([#443](https://github.com/LuanRT/YouTube.js/issues/443)) ([a9cdbf7](https://github.com/LuanRT/YouTube.js/commit/a9cdbf7010e7b9b9cfde5db645d51bdad51006c5))
### Bug Fixes***package:** Bump Jinter to fix bad export order ([#439](https://github.com/LuanRT/YouTube.js/issues/439)) ([2aef678](https://github.com/LuanRT/YouTube.js/commit/2aef67876ec19118b37d3cecd429ccf8239989e0))
***StructuredDescriptionContent:**`items` can also be a `HorizontalCardList` ([b50d1ef](https://github.com/LuanRT/YouTube.js/commit/b50d1ef67d81276864818de10c61b5a7980cbc1a))
## [5.4.0](https://github.com/LuanRT/YouTube.js/compare/v5.3.0...v5.4.0) (2023-07-14)### Features***Channel:** Add `getPodcasts()` method ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
***Channel:** Add `getReleases()` method ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
***parser:** Add `Quiz` ([#437](https://github.com/LuanRT/YouTube.js/issues/437)) ([cffa868](https://github.com/LuanRT/YouTube.js/commit/cffa868c6eeb579047653fac65da8e913fb3c621))
### Bug Fixes***Playlist:** Parse `PlaylistCustomThumbnail` for `thumbnail_renderer` ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
## [5.3.0](https://github.com/LuanRT/YouTube.js/compare/v5.2.1...v5.3.0) (2023-07-11)### Features***toDash:** Add color information ([#430](https://github.com/LuanRT/YouTube.js/issues/430)) ([3500e92](https://github.com/LuanRT/YouTube.js/commit/3500e926327d560b1db036bfe503c276b91922ac))
### Performance Improvements***Format:** Cleanup the xtags parsing ([#434](https://github.com/LuanRT/YouTube.js/issues/434)) ([1ca2083](https://github.com/LuanRT/YouTube.js/commit/1ca20836bf343c78461fab7ad3b71db2b96e65c3))
***toDash:** Hoist duplicates from Representation to AdaptationSet ([#431](https://github.com/LuanRT/YouTube.js/issues/431)) ([5f058e6](https://github.com/LuanRT/YouTube.js/commit/5f058e69ae8594491133f7f96287bea4137f7822))
## [5.2.1](https://github.com/LuanRT/YouTube.js/compare/v5.2.0...v5.2.1) (2023-07-04)### Bug Fixes* incorrect node parser implementations ([#428](https://github.com/LuanRT/YouTube.js/issues/428)) ([222dfce](https://github.com/LuanRT/YouTube.js/commit/222dfce6bbd13b2cd80ae11540cbc0edd9053fc5))
## [5.2.0](https://github.com/LuanRT/YouTube.js/compare/v5.1.0...v5.2.0) (2023-06-28)### Features***VideoDetails:** Add is_post_live_dvr property ([#411](https://github.com/LuanRT/YouTube.js/issues/411)) ([a11e596](https://github.com/LuanRT/YouTube.js/commit/a11e5962c6eb73b14623a9de1e6c8c2534146b1e))
***ytmusic:** Add support for YouTube Music mood filters ([#404](https://github.com/LuanRT/YouTube.js/issues/404)) ([77b39c7](https://github.com/LuanRT/YouTube.js/commit/77b39c79ee0768eb203b7d47ea81286d470c21f2))
### Bug Fixes***OAuth:** client identity matching ([#421](https://github.com/LuanRT/YouTube.js/issues/421)) ([07c1b3e](https://github.com/LuanRT/YouTube.js/commit/07c1b3e0e57cb1fa42e4772775bfd1437bbc731f))
***PlayerEndpoint:** Use different player params ([#419](https://github.com/LuanRT/YouTube.js/issues/419)) ([519be72](https://github.com/LuanRT/YouTube.js/commit/519be72445b7ff392b396e16bcb1dc05c7df8976))
***Playlist:** Add thumbnail_renderer on Playlist when response includes it ([#424](https://github.com/LuanRT/YouTube.js/issues/424)) ([4f9427d](https://github.com/LuanRT/YouTube.js/commit/4f9427d752e89faec8dd1c4fd7a9607dca998c7a))
***VideoInfo.ts:** reimplement `get music_tracks` ([#409](https://github.com/LuanRT/YouTube.js/issues/409)) ([e434bb2](https://github.com/LuanRT/YouTube.js/commit/e434bb2632fe2b20aab6f1e707a93ca76f9d5c91))
### Performance Improvements***Search:** Speed up results parsing ([#408](https://github.com/LuanRT/YouTube.js/issues/408)) ([1e07a18](https://github.com/LuanRT/YouTube.js/commit/1e07a184ffaff508ad5ba869cb5e7dc9f095f744))
***toDash:** Speed up format filtering ([#405](https://github.com/LuanRT/YouTube.js/issues/405)) ([5de7b24](https://github.com/LuanRT/YouTube.js/commit/5de7b24dc55fca3eb8fccc6fa30d3c2cd60b8184))
## [5.1.0](https://github.com/LuanRT/YouTube.js/compare/v5.0.4...v5.1.0) (2023-05-14)### Features***ReelItem:** Add accessibility label ([#401](https://github.com/LuanRT/YouTube.js/issues/401)) ([046103a](https://github.com/LuanRT/YouTube.js/commit/046103a4d8af09fafefab6e9f971184eeca75c2e))
***toDash:** Add audio track labels to the manifest when available ([#402](https://github.com/LuanRT/YouTube.js/issues/402)) ([84b4f1e](https://github.com/LuanRT/YouTube.js/commit/84b4f1efd111321e4f3e5a87844790c4ec9b0b52))
## [5.0.4](https://github.com/LuanRT/YouTube.js/compare/v5.0.3...v5.0.4) (2023-05-10)### Bug Fixes***bundles:** Use ESM tslib build for the browser bundles ([#397](https://github.com/LuanRT/YouTube.js/issues/397)) ([2673419](https://github.com/LuanRT/YouTube.js/commit/26734194ab0bc5a9f57e1c509d7646ce8903d0c6))
***Utils:** Circular dependency introduced in 38a83c3c2aa814150d1d9b8ed99fca915c1d67fe ([#400](https://github.com/LuanRT/YouTube.js/issues/400)) ([66b026b](https://github.com/LuanRT/YouTube.js/commit/66b026bf493d71a39e12825938fe54dc63aefd16))
***Utils:** Use instanceof in deepCompare instead of the constructor name ([#398](https://github.com/LuanRT/YouTube.js/issues/398)) ([38a83c3](https://github.com/LuanRT/YouTube.js/commit/38a83c3c2aa814150d1d9b8ed99fca915c1d67fe))
## [5.0.3](https://github.com/LuanRT/YouTube.js/compare/v5.0.2...v5.0.3) (2023-05-03)### Bug Fixes***Video:** typo causing node parsing to fail ([3b0498b](https://github.com/LuanRT/YouTube.js/commit/3b0498b68b5378e63283e792bd45571c0b919e0b))
## [5.0.2](https://github.com/LuanRT/YouTube.js/compare/v5.0.1...v5.0.2) (2023-04-30)### Bug Fixes***VideoInfo:** Use microformat view_count when videoDetails view_count is NaN ([#393](https://github.com/LuanRT/YouTube.js/issues/393)) ([7c0abfc](https://github.com/LuanRT/YouTube.js/commit/7c0abfccd78a6c291d898f898d73a4f16170e2a9))
## [5.0.1](https://github.com/LuanRT/YouTube.js/compare/v5.0.0...v5.0.1) (2023-04-30)### Bug Fixes***web:** slow downloads due to visitor data ([#391](https://github.com/LuanRT/YouTube.js/issues/391)) ([4f7ec07](https://github.com/LuanRT/YouTube.js/commit/4f7ec07c3f689219b07e8291877c23b6fbf45fb1))
## [5.0.0](https://github.com/LuanRT/YouTube.js/compare/v4.3.0...v5.0.0) (2023-04-29)### β BREAKING CHANGES* overhaul core classes and remove redundant code ([#388](https://github.com/LuanRT/YouTube.js/issues/388))
### Features***NavigationEndpoint:** parse `content` prop ([dd21f8c](https://github.com/LuanRT/YouTube.js/commit/dd21f8c75ae1d76180faab4f0ef9ee40920966e3))
### Bug Fixes***android:** workaround streaming URLs returning 403 ([#390](https://github.com/LuanRT/YouTube.js/issues/390)) ([75ea09d](https://github.com/LuanRT/YouTube.js/commit/75ea09dde86b1bdf13b197d6e02701899300a371))
### Code Refactoring* overhaul core classes and remove redundant code ([#388](https://github.com/LuanRT/YouTube.js/issues/388)) ([95e0294](https://github.com/LuanRT/YouTube.js/commit/95e0294eabfdb20bbee2a4bfb751fd101402c5d6))
## [4.3.0](https://github.com/LuanRT/YouTube.js/compare/v4.2.0...v4.3.0) (2023-04-13)### Features***GridVideo:** add `upcoming`, `upcoming_text`, `is_reminder_set` and `buttons` ([05de3ec](https://github.com/LuanRT/YouTube.js/commit/05de3ec97a1fea92543b5e5f84933b86a07ab830)), closes [#380](https://github.com/LuanRT/YouTube.js/issues/380)***MusicResponsiveListItem:** make flex/fixed cols public ([#382](https://github.com/LuanRT/YouTube.js/issues/382)) ([096bf36](https://github.com/LuanRT/YouTube.js/commit/096bf362c9bd46a510ecb0d01623c70841e26e26))
***ToggleMenuServiceItem:** parse default nav endpoint ([a056696](https://github.com/LuanRT/YouTube.js/commit/a0566969ba436f31ca3722d09442a0c6302235d7))
***ytmusic:** add taste builder nodes ([#383](https://github.com/LuanRT/YouTube.js/issues/383)) ([a9cad49](https://github.com/LuanRT/YouTube.js/commit/a9cad49333aa85c98bbb96e5f2d5b57d9beeb0c7))
## [4.2.0](https://github.com/LuanRT/YouTube.js/compare/v4.1.1...v4.2.0) (2023-04-09)### Features* Enable importHelpers in tsconfig to reduce output size ([#378](https://github.com/LuanRT/YouTube.js/issues/378)) ([0b301de](https://github.com/LuanRT/YouTube.js/commit/0b301de6a1e1352a64881c1751a84360922a77cd))
***parser:** ignore PrimetimePromo node ([ce9d9c5](https://github.com/LuanRT/YouTube.js/commit/ce9d9c56b4f45c0139d74edc95c295ecfd1ee4b1))
***PlaylistVideo:** Extract video_info and accessibility_label texts ([#376](https://github.com/LuanRT/YouTube.js/issues/376)) ([c9135e6](https://github.com/LuanRT/YouTube.js/commit/c9135e66d3c9c72b8d063eedcf3cc2123800946d))
## [4.1.1](https://github.com/LuanRT/YouTube.js/compare/v4.1.0...v4.1.1) (2023-03-29)### Bug Fixes***PlayerCaptionsTracklist:** parse props only if they exist in the node ([470d8d9](https://github.com/LuanRT/YouTube.js/commit/470d8d94063f0159cd005c9eb15fd1a4a175bea0)), closes [#372](https://github.com/LuanRT/YouTube.js/issues/372)***Search:** Return search results even if there are ads ([#373](https://github.com/LuanRT/YouTube.js/issues/373)) ([2c5907f](https://github.com/LuanRT/YouTube.js/commit/2c5907f80fd76452afe87d1722fe35a4f45a22e0))
## [4.1.0](https://github.com/LuanRT/YouTube.js/compare/v4.0.1...v4.1.0) (2023-03-24)### Features***Session:** allow setting a custom visitor data token ([#371](https://github.com/LuanRT/YouTube.js/issues/371)) ([13ebf0a](https://github.com/LuanRT/YouTube.js/commit/13ebf0a03973e7ba7b65e9f72c4333927e4254f6))
***ShowingResultsFor:** parse all props ([1d9587e](https://github.com/LuanRT/YouTube.js/commit/1d9587e8c1ee0b11bb0e444c3d1e98162e9e1059))
### Bug Fixes***http:** android tv http client missing `clientName` ([#370](https://github.com/LuanRT/YouTube.js/issues/370)) ([cb8fafe](https://github.com/LuanRT/YouTube.js/commit/cb8fafe94b8ab330ae58211a892923321d65d890))
***node:** Electron apps crashing ([#367](https://github.com/LuanRT/YouTube.js/issues/367)) ([e7eacd9](https://github.com/LuanRT/YouTube.js/commit/e7eacd974211c90e7fbddfbf8019388cda3dfa5a))
***parser:** Make Video.is_live work on channel pages ([#368](https://github.com/LuanRT/YouTube.js/issues/368)) ([bd35faa](https://github.com/LuanRT/YouTube.js/commit/bd35faa5978f0b822e98d019523be1303374ddc0))
***toDash:** Generate unique Representation ids ([#366](https://github.com/LuanRT/YouTube.js/issues/366)) ([a8b507e](https://github.com/LuanRT/YouTube.js/commit/a8b507ee65ccd8edc9aea2aef8a908fa272bb23c))
***Utils:** Properly parse timestamps with thousands separators ([#363](https://github.com/LuanRT/YouTube.js/issues/363)) ([1c72a41](https://github.com/LuanRT/YouTube.js/commit/1c72a41675e47f711bd61ebb898bca5527406a79))
## [4.0.1](https://github.com/LuanRT/YouTube.js/compare/v4.0.0...v4.0.1) (2023-03-16)### Bug Fixes***Channel:** type mismatch in `subscribe_button` prop ([573c864](https://github.com/LuanRT/YouTube.js/commit/573c8643aae16ec7b6be5b333619a5d8c91ca5c1))
## [4.0.0](https://github.com/LuanRT/YouTube.js/compare/v3.3.0...v4.0.0) (2023-03-15)### β BREAKING CHANGES***Parser:** general refactoring of parsers ([#344](https://github.com/LuanRT/YouTube.js/issues/344))
* The `toDash` functions are now asynchronous, they now return a `Promise<string>` instead of a `string`, as we need to fetch the first sequence of the OTF format streams while building the manifest.
### Features* Add support for OTF format streams ([3e4d41b](https://github.com/LuanRT/YouTube.js/commit/3e4d41bf06ba16232979977c705444f2032bcde6))
***parser:** add `GridMix` ([#356](https://github.com/LuanRT/YouTube.js/issues/356)) ([a8e7e64](https://github.com/LuanRT/YouTube.js/commit/a8e7e644ec6df3b3c98a313f0321da27b4ca456e))
***parser:** add `GridShow` and `ShowCustomThumbnail` ([8ef4b42](https://github.com/LuanRT/YouTube.js/commit/8ef4b42d444c4fbe5cd65a55c0e0e7aa31738755)), closes [#459](https://github.com/LuanRT/YouTube.js/issues/459)***parser:** add `MusicCardShelf` ([#358](https://github.com/LuanRT/YouTube.js/issues/358)) ([9b005d6](https://github.com/LuanRT/YouTube.js/commit/9b005d62d6590a2ddf6848dabfa33fce36e8df9c))
***parser:** Add `play_all_button` to `Shelf` ([#345](https://github.com/LuanRT/YouTube.js/issues/345)) ([427db5b](https://github.com/LuanRT/YouTube.js/commit/427db5bbc2bf3e8ec60371d504c2ab1cdae6e918))
***parser:** add `view_playlist` to `Playlist` ([#348](https://github.com/LuanRT/YouTube.js/issues/348)) ([9cb4530](https://github.com/LuanRT/YouTube.js/commit/9cb45302997771d909487b1ecba6f38655abef48))
***parser:** add InfoPanelContent and InfoPanelContainer nodes ([4784dfa](https://github.com/LuanRT/YouTube.js/commit/4784dfa563a4dbeaee31811824d5aa37a67f5557)), closes [#326](https://github.com/LuanRT/YouTube.js/issues/326)***Parser:** just-in-time YTNode generation ([#310](https://github.com/LuanRT/YouTube.js/issues/310)) ([2cee590](https://github.com/LuanRT/YouTube.js/commit/2cee59024c730c34aa06052849ed6fb3f862ef33))
***yt:** add support for movie items and trailers ([#349](https://github.com/LuanRT/YouTube.js/issues/349)) ([9f1c31d](https://github.com/LuanRT/YouTube.js/commit/9f1c31d7a09532e80a187b14acceff31c22579bf))
### Code Refactoring***Parser:** general refactoring of parsers ([#344](https://github.com/LuanRT/YouTube.js/issues/344)) ([b13bf6e](https://github.com/LuanRT/YouTube.js/commit/b13bf6e9926c19a1939e0f4b69cbd53d1af0f7c8))
## [3.3.0](https://github.com/LuanRT/YouTube.js/compare/v3.2.0...v3.3.0) (2023-03-09)### Features***parser:** add `ConversationBar` node ([b2253df](https://github.com/LuanRT/YouTube.js/commit/b2253df8022217dc486155d8cacbf22db04dd9c2))
***VideoInfo:** support get by endpoint + more info ([#342](https://github.com/LuanRT/YouTube.js/issues/342)) ([0d35fe0](https://github.com/LuanRT/YouTube.js/commit/0d35fe0ca5e87a877b76cbb6cf3c92843eac5a99))
### Bug Fixes***MultiMarkersPlayerBar:** avoid observing undefined objects ([f351770](https://github.com/LuanRT/YouTube.js/commit/f3517708ff34093a544c09d6f5f1ec806130d5cc))
***SharedPost:** import `Menu` node directly (oops) ([3e3dc35](https://github.com/LuanRT/YouTube.js/commit/3e3dc351bb44faec87616d9b922924d14a95f29f))
***ytmusic:** use static visitor id to avoid empty API responses ([f9754f5](https://github.com/LuanRT/YouTube.js/commit/f9754f5ac61d0f11b025f37f93783f971560268b)), closes [#279](https://github.com/LuanRT/YouTube.js/issues/279)## [3.2.0](https://github.com/LuanRT/YouTube.js/compare/v3.1.1...v3.2.0) (2023-03-08)### Features* Add support for descriptive audio tracks ([#338](https://github.com/LuanRT/YouTube.js/issues/338)) ([574b67a](https://github.com/LuanRT/YouTube.js/commit/574b67a1f707a32378586dd2fe7b2f36f4ab6ddb))
* export `FormatUtils`' types ([2d774e2](https://github.com/LuanRT/YouTube.js/commit/2d774e26aae79f3d1b115e0e85c148ae80985529))
***parser:** add `banner` to `PlaylistHeader` ([#337](https://github.com/LuanRT/YouTube.js/issues/337)) ([95033e7](https://github.com/LuanRT/YouTube.js/commit/95033e723ef912706e4d176de6b2760f017184e1))
***parser:** SharedPost ([#332](https://github.com/LuanRT/YouTube.js/issues/332)) ([ce53ac1](https://github.com/LuanRT/YouTube.js/commit/ce53ac18435cbcb20d6d4c4ab52fd156091e7592))
***VideoInfo:** add `game_info` and `category` ([#333](https://github.com/LuanRT/YouTube.js/issues/333)) ([214aa14](https://github.com/LuanRT/YouTube.js/commit/214aa147ce6306e37a6bf860a7bed5635db4797e))
***YouTube/Search:** add `SearchSubMenu` node ([#340](https://github.com/LuanRT/YouTube.js/issues/340)) ([a511608](https://github.com/LuanRT/YouTube.js/commit/a511608f18b37b0d9f2c7958ed5128330fabcfa0))
***yt:** add `getGuide()` ([#335](https://github.com/LuanRT/YouTube.js/issues/335)) ([2cc7b8b](https://github.com/LuanRT/YouTube.js/commit/2cc7b8bcd6938c7fb3af4f854a1d78b86d153873))
### Bug Fixes***SegmentedLikeDislikeButton:** like/dislike buttons can also be a simple `Button` ([9b2738f](https://github.com/LuanRT/YouTube.js/commit/9b2738f1285b278c3e83541857651be9a6248288))
***YouTube:** fix warnings when retrieving members-only content ([#341](https://github.com/LuanRT/YouTube.js/issues/341)) ([95f1d40](https://github.com/LuanRT/YouTube.js/commit/95f1d4077ff3775f36967dca786139a09e2830a2))
***ytmusic:** export search filters type ([cf8a33c](https://github.com/LuanRT/YouTube.js/commit/cf8a33c79f5432136b865d535fd0ecedc2393382))
## [3.1.1](https://github.com/LuanRT/YouTube.js/compare/v3.1.0...v3.1.1) (2023-03-01)### Bug Fixes***Channel:** getting community continuations ([#329](https://github.com/LuanRT/YouTube.js/issues/329)) ([4c7b8a3](https://github.com/LuanRT/YouTube.js/commit/4c7b8a34030effa26c4ea186d3e9509128aec31c))
## [3.1.0](https://github.com/LuanRT/YouTube.js/compare/v3.0.0...v3.1.0) (2023-02-26)### Features* Add upcoming and live info to playlist videos ([#317](https://github.com/LuanRT/YouTube.js/issues/317)) ([a0bfe16](https://github.com/LuanRT/YouTube.js/commit/a0bfe164279ec27b0c49c6b0c32222c1a92df5c3))
***VideoSecondaryInfo:** add support for attributed descriptions ([#325](https://github.com/LuanRT/YouTube.js/issues/325)) ([f933cb4](https://github.com/LuanRT/YouTube.js/commit/f933cb45bcb92c07b3bc063d63869a51cbff4eb0))
### Bug Fixes***parser:** export YTNodes individually so they can be used as types ([200632f](https://github.com/LuanRT/YouTube.js/commit/200632f374d5e0e105b600d579a2665a6fb36e38)), closes [#321](https://github.com/LuanRT/YouTube.js/issues/321)***PlayerMicroformat:** Make the embed field optional ([#320](https://github.com/LuanRT/YouTube.js/issues/320)) ([a0e6cef](https://github.com/LuanRT/YouTube.js/commit/a0e6cef00fb9e3f52593cec22704f7ddc1f7553e))
* send correct UA for Android requests ([f4e0f30](https://github.com/LuanRT/YouTube.js/commit/f4e0f30e6e94b347b28d67d9a86284ea2d23ee15)), closes [#322](https://github.com/LuanRT/YouTube.js/issues/322)## [3.0.0](https://github.com/LuanRT/YouTube.js/compare/v2.9.0...v3.0.0) (2023-02-17)### β BREAKING CHANGES* cleanup platform support ([#306](https://github.com/LuanRT/YouTube.js/issues/306))
### Features* add parser support for MultiImage community posts ([#298](https://github.com/LuanRT/YouTube.js/issues/298)) ([de61782](https://github.com/LuanRT/YouTube.js/commit/de61782f1a673cbe66ae9b410341e39b7501ba84))
* add support for hashtag feeds ([#312](https://github.com/LuanRT/YouTube.js/issues/312)) ([bf12740](https://github.com/LuanRT/YouTube.js/commit/bf12740333a82c26fe84e7c702c2fbb8859814fc))
* add support for YouTube Kids ([#291](https://github.com/LuanRT/YouTube.js/issues/291)) ([2bbefef](https://github.com/LuanRT/YouTube.js/commit/2bbefefbb7cb061f3e7b686158b7568c32f0da5d))
* allow checking whether a channel has optional tabs ([#296](https://github.com/LuanRT/YouTube.js/issues/296)) ([ceefbed](https://github.com/LuanRT/YouTube.js/commit/ceefbed98c70bb936e2d2df58c02834842acfdfc))
***Channel:** Add getters for all optional tabs ([#303](https://github.com/LuanRT/YouTube.js/issues/303)) ([b2900f4](https://github.com/LuanRT/YouTube.js/commit/b2900f48a7aa4c22635e1819ba9f636e81964f2c))
***Channel:** add support for sorting the playlist tab ([#295](https://github.com/LuanRT/YouTube.js/issues/295)) ([50ef712](https://github.com/LuanRT/YouTube.js/commit/50ef71284db41e5f94bb511892651d22a1d363a0))
* extract channel error alert ([0b99180](https://github.com/LuanRT/YouTube.js/commit/0b991800a5c67f0e702251982b52eb8531f36f19))
***FormatUtils:** support multiple audio tracks in the DASH manifest ([#308](https://github.com/LuanRT/YouTube.js/issues/308)) ([a69e43b](https://github.com/LuanRT/YouTube.js/commit/a69e43bf3ae02f2428c4aa86f647e3e5e0db5ba6))
* improve support for dubbed content ([#293](https://github.com/LuanRT/YouTube.js/issues/293)) ([d6c5a9b](https://github.com/LuanRT/YouTube.js/commit/d6c5a9b971444d0cd746aaf5310d3389793680ea))
* parse isLive in CompactVideo ([#294](https://github.com/LuanRT/YouTube.js/issues/294)) ([2acb7da](https://github.com/LuanRT/YouTube.js/commit/2acb7da0198bfeca6ff911cf95cf06a220fccaa5))
***parser:** add `ChannelAgeGate` node ([1cdf701](https://github.com/LuanRT/YouTube.js/commit/1cdf701c8403db6b681a26ecb1df2daa51add454))
***parser:** Text#toHTML ([#300](https://github.com/LuanRT/YouTube.js/issues/300)) ([e82e23d](https://github.com/LuanRT/YouTube.js/commit/e82e23dfbb24dff3ddf45754c7319d783990e254))
***ytkids:** add `getChannel()` ([#292](https://github.com/LuanRT/YouTube.js/issues/292)) ([0fc29f0](https://github.com/LuanRT/YouTube.js/commit/0fc29f0bbf965215146a6ae192494c74e6cefcbb))
### Bug Fixes* assign MetadataBadge's label ([#311](https://github.com/LuanRT/YouTube.js/issues/311)) ([e37cf62](https://github.com/LuanRT/YouTube.js/commit/e37cf627322f688fcef18d41345f77cbccd58829))
***ChannelAboutFullMetadata:** fix error when there are no primary links ([#299](https://github.com/LuanRT/YouTube.js/issues/299)) ([f62c66d](https://github.com/LuanRT/YouTube.js/commit/f62c66db396ba7d2f93007414101112b49d8375f))
***TopicChannelDetails:** avatar and subtitle parsing ([#302](https://github.com/LuanRT/YouTube.js/issues/302)) ([d612590](https://github.com/LuanRT/YouTube.js/commit/d612590530f5fe590fee969810b1dd44c37f0457))
***VideoInfo:** Gracefully handle missing watch next continuation ([#288](https://github.com/LuanRT/YouTube.js/issues/288)) ([13ad377](https://github.com/LuanRT/YouTube.js/commit/13ad3774c9783ed2a9f286aeee88110bd43b3a73))
### Code Refactoring* cleanup platform support ([#306](https://github.com/LuanRT/YouTube.js/issues/306)) ([2ccbe2c](https://github.com/LuanRT/YouTube.js/commit/2ccbe2ce6260ace3bfac8b4b391e583fbcc4e286))
LuanRT/YouTube.js/blob/main/COLLABORATORS.md:
# Collaborators
This page lists the collaborators who have contributed to the development and success of the project.
## [LuanRT](https://github.com/LuanRT)[](https://github.com/sponsors/LuanRT)
Owner and maintainer.
## [Wykerd](https://github.com/wykerd/)
Initial parser implementation, several bug fixes, major refactorings and general maintenance.
## [MasterOfBob777](https://github.com/MasterOfBob777)
Bug fixes and TypeScript support.
## [patrickkfkan](https://github.com/patrickkfkan)
Major refactorings, improved YouTube Music support, and bug fixes.
## [Absidue](https://github.com/absidue)
Several bug fixes, new features & improved MPD support.
LuanRT/YouTube.js/blob/main/CONTRIBUTING.md:
Welcome to YouTube.js! We're thrilled to have you interested in contributing to our project. As a community-driven project, we believe in the power of collaboration and look forward to working with you. To get started, please follow our guidelines:
## Issues### Creating a new issue
Before creating a new issue, we recommend searching for similar or related issues to avoid duplication efforts. However, if you can't find one, you're more than welcome to create a new issue using a relevant issue form. Please make sure to describe the issue as clearly and concisely as possible.
### Solving an issue
If you want to lend a hand by solving an issue, it's always good to browse existing issues to find one that grabs your attention. You can narrow down the search using tags as filters. If you find an issue you'd like to help with, please feel free to open a Pull Request with a fix. We appreciate documentation updates and grammar fixes too!
## Making Changes1. Fork the repository on GitHub.
2. Ensure that you have the latest Node.js v16 version installed.
3. Create a working branch and start making your changes and improvements!
### Committing updates
When you're done with the changes, make sure to commit them. Don't forget to write a clear, descriptive commit message. We recommend following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
### Creating a Pull Request
Once you're happy with your updates, create a pull request on GitHub. This is the most efficient way to get your contribution reviewed and eventually merged into our codebase.
- Use the pull request template to fill in the necessary details.
- If you're solving an issue, link the pull request to that issue.
- Enable the checkbox to allow maintainers to edit the branch and update it for merging.
- Changes may be required before we can merge your changes, and we'll let you know what needs to be done.
### Testing, Linting, and Building
We have some automated processes set up for testing, linting, and building. Please run the following commands to test, lint, and build your code before submitting it:
Testing:
```sh
npm run test
Linting:
npm run lint
Building:
# Build all
npm run build
# Protobuf
npm run build:proto
# Parser map
npm run build:parser-map
# Deno
npm run build:deno
# ES Module
npm run build:esm
# Node
npm run bundle:node
# Browser
npm run bundle:browser
npm run bundle:browser:prod
We appreciate your efforts and contributions to YouTube.js! Together, we can make this project even better.
LuanRT/YouTube.js/blob/main/README.md:
```md
<!-- BADGE LINKS -->
[npm]: https://www.npmjs.com/package/youtubei.js
[versions]: https://www.npmjs.com/package/youtubei.js?activeTab=versions
[codefactor]: https://www.codefactor.io/repository/github/luanrt/youtube.js
[actions]: https://github.com/LuanRT/YouTube.js/actions
[collaborators]: https://github.com/LuanRT/YouTube.js/blob/main/COLLABORATORS.md
<!-- OTHER LINKS -->
[project]: https://github.com/LuanRT/YouTube.js
[twitter]: https://twitter.com/thesciencephile
[discord]: https://discord.gg/syDu7Yks54
<div align="center">
<br/>
<p>
<a href="https://github.com/LuanRT/YouTube.js"><img src="https://luanrt.github.io/assets/img/ytjs.svg" title="youtube.js" alt="YouTube.js' Github Page" width="200" /></a>
</p>
<p align="center">A full-featured wrapper around the InnerTube API</p>
[][discord]
[][actions]
[][versions]
[][npm]
[][codefactor]
<h5>
Sponsored by <a href="https://serpapi.com"><img src="https://luanrt.github.io/assets/img/serpapi.svg" alt="SerpApi - API to get search engine results with ease." height=35 valign="middle"></a>
</h5>
<br>
</div>
InnerTube is an API used by all YouTube clients. It was created to simplify the deployment of new features and experiments across the platform [^1]. This library manages all low-level communication with InnerTube, providing a simple and efficient way to interact with YouTube programmatically. Its design aims to closely emulate an actual client, including the parsing of API responses.
If you have any questions or need help, feel free to reach out to us on our [Discord server][discord] or open an issue [here](https://github.com/LuanRT/YouTube.js/issues).
### Table of Contents
<ol>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
<li>
<a href="#usage">Usage</a>
<ul>
<li><a href="#browser-usage">Browser Usage</a></li>
<li><a href="#caching">Caching</a></li>
<li><a href="#api">API</a></li>
<li><a href="#extending-the-library">Extending the library</a></li>
</ul>
</li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#disclaimer">Disclaimer</a></li>
<li><a href="#license">License</a></li>
</ol>
### Prerequisites
YouTube.js runs on Node.js, Deno, and modern browsers.
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](https://github.com/nodejs/undici)'s fetch implementation, which requires Node.js 16.8+. If you need to use an older version, you may provide your own fetch implementation. 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` return 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) are required.
### Installation
```bash
# NPM
npm install youtubei.js@latest
# Yarn
yarn add youtubei.js@latest
# Git (edge version)
npm install github:LuanRT/YouTube.js
When using Deno, you can import YouTube.js directly from deno.land:
The account index to use. This is useful if you have multiple accounts logged in. NOTE: Only works if you are signed in with cookies.
0
visitor_data
string
Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in. A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's /visitor_id endpoint.
undefined
po_token
string
Proof of Origin Token. This is an attestation token generated by BotGuard/DroidGuard. It is used to confirm that the request is coming from a genuine device. To obtain a valid token, please refer to Invidious' Trusted Session Generator.
undefined
retrieve_player
boolean
Specifies whether to retrieve the JS player. Disabling this will make session creation faster. NOTE: Deciphering formats is not possible without the JS player.
true
enable_safety_mode
boolean
Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content.
false
generate_session_locally
boolean
Specifies whether to generate the session data locally or retrieve it from YouTube. This can be useful if you need more performance. NOTE: If you are using the cache option and a session has already been generated, this will be ignored. If you want to force a new session to be generated, you must clear the cache or disable session caching.
false
enable_session_cache
boolean
Specifies whether to cache the session data.
true
device_category
DeviceCategory
Platform to use for the session.
DESKTOP
client_type
ClientType
InnerTube client type. It is not recommended to change this unless you know what you are doing.
WEB
timezone
string
The time zone.
*
cache
ICache
Used to cache algorithms, session data, and OAuth2 tokens.
undefined
cookie
string
YouTube cookies.
undefined
fetch
FetchFunction
Fetch function to use.
fetch
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 at examples/browser/proxy/deno.ts.
You may provide your own fetch implementation to be used by YouTube.js, which we will use to modify and send the requests through a proxy. See examples/browser/web for a simple example using Vite.
// Multiple exports are available for the web.// Unbundled ESM versionimport{Innertube}from'youtubei.js/web';// Bundled ESM version// import { Innertube } from 'youtubei.js/web.bundle';// Production Bundled ESM version// import { Innertube } from 'youtubei.js/web.bundle.min';awaitInnertube.create({fetch: async(input: RequestInfo|URL,init?: RequestInit)=>{// Modify the request// and send it to the proxy// fetch the URLreturnfetch(request,init);}});
Streaming
YouTube.js supports streaming of videos in the browser by converting YouTube's streaming data into an MPEG-DASH manifest.
import{Innertube}from'youtubei.js/web';importdashjsfrom'dashjs';constyoutube=awaitInnertube.create({/* setup - see above */});// Get the video infoconstvideoInfo=awaityoutube.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 manifestconstmanifest=awaitvideoInfo.toDash(url=>{// modify the url// and return itreturnurl;});consturi="data:application/dash+xml;charset=utf-8;base64,"+btoa(manifest);constvideoElement=document.getElementById('video_player');constplayer=dashjs.MediaPlayer().create();player.initialize(videoElement,uri,true);
You may provide your own fetch implementation to be used by YouTube.js. This can be useful in some cases to modify the requests before they are sent and transform the responses before they are returned (eg. for proxies).
// provide a fetch implementationconstyt=awaitInnertube.create({fetch: async(input: RequestInfo|URL,init?: RequestInit)=>{// make the request with your own fetch implementation// and return the responsereturnnewResponse(/* ... */);}});
Caching
Caching the transformed player instance can greatly improve the performance. Our UniversalCache implementation uses different caching methods depending on the environment.
In Node.js, we use the node:fs module, Deno.writeFile() in Deno, and indexedDB in browsers.
By default, the cache stores data in the operating system's temporary directory (or indexedDB in browsers). You can make this cache persistent by specifying the path to the cache directory, which will be created if it doesn't exist.
import{Innertube,UniversalCache}from'youtubei.js';// Create a cache that stores files in the OS temp directory (or indexedDB in browsers) by default.constyt=awaitInnertube.create({cache: newUniversalCache(false)});// You may want to create a persistent cache instead (on Node and Deno).constyt=awaitInnertube.create({cache: newUniversalCache(// Enables persistent cachingtrue,// Path to the cache directory. The directory will be created if it doesn't exist'./.cache')});
If string, the id of the video. If NavigationEndpoint, the endpoint of watchable elements such as Video, Mix and Playlist. To clarify, valid endpoints have payloads containing at least videoId and optionally playlistId, params and index.
client?
InnerTubeClient
InnerTube client to use.
Methods & Getters
<info>#like()
Likes the video.
<info>#dislike()
Dislikes the video.
<info>#removeRating()
Removes like/dislike.
<info>#getLiveChat()
Returns a LiveChat instance.
<info>#getTrailerInfo()
Returns trailer info in a new VideoInfo instance, or null if none. Typically available for non-purchased movies or films.
Applies given filter and returns a new HashtagFeed instance.
<hashtag>#getContinuation()
Retrieves next batch of contents.
getStreamingData(video_id, options)
Returns deciphered streaming data.
Note
This method will be deprecated in the future. We recommend retrieving streaming data from a VideoInfo or TrackInfo object instead if you want to select formats manually. Please refer to the following example:
YouTube.js is modular and easy to extend. Most of the methods, classes, and utilities used internally are exposed and can be used to implement your own extensions without having to modify the library's source code.
For example, let's say we want to implement a method to retrieve video info. We can do that by using an instance of the Actions class:
import{Innertube}from'youtubei.js';(async()=>{constyt=awaitInnertube.create();asyncfunctiongetVideoInfo(videoId: string){constvideoInfo=awaityt.actions.execute('/player',{// You can add any additional payloads here, and they'll merge with the default payload sent to InnerTube.
videoId,client: 'YTMUSIC',// InnerTube client to use.parse: true// tells YouTube.js to parse the response (not sent to InnerTube).});returnvideoInfo;}constvideoInfo=awaitgetVideoInfo('jLTOuvBTLxA');console.info(videoInfo);})();
Alternatively, suppose we locate a NavigationEndpoint in a parsed response and want to see what happens when we call it:
import{Innertube,YTNodes}from'youtubei.js';(async()=>{constyt=awaitInnertube.create();constartist=awaityt.music.getArtist('UC52ZqHVQz5OoGhvbWiRal6g');constalbums=artist.sections[1].as(YTNodes.MusicCarouselShelf);// Let's imagine that we wish to click on the βMoreβ button:constbutton=albums.as(YTNodes.MusicCarouselShelf).header?.more_content;if(button){// Having ensured that it exists, we can then call its navigation endpoint using the following code:constpage=awaitbutton.endpoint.call(yt.actions,{parse: true});console.info(page);}})();
Parser
YouTube.js' parser enables you to parse InnerTube responses and convert their nodes into strongly-typed objects that are simple to manipulate. Additionally, it provides numerous utility methods that make working with InnerTube a breeze.
Here's an example of its usage:
// See ./examples/parserimport{Parser,YTNodes}from'youtubei.js';import{readFileSync}from'fs';// YouTube Music's artist page responseconstdata=readFileSync('./artist.json').toString();constpage=Parser.parseResponse(JSON.parse(data));constheader=page.header?.item().as(YTNodes.MusicImmersiveHeader,YTNodes.MusicVisualHeader);console.info('Header:',header);// The parser uses a proxy object to add type safety and utility methods for working with InnerTube's data arrays:consttab=page.contents?.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);if(!tab)thrownewError('Target tab not found');if(!tab.content)thrownewError('Target tab appears to be empty');constsections=tab.content?.as(YTNodes.SectionList).contents.as(YTNodes.MusicCarouselShelf,YTNodes.MusicDescriptionShelf,YTNodes.MusicShelf);console.info('Sections:',sections);
We welcome all contributions, issues and feature requests, whether small or large. If you want to contribute, feel free to check out our issues page and our guidelines.
We are immensely grateful to all the wonderful people who have contributed to this project. A special shoutout to all our contributors! π
This project is not affiliated with, endorsed, or sponsored by YouTube or any of its affiliates or subsidiaries. All trademarks, logos, and brand names used in this project are the property of their respective owners and are used solely to describe the services provided.
As such, any usage of trademarks to refer to such services is considered nominative use. If you have any questions or concerns, please contact me directly via email.
importglobfrom'glob';importpathfrom'path';importfsfrom'fs';importurlfrom'url';constimport_list=[];constmisc_imports=[];const__dirname=path.dirname(url.fileURLToPath(import.meta.url));glob.sync('../src/parser/classes/**/*.{js,ts}',{cwd: __dirname}).forEach((file)=>{// Trim pathconstis_misc=file.includes('/misc/');file=file.replace('../src/parser/classes/','').replace('.js','').replace('.ts','');constimport_name=file.split('/').pop();if(is_misc){constclass_name=file.split('/').pop().replace('.js','').replace('.ts','');misc_imports.push(`export { default as ${class_name} } from './classes/${file}.js';`);}else{import_list.push(`export { default as ${import_name} } from './classes/${file}.js';`);}});fs.writeFileSync(path.resolve(__dirname,'../src/parser/nodes.ts'),`// This file was auto generated, do not edit.// See ./scripts/build-parser-map.js${import_list.join('\n')}`);fs.writeFileSync(path.resolve(__dirname,'../src/parser/misc.ts'),`// This file was auto generated, do not edit.// See ./scripts/build-parser-map.js${misc_imports.join('\n')}`);
import{fetch}from'undici';import{gunzip}from'zlib';import{dirname,resolve}from'path';import{fileURLToPath}from'url';import{writeFile}from'fs/promises';const__dirname=dirname(fileURLToPath(import.meta.url));constbuf=await(awaitfetch('https://github.com/intoli/user-agents/blob/master/src/user-agents.json.gz?raw=true')).arrayBuffer();constbytes=newUint8Array(buf);// Only get desktop and mobile agentsconstallowed_agents=newSet(['desktop','mobile']);constdecompressed=awaitnewPromise((resolve,reject)=>{gunzip(bytes,(err,result)=>{if(err){reject(err);}else{resolve(result.buffer);}});});constcontents=newTextDecoder().decode(decompressed);constagents=JSON.parse(contents);if(!Array.isArray(agents)){thrownewError('Invalid user-agents.json');}constagentsByDevice=agents.reduce((acc,agent)=>{constdevice=agent.deviceCategory;if(!allowed_agents.has(device))returnacc;if(!acc[device]){acc[device]=[];}// We dont want to massive of a list of agents for each deviceif(acc[device].length<=25)acc[device].push(agent.userAgent);returnacc;},{});awaitwriteFile(resolve(__dirname,'..','src','utils','user-agents.ts'),`/* eslint-disable */\n/* Generated file do not edit */\nexport default ${JSON.stringify(agentsByDevice,null,2)} as { desktop: string[], mobile: string[] };`);
LuanRT/YouTube.js/blob/main/docs/API/account.md:
# Account
YouTube account manager.
## API* Account
*[.channel](#channel)*[.getInfo()](#getinfo)*[.getTimeWatched()](#gettimewatched)*[.getSettings()](#getsettings)*[.getAnalytics](#getanalytics)
<aname="channel"></a>
### channel
Channel settings.
**Returns:**`object`
<details>
<summary>Methods & Getters</summary>
<p>
-`<channel>#editName(new_name)`- Edits the name of the channel.
-`<channel>#editDescription(new_description)`- Edits channel description.
-`<channel>#getBasicAnalytics()`- Alias for [`Account#getAnalytics()`](#getanalytics) β returns basic channel analytics.
</p>
</details>
<aname="getinfo"></a>
### getInfo()
Retrieves account information.
**Returns:**`Promise.<AccountInfo>`
<details>
<summary>Methods & Getters</summary>
<p>
-`<accountinfo>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="gettimewatched"></a>
### getTimeWatched()
Retrieves time watched statistics.
**Returns:**`Promise.<TimeWatched>`
<details>
<summary>Methods & Getters</summary>
<p>
-`<timewatched>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getsettings"></a>
### getSettings()
Retrieves YouTube settings.
**Returns:**`Promise.<Settings>`
<details>
<summary>Methods & Getters</summary>
<p>
-`<settings>#selectSidebarItem(name)`- Selects an item from the sidebar menu. Use `settings#sidebar_items` to see available items.
-`<settings>#getSettingOption(name)`- Finds a setting by name and returns it. Use `settings#setting_options` to see available options.
-`<settings>#setting_options`- Returns settings available in the page.
-`<settings>#sidebar_items`- Returns options available in the sidebar menu.
-`<settings>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getanalytics"></a>
### getAnalytics()
Retrieves basic channel analytics.
**Returns:**`Promise.<Analytics>`
<details>
<summary>Methods & Getters</summary>
<p>
-`<analytics>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
LuanRT/YouTube.js/blob/main/docs/API/feed.md:
# Feed
Represents a YouTube feed. This class provides a set of utility methods for parsing and interacting with feeds.
## API* Feed
*[.videos](#videos)*[.posts](#posts)*[.channels](#channels)*[.playlists](#playlists)*[.shelves](#shelves)*[.memo](#memo)*[.page_contents](#page_contents)*[.secondary_contents](#secondary_contents)*[.page](#page)*[.has_continuation](#has_continuation)*[.getContinuationData()](#getcontinuationdata)*[.getContinuation()](#getcontinuation)*[.getShelf(title)](#getshelf)
<aname="videos"></a>
### videos
Returns all videos in the feed.
**Returns:**`ObservedArray<Video | GridVideo | ReelItem | CompactVideo | PlaylistVideo | PlaylistPanelVideo | WatchCardCompactVideo>`
<aname="posts"></a>
### posts
Returns all posts in the feed.
**Returns:**`ObservedArray<Post | BackstagePost>`
<aname="channels"></a>
### channels
Returns all channels in the feed.
**Returns:**`ObservedArray<Channel | GridChannel>`
<aname="playlists"></a>
### playlists
Returns all playlists in the feed.
**Returns:**`ObservedArray<Playlist | GridPlaylist>`
<aname="shelves"></a>
### shelves
Returns all shelves in the feed.
**Returns:**`ObservedArray<Shelf | RichShelf | ReelShelf>`
<aname="memo"></a>
### memo
Returns the memoized feed contents.
**Returns:**`Memo`
<aname="page_contents"></a>
### page_contents
Returns the page contents.
**Returns:**`SectionList | MusicQueue | RichGrid | ReloadContinuationItemsCommand`
<aname="secondary_contents"></a>
### secondary_contents
Returns the secondary contents node.
**Returns:**`SuperParsedResult<YTNode> | undefined `
<aname="page"></a>
### page
Returns the original InnerTube response, parsed and sanitized.
**Returns:**`T extends IParsedResponse = IParsedResponse`
<aname="has_continuation"></a>
### has_continuation
Returns whether the feed has a continuation.
**Returns:**`boolean`
<aname="getcontinuationdata"></a>
### getContinuationData()
Returns the continuation data.
**Returns:**`Promise<T | undefined>`
<aname="getcontinuation"></a>
### getContinuation()
Retrieves the feed's continuation.
**Returns:**`Promise<Feed<T>>`
<aname="getshelf"></a>
### getShelf(title)
Gets a shelf by its title.
**Returns:**`Shelf | RichShelf | ReelShelf | undefined`| Param | Type | Description || --- | --- | --- || title |`string`| The title of the shelf to get |
# FilterableFeed
Represents a feed that can be filtered.
> **Note**> This class extends the [Feed](feed.md) class.## API* FilterableFeed
*[.filter_chips](#filter_chips)*[.filters](#filters)*[.getFilteredFeed(filter: string | ChipCloudChip)](#getfilteredfeed)
<aname="filter_chips"></a>
### filter_chips
Returns the feed's filter chips.
**Returns:**`ObservedArray<ChipCloudChip>`
<aname="filters"></a>
### filters
Returns the feed's filter chips as an array of strings.
**Returns:**`string[]`
<aname="getfilteredfeed"></a>
### getFilteredFeed(filter: string | ChipCloudChip)
Returns a new [Feed](feed.md) with the given filter applied.
**Returns:**`Promise<Feed<T>>`| Param | Type | Description || --- | --- | --- || filter |`string`\|`ChipCloudChip`| The filter to apply |
# InteractionManager
Handles direct interactions.
## API* InteractionManager
*[.like(video_id)](#like)*[.dislike(video_id)](#dislike)*[.removeRating(video_id)](#removerating)*[.subscribe(video_id)](#subscribe)*[.unsubscribe(video_id)](#unsubscribe)*[.comment(video_id, text)](#comment)*[.translate(text, target_language, args?)](#translate)*[.setNotificationPreferences(channel_id, type)](#setnotificationpreferences)
<aname="like"></a>
### like(video_id)
Likes given video.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id |
<aname="dislike"></a>
### dislike(video_id)
Dislikes given video.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id |
<aname="removerating"></a>
### removeRating(video_id)
Remover like/dislike.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id |
<aname="subscribe"></a>
### subscribe(channel_id)
Subscribes to given channel.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || channel_id |`string`| Channel id |
<aname="unsubscribe"></a>
### unsubscribe(channel_id)
Unsubscribes from given channel.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || channel_id |`string`| Channel id |
<aname="comment"></a>
### comment(video_id, text)
Posts a comment on given video.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id || text |`string`| Comment content |
<aname="translate"></a>
### translate(text, target_language, args?)
Translates given text using YouTube's comment translation feature.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || text |`string`| Text to be translated || target_language |`string`| ISO language code || args? |`object`| Additional arguments |
<aname="setnotificationpreferences"></a>
### setNotificationPreferences(channel_id, type)
Changes notification preferences for a given channel.
Only works with channels you are subscribed to.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || channel_id |`string`| Channel id || type |`string`|`PERSONALIZED`, `ALL` or `NONE`|
LuanRT/YouTube.js/blob/main/docs/API/kids.md:
# YouTube Kids
YouTube Kids is a modified version of the YouTube app, with a simplified interface and curated content. This class allows you to interact with its API.
## API* Kids
*[.search(query)](#search)*[.getInfo(video_id)](#getinfo)*[.getChannel(channel_id)](#getchannel)*[.getHomeFeed()](#gethomefeed)*[.blockChannel(channel_id)](#blockchannel)
<aname="search"></a>
### search(query)
Searches the given query on YouTube Kids.
**Returns:**`Promise.<Search>`| Param | Type | Description || --- | --- | --- || query |`string`| The query to search |
<details>
<summary>Methods & Getters</summary>
<p>
-`<search>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getinfo"></a>
### getInfo(video_id)
Retrieves video info.
**Returns:**`Promise.<VideoInfo>`| Param | Type | Description || --- | --- | --- || video_id |`string`| The video id |
<details>
<summary>Methods & Getters</summary>
<p>
-`<info>#toDash(url_transformer?, format_filter?)`- Generates a DASH manifest from the streaming data.
-`<info>#chooseFormat(options)`- Selects the format that best matches the given options. This method is used internally by `#download`.
-`<info>#download(options?)`- Downloads the video.
-`<info>#addToWatchHistory()`- Adds the video to the watch history.
-`<info>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getchannel"></a>
### getChannel(channel_id)
Retrieves channel info.
**Returns:**`Promise.<Channel>`| Param | Type | Description || --- | --- | --- || channel_id |`string`| The channel id |
<details>
<summary>Methods & Getters</summary>
<p>
-`<channel>#getContinuation()`- Retrieves next batch of videos.
-`<channel>#has_continuation`- Returns whether there are more videos to retrieve.
-`<channel>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="gethomefeed"></a>
### getHomeFeed()
Retrieves the home feed.
**Returns:**`Promise.<HomeFeed>`
<details>
<summary>Methods & Getters</summary>
<p>
-`<feed>#selectCategoryTab(tab: string | KidsCategoryTab)`- Selects the given category tab.
-`<feed>#categories`- Returns available categories.
-`<feed>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</details>
<aname="blockChannel"></a>
### blockChannel(channel_id)
Retrieves the list of supervised accounts that the signed-in user has access to and blocks the given channel for each of them.
**Returns:**`Promise.<ApiResponse[]>`| Param | Type | Description || --- | --- | --- || channel_id |`string`| Channel id |
LuanRT/YouTube.js/blob/main/docs/API/music.md:
# YouTube Music
YouTube Music is a music streaming service developed by YouTube, a subsidiary of Google. It provides a tailored interface for the service oriented towards music streaming, with a greater emphasis on browsing and discovery compared to its main service. This class allows you to interact with its API.
## API* Music
*[.getInfo(target)](#getinfo)*[.search(query, filters?)](#search)*[.getHomeFeed()](#gethomefeed)*[.getExplore()](#getexplore)*[.getLibrary()](#getlibrary)*[.getArtist(artist_id)](#getartist)*[.getAlbum(album_id)](#getalbum)*[.getPlaylist(playlist_id)](#getplaylist)*[.getLyrics(video_id)](#getlyrics)*[.getUpNext(video_id, automix?)](#getupnext)*[.getRelated(video_id)](#getrelated)*[.getRecap()](#getrecap)*[.getSearchSuggestions(query)](#getsearchsuggestions)
<aname="getinfo"></a>
### getInfo(target)
Retrieves track info.
**Returns:**`Promise.<TrackInfo>`| Param | Type | Description || --- | --- | --- || target |`string` or `MusicTwoRowItem`| video id or list item |
<details>
<summary>Methods & Getters</summary>
<p>
-`<info>#getTab(title)`- Retrieves contents of the given tab.
-`<info>#getUpNext(automix?)`- Retrieves up next.
-`<info>#getRelated()`- Retrieves related content.
-`<info>#getLyrics()`- Retrieves song lyrics.
-`<info>#available_tabs`- Returns available tabs.
-`<info>#toDash(url_transformer?, format_filter?)`- Generates a DASH manifest from the streaming data.
-`<info>#chooseFormat(options)`- Selects the format that best matches the given options. This method is used internally by `#download`.
-`<info>#download(options?)`- Downloads the track.
-`<info>#addToWatchHistory()`- Adds the song to the watch history.
-`<info>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="search"></a>
### search(query, filters?)
Searches on YouTube Music.
**Returns:**`Promise.<Search>`| Param | Type | Description || --- | --- | --- || query |`string`| Search query || filters? |`MusicSearchFilters`| Search filters |
<details>
<summary>Search Filters</summary>
| Filter | Type | Value | Description || --- | --- | --- | --- || type |`string`|`all`, `song`, `video`, `album`, `playlist`, `artist`| Search type |
</details>
<details>
<summary>Methods & Getters</summary>
<p>
-`<search>#getMore(shelf)`- Equivalent to clicking on the shelf to load more items.
-`<search>#getContinuation()`- Retrieves continuation, only works for individual sections or filtered results.
-`<search>#selectFilter(name)`- Applies given filter to the search.
-`<search>#has_continuation`- Checks if continuation is available.
-`<search>#filters`- Returns available filters.
-`<search>#songs`- Returns songs shelf.
-`<search>#videos`- Returns videos shelf.
-`<search>#albums`- Returns albums shelf.
-`<search>#artists`- Returns artists shelf.
-`<search>#playlists`- Returns songs shelf.
-`<search>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="gethomefeed"></a>
### getHomeFeed()
Retrieves home feed.
**Returns:**`Promise.<HomeFeed>`
<details>
<summary>Methods & Getters</summary>
<p>
-`<homefeed>#getContinuation()`- Retrieves continuation, only works for individual sections or filtered results.
-`<homefeed>#has_continuation`- Checks if continuation is available.
-`<homefeed>#page`- Returns original InnerTube response (sanitized).
-`<homefeed>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getexplore"></a>
### getExplore()
Retrieves βExploreβ feed.
**Returns:**`Promise.<Explore>`
<details>
<summary>Methods & Getters</summary>
<p>
-`<explore>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getlibrary"></a>
### getLibrary()
Retrieves library.
**Returns:**`Library`
<details>
<summary>Methods & Getters</summary>
<p>
-`<library>#applyFilter(filter)`- Applies given filter to the library.
-`<library>#applySort(sort_by)`- Applies given sort option to the library items.
-`<library>#getContinuation()`- Retrieves continuation of the library items.
-`<library>#has_continuation`- Checks if continuation is available.
-`<library>#filters`- Returns available filters.
-`<library>#sort_options`- Returns available sort options.
-`<library>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getartist"></a>
### getArtist(artist_id)
Retrieves artist's info & content.
**Returns:**`Promise.<Artist>`| Param | Type | Description || --- | --- | --- || artist_id |`string`| Artist id |
<details>
<summary>Methods & Getters</summary>
<p>
-`<artist>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getalbum"></a>
### getAlbum(album_id)
Retrieves given album.
**Returns:**`Promise.<Album>`| Param | Type | Description || --- | --- | --- || album_id |`string`| Album id |
<details>
<summary>Methods & Getters</summary>
<p>
-`<album>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getplaylist"></a>
### getPlaylist(playlist_id)
Retrieves given playlist.
**Returns:**`Promise.<Playlist>`| Param | Type | Description || --- | --- | --- || playlist_id |`string`| Playlist id |
<details>
<summary>Methods & Getters</summary>
<p>
-`<playlist>#getRelated()`- Retrieves related playlists.
-`<playlist>#getSuggestions()`- Retrieves playlist suggestions.
-`<playlist>#getContinuation()`- Retrieves continuation.
-`<playlist>#has_continuation`- Checks if continuation is available.
-`<playlist>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getlyrics"></a>
### getLyrics(video_id)
Retrieves song lyrics.
**Returns:**`Promise.<MusicDescriptionShelf | undefined>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id |
<aname="getupnext"></a>
### getUpNext(video_id, automix?)
Retrieves up next content.
**Returns:**`Promise.<PlaylistPanel>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id || automix? |`boolean`| if automix should be fetched |
<aname="getrelated"></a>
### getRelated(video_id)
Retrieves related content.
**Returns:**`Promise.<Array.<MusicCarouselShelf | MusicDescriptionShelf>>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id |
<aname="getrecap"></a>
### getRecap()
Retrieves your YouTube Music recap.
**Returns:**`Promise.<Recap>`
<details>
<summary>Methods & Getters</summary>
<p>
-`<recap>#getPlaylist()`- Retrieves recap playlist.
-`<recap>#page`- Returns the original InnerTube response(s), parsed and sanitized.
</p>
</details>
<aname="getsearchsuggestions"></a>
### getSearchSuggestions(query)
Retrieves search suggestions.
**Returns:**`Promise.<Array.<SearchSuggestion | HistorySuggestion>>`| Param | Type | Description || --- | --- | --- || query |`string`| Search query |
LuanRT/YouTube.js/blob/main/docs/API/playlist.md:
# PlaylistManager
Playlist management class.
## API* PlaylistManager
*[.create(title, video_ids)](#create)*[.delete(playlist_id)](#delete)*[.addVideos(playlist_id, video_ids)](#addvideos)*[.removeVideos(playlist_id, video_ids)](#removevideos)*[.moveVideo(playlist_id, moved_video_id, predecessor_video_id)](#movevideo)*[.setName(playlist_id, name)](#setname)*[.setDescription(playlist_id, description)](#setdescription)
<aname="create"></a>
### create(title, video_ids)
Creates a playlist.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || title |`string`| Playlist name || video_ids |`string[]`| array of videos |
<aname="delete"></a>
### delete(playlist_id)
Deletes given playlist.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || playlist_id |`string`| Playlist id |
<aname="addvideos"></a>
### addVideos(playlist_id, video_ids)
Adds videos to given playlist.
**Returns:**`Promise.<{ playlist_id: string; action_result: any[] }>`| Param | Type | Description || --- | --- | --- || playlist_id |`string`| Playlist id || video_ids |`string`| array of videos |
<aname="removevideos"></a>
### removeVideos(playlist_id, video_ids)
Removes videos from given playlist.
**Returns:**`Promise.<{ playlist_id: string; action_result: any[] }>`| Param | Type | Description || --- | --- | --- || playlist_id |`string`| Playlist id || video_ids |`string`| array of videos |
<aname="movevideo"></a>
### moveVideo(playlist_id, moved_video_id, predecessor_video_id)
Moves a video to a new position within a given playlist.
**Returns:**`Promise.<{ playlist_id: string; action_result: any[] }>`| Param | Type | Description || --- | --- | --- || playlist_id |`string`| Playlist id || moved_video_id |`string`| the video to be moved || predecessor_video_id |`string`| the video present in the target position |
<aname="setname"></a>
### setName(playlist_id, name)
Sets the name / title for the given playlist.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || playlist_id |`string`| Playlist id || name |`string`| Name / title |
<aname="setdescription"></a>
### setDescription(playlist_id, description)
Sets the description for the given playlist.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || playlist_id |`string`| Playlist id || description |`string`| Description |
LuanRT/YouTube.js/blob/main/docs/API/session.md:
# Session
Represents an InnerTube session.
## API* Session
*[.signIn(credentials?)](#signin) β `function`*[.signOut()](#signout) β `function`*[.key](#key) β `getter`*[.api_version](#api_version) β `getter`*[.client_version](#client_version) β `getter`*[.client_name](#client_name) β `getter`*[.context](#context) β `getter`*[.player](#player) β `getter`*[.lang](#lang) β `getter`
<aname="signin"></a>
### signIn(credentials?)
Signs in with given credentials.
**Returns:**`Promise<void>`| Param | Type | Description || --- | --- | --- || credentials? |`Credentials`| OAuth credentials |
<aname="signout"></a>
### signOut()
Signs out of the current account.
**Returns:**`Promise<ActionsResponse>`
<aname="key"></a>
### key
InnerTube API key.
**Returns:**`string`
<aname="api_version"></a>
### api_version
InnerTube API version.
**Returns:**`string`
<aname="client_version"></a>
### client_version
InnerTube client version.
**Returns:**`string`
<aname="client_name"></a>
### client_name
InnerTube client name.
**Returns:**`string`
<aname="context"></a>
### context
InnerTube context.
**Returns:**`Context`
<aname="player"></a>
### player
Player script object.
**Returns:**`Player`
<aname="lang"></a>
### lang
Client language.
**Returns:**`string`
LuanRT/YouTube.js/blob/main/docs/API/studio.md:
# Studio
YouTube Studio class (WIP).
## API* Studio
*[.setThumbnail(video_id, buffer)](#setthumbnail)*[.updateVideoMetadata(video_id, metadata)](#updatemetadata)*[.upload(file, metadata)](#upload)
<aname="setthumbnail"></a>
### setThumbnail(video_id, buffer)
Uploads a custom thumbnail and sets it for a video.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id || buffer |`Uint8Array`| Thumbnail buffer |
<aname="updatemetadata"></a>
### updateVideoMetadata(video_id, metadata)
Updates given video's metadata.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || video_id |`string`| Video id || metadata |`VideoMetadata`| Video metadata |
<aname="upload"></a>
### upload(file, metadata)
Uploads a video to YouTube.
**Returns:**`Promise.<ApiResponse>`| Param | Type | Description || --- | --- | --- || file |`BodyInit`| Video file || metadata |`UploadedVideoMetadata`| Video metadata |
# TabbedFeed
Represents a feed with tabs.
> **Note**> This class extends the [Feed](feed.md) class.## API* TabbedFeed
*[.tabs](#tabs)*[.getTabByName(title: string)](#gettabbyname)*[.getTabByURL(url: string)](#gettabbyurl)*[.hasTabWithURL(url: string)](#hastabwithurl)*[.title](#title)
<aname="tabs"></a>
### tabs
Returns the feed's tabs as an array of strings.
**Returns:**`string[]`
<aname="gettabbyname"></a>
### getTabByName(title: string)
Fetches a tab by its title.
**Returns:**`Promise<TabbedFeed<T>>`| Param | Type | Description || --- | --- | --- || title |`string`| The title of the tab to get |
<aname="gettabbyurl"></a>
### getTabByURL(url: string)
Fetches a tab by its URL.
**Returns:**`Promise<TabbedFeed<T>>`| Param | Type | Description || --- | --- | --- || url |`string`| The URL of the tab to get |
<aname="hastabwithurl"></a>
### hasTabWithURL(url: string)
Returns whether the feed has a tab with the given URL.
**Returns:**`boolean`| Param | Type | Description || --- | --- | --- || url |`string`| The URL to check |
<aname="title"></a>
### title
Returns the currently selected tab's title.
**Returns:**`string | undefined`
# Updating the Parser
YouTube is constantly changing, so it is not uncommon to see YouTube crawlers/scrapers breaking every now and then.
Our parser, on the other hand, was written so that it behaves similarly to an official client, parsing and mapping renderers (also known as YTNodes) dynamically without hard-coding their path in the response. This way, whenever a new renderer pops up (e.g., when YouTube adds a new feature or makes a minor UI change), the library will print a warning similar to this:
This warning **does not** throw an error. The parser itself will continue working normally, even if a parsing error occurs in an existing renderer parser.
## Adding a New Renderer Parser
Thanks to the modularity of the parser, a renderer can be implemented by simply adding a new file anywhere in the [classes directory](../src/parser/classes)!
For example, suppose we have found a new renderer named `verticalListRenderer`. In that case, to let the parser know it exists at compile-time, we would have to create a file with the following structure:
> `../classes/VerticalList.ts`
```ts
import { Parser, RawNode } from '../index.js';
import { YTNode } from '../helpers.js';
export default class VerticalList extends YTNode {
static type = 'VerticalList';
header;
contents;
constructor(data: RawNode) {
super();
// parse the data here, ex;
this.header = Parser.parseItem(data.header);
this.contents = Parser.parseArray(data.contents);
}
}
You may use the parser's generated class for the new renderer as a starting point for your own implementation.
importSessionfrom'./core/Session.js';import{Kids,Music,Studio}from'./core/clients/index.js';import{AccountManager,InteractionManager,PlaylistManager}from'./core/managers/index.js';import{Feed,TabbedFeed}from'./core/mixins/index.js';import{BrowseEndpoint,GetNotificationMenuEndpoint,GuideEndpoint,NextEndpoint,PlayerEndpoint,ResolveURLEndpoint,SearchEndpoint,Reel,Notification}from'./core/endpoints/index.js';import{Channel,Comments,Guide,HashtagFeed,History,HomeFeed,Library,NotificationsMenu,Playlist,Search,VideoInfo}from'./parser/youtube/index.js';import{ShortFormVideoInfo}from'./parser/ytshorts/index.js';importNavigationEndpointfrom'./parser/classes/NavigationEndpoint.js';import*asProtofrom'./proto/index.js';import*asConstantsfrom'./utils/Constants.js';import{InnertubeError,generateRandomString,throwIfMissing}from'./utils/Utils.js';importtype{ApiResponse}from'./core/Actions.js';importtype{INextRequest}from'./types/index.js';importtype{IBrowseResponse,IParsedResponse}from'./parser/types/index.js';importtype{DownloadOptions,FormatOptions}from'./types/FormatUtils.js';importtype{SessionOptions}from'./core/Session.js';importtypeFormatfrom'./parser/classes/misc/Format.js';exporttypeInnertubeConfig=SessionOptions;exporttypeInnerTubeClient='IOS'|'WEB'|'ANDROID'|'YTMUSIC'|'YTMUSIC_ANDROID'|'YTSTUDIO_ANDROID'|'TV_EMBEDDED'|'YTKIDS';exporttypeSearchFilters=Partial<{upload_date: 'all'|'hour'|'today'|'week'|'month'|'year';type: 'all'|'video'|'channel'|'playlist'|'movie';duration: 'all'|'short'|'medium'|'long';sort_by: 'relevance'|'rating'|'upload_date'|'view_count';features: ('hd'|'subtitles'|'creative_commons'|'3d'|'live'|'purchased'|'4k'|'360'|'location'|'hdr'|'vr180')[];}>;/** * Provides access to various services and modules in the YouTube API. */exportdefaultclassInnertube{
#session: Session;constructor(session: Session){this.#session =session;}staticasynccreate(config: InnertubeConfig={}): Promise<Innertube>{returnnewInnertube(awaitSession.create(config));}asyncgetInfo(target: string|NavigationEndpoint,client?: InnerTubeClient): Promise<VideoInfo>{throwIfMissing({target: target});letnext_payload: INextRequest;if(targetinstanceofNavigationEndpoint){next_payload=NextEndpoint.build({video_id: target.payload?.videoId,playlist_id: target.payload?.playlistId,params: target.payload?.params,playlist_index: target.payload?.index});}elseif(typeoftarget==='string'){next_payload=NextEndpoint.build({video_id: target});}else{thrownewInnertubeError('Invalid target. Expected a video id or NavigationEndpoint.',target);}if(!next_payload.videoId)thrownewInnertubeError('Video id cannot be empty',next_payload);constplayer_payload=PlayerEndpoint.build({video_id: next_payload.videoId,playlist_id: next_payload?.playlistId,client: client,sts: this.#session.player?.sts,po_token: this.#session.po_token});constplayer_response=this.actions.execute(PlayerEndpoint.PATH,player_payload);constnext_response=this.actions.execute(NextEndpoint.PATH,next_payload);constresponse=awaitPromise.all([player_response,next_response]);constcpn=generateRandomString(16);returnnewVideoInfo(response,this.actions,cpn);}asyncgetBasicInfo(video_id: string,client?: InnerTubeClient): Promise<VideoInfo>{throwIfMissing({ video_id });constresponse=awaitthis.actions.execute(PlayerEndpoint.PATH,PlayerEndpoint.build({video_id: video_id,client: client,sts: this.#session.player?.sts,po_token: this.#session.po_token}));constcpn=generateRandomString(16);returnnewVideoInfo([response],this.actions,cpn);}asyncgetShortsVideoInfo(video_id: string,client?: InnerTubeClient): Promise<ShortFormVideoInfo>{throwIfMissing({ video_id });constwatch_response=this.actions.execute(Reel.ReelItemWatchEndpoint.PATH,Reel.ReelItemWatchEndpoint.build({ video_id, client }));constsequence_response=this.actions.execute(Reel.ReelWatchSequenceEndpoint.PATH,Reel.ReelWatchSequenceEndpoint.build({sequence_params: Proto.encodeReelSequence(video_id)}));constresponse=awaitPromise.all([watch_response,sequence_response]);constcpn=generateRandomString(16);returnnewShortFormVideoInfo([response[0]],this.actions,cpn,response[1]);}asyncsearch(query: string,filters: SearchFilters={}): Promise<Search>{throwIfMissing({ query });constresponse=awaitthis.actions.execute(SearchEndpoint.PATH,SearchEndpoint.build({
query,params: filters ? Proto.encodeSearchFilters(filters) : undefined}));returnnewSearch(this.actions,response);}asyncgetSearchSuggestions(query: string): Promise<string[]>{throwIfMissing({ query });consturl=newURL(`${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');constresponse=awaitthis.#session.http.fetch(url);constresponse_data=awaitresponse.text();constdata=JSON.parse(response_data.replace(')]}\'',''));constsuggestions=data[1].map((suggestion: any)=>suggestion[0]);returnsuggestions;}asyncgetComments(video_id: string,sort_by?: 'TOP_COMMENTS'|'NEWEST_FIRST'): Promise<Comments>{throwIfMissing({ video_id });constresponse=awaitthis.actions.execute(NextEndpoint.PATH,NextEndpoint.build({continuation: Proto.encodeCommentsSectionParams(video_id,{sort_by: sort_by||'TOP_COMMENTS'})}));returnnewComments(this.actions,response.data);}asyncgetHomeFeed(): Promise<HomeFeed>{constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: 'FEwhat_to_watch'}));returnnewHomeFeed(this.actions,response);}/** * Retrieves YouTube's content guide. */asyncgetGuide(): Promise<Guide>{constresponse=awaitthis.actions.execute(GuideEndpoint.PATH);returnnewGuide(response.data);}asyncgetLibrary(): Promise<Library>{constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: 'FElibrary'}));returnnewLibrary(this.actions,response);}asyncgetHistory(): Promise<History>{constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: 'FEhistory'}));returnnewHistory(this.actions,response);}asyncgetTrending(): Promise<TabbedFeed<IBrowseResponse>>{constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,{ ...BrowseEndpoint.build({browse_id: 'FEtrending'}),parse: true});returnnewTabbedFeed(this.actions,response);}asyncgetSubscriptionsFeed(): Promise<Feed<IBrowseResponse>>{constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,{ ...BrowseEndpoint.build({browse_id: 'FEsubscriptions'}),parse: true});returnnewFeed(this.actions,response);}asyncgetChannelsFeed(): Promise<Feed<IBrowseResponse>>{constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,{ ...BrowseEndpoint.build({browse_id: 'FEchannels'}),parse: true});returnnewFeed(this.actions,response);}asyncgetChannel(id: string): Promise<Channel>{throwIfMissing({ id });constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: id}));returnnewChannel(this.actions,response);}asyncgetNotifications(): Promise<NotificationsMenu>{constresponse=awaitthis.actions.execute(GetNotificationMenuEndpoint.PATH,GetNotificationMenuEndpoint.build({notifications_menu_request_type: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX'}));returnnewNotificationsMenu(this.actions,response);}asyncgetUnseenNotificationsCount(): Promise<number>{constresponse=awaitthis.actions.execute(Notification.GetUnseenCountEndpoint.PATH);// FIXME: properly parse this.returnresponse.data?.unseenCount||response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount||0;}/** * Retrieves the user's playlists. */asyncgetPlaylists(): Promise<Feed<IBrowseResponse>>{constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,{ ...BrowseEndpoint.build({browse_id: 'FEplaylist_aggregation'}),parse: true});returnnewFeed(this.actions,response);}asyncgetPlaylist(id: string): Promise<Playlist>{throwIfMissing({ id });if(!id.startsWith('VL')){id=`VL${id}`;}constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: id}));returnnewPlaylist(this.actions,response);}asyncgetHashtag(hashtag: string): Promise<HashtagFeed>{throwIfMissing({ hashtag });constresponse=awaitthis.actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: 'FEhashtag',params: Proto.encodeHashtag(hashtag)}));returnnewHashtagFeed(this.actions,response);}/** * 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}. * @param video_id - The video id. * @param options - Format options. */asyncgetStreamingData(video_id: string,options: FormatOptions={}): Promise<Format>{constinfo=awaitthis.getBasicInfo(video_id);constformat=info.chooseFormat(options);format.url=format.decipher(this.#session.player);returnformat;}/** * Downloads a given video. If all you need the direct download link, see {@link getStreamingData}. * If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}. * @param video_id - The video id. * @param options - Download options. */asyncdownload(video_id: string,options?: DownloadOptions): Promise<ReadableStream<Uint8Array>>{constinfo=awaitthis.getBasicInfo(video_id,options?.client);returninfo.download(options);}/** * Resolves the given URL. * @param url - The URL. */asyncresolveURL(url: string): Promise<NavigationEndpoint>{constresponse=awaitthis.actions.execute(ResolveURLEndpoint.PATH,{ ...ResolveURLEndpoint.build({ url }),parse: true});if(!response.endpoint)thrownewInnertubeError('Failed to resolve URL. Expected a NavigationEndpoint but got undefined',response);returnresponse.endpoint;}/** * Utility method to call an endpoint without having to use {@link Actions}. * @param endpoint -The endpoint to call. * @param args - Call arguments. */call<TextendsIParsedResponse>(endpoint: NavigationEndpoint,args: {[key: string]: any;parse: true}): Promise<T>;call(endpoint: NavigationEndpoint,args?: {[key: string]: any;parse?: false}): Promise<ApiResponse>;call(endpoint: NavigationEndpoint,args?: object): Promise<IParsedResponse|ApiResponse>{returnendpoint.call(this.actions,args);}/** * An interface for interacting with YouTube Music. */getmusic(){returnnewMusic(this.#session);}/** * An interface for interacting with YouTube Studio. */getstudio(){returnnewStudio(this.#session);}/** * An interface for interacting with YouTube Kids. */getkids(){returnnewKids(this.#session);}/** * An interface for managing and retrieving account information. */getaccount(){returnnewAccountManager(this.#session.actions);}/** * An interface for managing playlists. */getplaylist(){returnnewPlaylistManager(this.#session.actions);}/** * An interface for directly interacting with certain YouTube features. */getinteract(){returnnewInteractionManager(this.#session.actions);}/** * An internal class used to dispatch requests. */getactions(){returnthis.#session.actions;}/** * The session used by this instance. */getsession(){returnthis.#session;}}
LuanRT/YouTube.js/blob/main/src/core/Actions.ts:
import{Parser,NavigateAction}from'../parser/index.js';import{InnertubeError}from'../utils/Utils.js';importtype{Session}from'./index.js';importtype{IBrowseResponse,IGetNotificationsMenuResponse,INextResponse,IPlayerResponse,IResolveURLResponse,ISearchResponse,IUpdatedMetadataResponse,IParsedResponse,IRawResponse}from'../parser/types/index.js';exportinterfaceApiResponse{success: boolean;status_code: number;data: IRawResponse;}exporttypeInnertubeEndpoint='/player'|'/search'|'/browse'|'/next'|'/reel'|'/updated_metadata'|'/notification/get_notification_menu'|string;exporttypeParsedResponse<T>=Textends'/player' ? IPlayerResponse :
Textends'/search' ? ISearchResponse :
Textends'/browse' ? IBrowseResponse :
Textends'/next' ? INextResponse :
Textends'/updated_metadata' ? IUpdatedMetadataResponse :
Textends'/navigation/resolve_url' ? IResolveURLResponse :
Textends'/notification/get_notification_menu' ? IGetNotificationsMenuResponse :
IParsedResponse;exportdefaultclassActions{session: Session;constructor(session: Session){this.session=session;}/** * Mimmics the Axios API using Fetch's Response object. * @param response - The response object. */async #wrap(response: Response): Promise<ApiResponse>{return{success: response.ok,status_code: response.status,data: JSON.parse(awaitresponse.text())};}/** * Makes calls to the playback tracking API. * @param url - The URL to call. * @param client - The client to use. * @param params - Call parameters. */asyncstats(url: string,client: {client_name: string;client_version: string},params: {[key: string]: any}): Promise<Response>{consts_url=newURL(url);s_url.searchParams.set('ver','2');s_url.searchParams.set('c',client.client_name.toLowerCase());s_url.searchParams.set('cbrver',client.client_version);s_url.searchParams.set('cver',client.client_version);for(constkeyofObject.keys(params)){s_url.searchParams.set(key,params[key]);}constresponse=awaitthis.session.http.fetch(s_url);returnresponse;}/** * Executes an API call. * @param endpoint - The endpoint to call. * @param args - Call arguments */asyncexecute<TextendsInnertubeEndpoint>(endpoint: T,args: {[key: string]: any;parse: true;protobuf?: false;serialized_data?: any}): Promise<ParsedResponse<T>>;asyncexecute<TextendsInnertubeEndpoint>(endpoint: T,args?: {[key: string]: any;parse?: false;protobuf?: true;serialized_data?: any}): Promise<ApiResponse>;asyncexecute<TextendsInnertubeEndpoint>(endpoint: T,args?: {[key: string]: any;parse?: boolean;protobuf?: boolean;serialized_data?: any}): Promise<ParsedResponse<T>|ApiResponse>{letdata;if(args&&!args.protobuf){data={ ...args};if(Reflect.has(data,'browseId')){if(this.#needsLogin(data.browseId)&&!this.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');}if(Reflect.has(data,'override_endpoint'))deletedata.override_endpoint;if(Reflect.has(data,'parse'))deletedata.parse;if(Reflect.has(data,'request'))deletedata.request;if(Reflect.has(data,'clientActions'))deletedata.clientActions;if(Reflect.has(data,'settingItemIdForClient'))deletedata.settingItemIdForClient;if(Reflect.has(data,'action')){data.actions=[data.action];deletedata.action;}if(Reflect.has(data,'boolValue')){data.newValue={boolValue: data.boolValue};deletedata.boolValue;}if(Reflect.has(data,'token')){data.continuation=data.token;deletedata.token;}if(data?.client==='YTMUSIC'){data.isAudioOnly=true;}}elseif(args){data=args.serialized_data;}consttarget_endpoint=Reflect.has(args||{},'override_endpoint') ? args?.override_endpoint : endpoint;constresponse=awaitthis.session.http.fetch(target_endpoint,{method: 'POST',body: args?.protobuf ? data : JSON.stringify((data||{})),headers: {'Content-Type': args?.protobuf ?
'application/x-protobuf' :
'application/json'}});if(args?.parse){letparsed_response=Parser.parseResponse<ParsedResponse<T>>(awaitresponse.json());// Handle redirectsif(this.#isBrowse(parsed_response)&&parsed_response.on_response_received_actions?.first()?.type==='navigateAction'){constnavigate_action=parsed_response.on_response_received_actions.firstOfType(NavigateAction);if(navigate_action){parsed_response=awaitnavigate_action.endpoint.call(this,{parse: true});}}returnparsed_response;}returnthis.#wrap(response);}
#isBrowse(response: IParsedResponse): response is IBrowseResponse{return'on_response_received_actions'inresponse;}
#needsLogin(id: string){return['FElibrary','FEhistory','FEsubscriptions','FEchannels','FEplaylist_aggregation','FEmusic_listening_review','FEmusic_library_landing','SPaccount_overview','SPaccount_notifications','SPaccount_privacy','SPtime_watched'].includes(id);}}
LuanRT/YouTube.js/blob/main/src/core/OAuth2.ts:
import{OAuth2Error,Platform}from'../utils/Utils.js';import{Log,Constants}from'../utils/index.js';importtypeSessionfrom'./Session.js';constTAG='OAuth2';exporttypeOAuth2ClientID={client_id: string;client_secret: string;};exporttypeOAuth2Tokens={access_token: string;expiry_date: string;expires_in?: number;refresh_token: string;scope?: string;token_type?: string;client?: OAuth2ClientID;};exporttypeDeviceAndUserCode={device_code: string;expires_in: number;interval: number;user_code: string;verification_url: string;error_code?: string;};exporttypeOAuth2AuthEventHandler=(data: {credentials: OAuth2Tokens;})=>void;exporttypeOAuth2AuthPendingEventHandler=(data: DeviceAndUserCode)=>void;exporttypeOAuth2AuthErrorEventHandler=(err: OAuth2Error)=>void;exportdefaultclassOAuth2{
#session: Session;YTTV_URL: URL;AUTH_SERVER_CODE_URL: URL;AUTH_SERVER_TOKEN_URL: URL;AUTH_SERVER_REVOKE_TOKEN_URL: URL;client_id: OAuth2ClientID|undefined;oauth2_tokens: OAuth2Tokens|undefined;constructor(session: Session){this.#session =session;this.YTTV_URL=newURL('/tv',Constants.URLS.YT_BASE);this.AUTH_SERVER_CODE_URL=newURL('/o/oauth2/device/code',Constants.URLS.YT_BASE);this.AUTH_SERVER_TOKEN_URL=newURL('/o/oauth2/token',Constants.URLS.YT_BASE);this.AUTH_SERVER_REVOKE_TOKEN_URL=newURL('/o/oauth2/revoke',Constants.URLS.YT_BASE);}asyncinit(tokens?: OAuth2Tokens): Promise<void>{if(tokens){this.setTokens(tokens);if(this.shouldRefreshToken()){awaitthis.refreshAccessToken();}this.#session.emit('auth',{credentials: this.oauth2_tokens});return;}constloaded_from_cache=awaitthis.#loadFromCache();if(loaded_from_cache){Log.info(TAG,'Loaded OAuth2 tokens from cache.',this.oauth2_tokens);return;}if(!this.client_id)this.client_id=awaitthis.getClientID();// Initialize OAuth2 flowconstdevice_and_user_code=awaitthis.getDeviceAndUserCode();this.#session.emit('auth-pending',device_and_user_code);this.pollForAccessToken(device_and_user_code);}setTokens(tokens: OAuth2Tokens): void{consttokensMod=tokens;// Convert access token remaining lifetime to ISO stringif(tokensMod.expires_in){tokensMod.expiry_date=newDate(Date.now()+tokensMod.expires_in*1000).toISOString();deletetokensMod.expires_in;// We don't need this anymore}if(!this.validateTokens(tokensMod))thrownewOAuth2Error('Invalid tokens provided.');this.oauth2_tokens=tokensMod;if(tokensMod.client){Log.info(TAG,'Using provided client id and secret.');this.client_id=tokensMod.client;}}asynccacheCredentials(): Promise<void>{constencoder=newTextEncoder();constdata=encoder.encode(JSON.stringify(this.oauth2_tokens));awaitthis.#session.cache?.set('youtubei_oauth_credentials',data.buffer);}async #loadFromCache(): Promise<boolean>{constdata=awaitthis.#session.cache?.get('youtubei_oauth_credentials');if(!data)returnfalse;constdecoder=newTextDecoder();constcredentials=JSON.parse(decoder.decode(data));this.setTokens(credentials);this.#session.emit('auth',{ credentials });returntrue;}asyncremoveCache(): Promise<void>{awaitthis.#session.cache?.remove('youtubei_oauth_credentials');}asyncpollForAccessToken(device_and_user_code: DeviceAndUserCode): Promise<void>{if(!this.client_id)thrownewOAuth2Error('Client ID is missing.');const{ device_code, interval }=device_and_user_code;const{ client_id, client_secret }=this.client_id;constpayload={
client_id,
client_secret,code: device_code,grant_type: 'http://oauth.net/grant_type/device/1.0'};constconnInterval=setInterval(async()=>{constresponse=awaitthis.#http.fetch_function(this.AUTH_SERVER_TOKEN_URL,{body: JSON.stringify(payload),method: 'POST',headers: {'Content-Type': 'application/json'}});constresponse_data=awaitresponse.json();if(response_data.error){switch(response_data.error){case'access_denied':
this.#session.emit('auth-error',newOAuth2Error('Access was denied.',response_data));clearInterval(connInterval);break;case'expired_token':
this.#session.emit('auth-error',newOAuth2Error('The device code has expired.',response_data));clearInterval(connInterval);break;case'authorization_pending':
case'slow_down':
Log.info(TAG,'Polling for access token...');break;default:
this.#session.emit('auth-error',newOAuth2Error('Server returned an unexpected error.',response_data));clearInterval(connInterval);break;}return;}this.setTokens(response_data);this.#session.emit('auth',{credentials: this.oauth2_tokens});clearInterval(connInterval);},interval*1000);}asyncrevokeCredentials(): Promise<Response|undefined>{if(!this.oauth2_tokens)thrownewOAuth2Error('Access token not found');awaitthis.removeCache();consturl=this.AUTH_SERVER_REVOKE_TOKEN_URL;url.searchParams.set('token',this.oauth2_tokens.access_token);returnthis.#session.http.fetch_function(url,{method: 'POST'});}asyncrefreshAccessToken(): Promise<void>{if(!this.client_id)this.client_id=awaitthis.getClientID();if(!this.oauth2_tokens)thrownewOAuth2Error('No tokens available to refresh.');const{ client_id, client_secret }=this.client_id;const{ refresh_token }=this.oauth2_tokens;constpayload={
client_id,
client_secret,
refresh_token,grant_type: 'refresh_token'};constresponse=awaitthis.#http.fetch_function(this.AUTH_SERVER_TOKEN_URL,{body: JSON.stringify(payload),method: 'POST',headers: {'Content-Type': 'application/json'}});if(!response.ok)thrownewOAuth2Error(`Failed to refresh access token: ${response.status}`);constresponse_data=awaitresponse.json();if(response_data.error_code)thrownewOAuth2Error('Authorization server returned an error',response_data);this.oauth2_tokens.access_token=response_data.access_token;this.oauth2_tokens.expiry_date=newDate(Date.now()+response_data.expires_in*1000).toISOString();this.#session.emit('update-credentials',{credentials: this.oauth2_tokens});}asyncgetDeviceAndUserCode(): Promise<DeviceAndUserCode>{if(!this.client_id)thrownewOAuth2Error('Client ID is missing.');const{ client_id }=this.client_id;constpayload={
client_id,scope: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content',device_id: Platform.shim.uuidv4(),device_model: 'ytlr::'};constresponse=awaitthis.#http.fetch_function(this.AUTH_SERVER_CODE_URL,{body: JSON.stringify(payload),method: 'POST',headers: {'Content-Type': 'application/json'}});if(!response.ok)thrownewOAuth2Error(`Failed to get device/user code: ${response.status}`);constresponse_data=awaitresponse.json();if(response_data.error_code)thrownewOAuth2Error('Authorization server returned an error',response_data);returnresponse_data;}asyncgetClientID(): Promise<OAuth2ClientID>{constyttv_response=awaitthis.#http.fetch_function(this.YTTV_URL,{headers: {'User-Agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version','Referer': 'https://www.youtube.com/tv','Accept-Language': 'en-US'}});if(!yttv_response.ok)thrownewOAuth2Error(`Failed to get client ID: ${yttv_response.status}`);constyttv_response_data=awaityttv_response.text();letscript_url_body: RegExpExecArray|null;if((script_url_body=Constants.OAUTH.REGEX.TV_SCRIPT.exec(yttv_response_data))!==null){Log.info(TAG,`Got YouTubeTV script URL (${script_url_body[1]})`);constscript_response=awaitthis.#http.fetch(script_url_body[1],{baseURL: Constants.URLS.YT_BASE});if(!script_response.ok)thrownewOAuth2Error(`TV script request failed with status code ${script_response.status}`);constscript_response_data=awaitscript_response.text();constclient_identity=script_response_data.match(Constants.OAUTH.REGEX.CLIENT_IDENTITY);if(!client_identity||!client_identity.groups)thrownewOAuth2Error('Could not obtain client ID.');const{ client_id, client_secret }=client_identity.groups;Log.info(TAG,`Client identity retrieved (clientId=${client_id}, clientSecret=${client_secret}).`);return{
client_id,
client_secret
};}thrownewOAuth2Error('Could not obtain script URL.');}shouldRefreshToken(): boolean{if(!this.oauth2_tokens)returnfalse;returnDate.now()>newDate(this.oauth2_tokens.expiry_date).getTime();}validateTokens(tokens: OAuth2Tokens): boolean{constpropertiesAreValid=(Boolean(tokens.access_token)&&Boolean(tokens.expiry_date)&&Boolean(tokens.refresh_token));consttypesAreValid=(typeoftokens.access_token==='string'&&typeoftokens.expiry_date==='string'&&typeoftokens.refresh_token==='string');returntypesAreValid&&propertiesAreValid;}get #http(){returnthis.#session.http;}}
LuanRT/YouTube.js/blob/main/src/core/Player.ts:
import{Log,LZW,Constants}from'../utils/index.js';import{Platform,getRandomUserAgent,getStringBetweenStrings,findFunction,PlayerError}from'../utils/Utils.js';importtype{ICache,FetchFunction}from'../types/index.js';constTAG='Player';/** * Represents YouTube's player script. This is required to decipher signatures. */exportdefaultclassPlayer{player_id: string;sts: number;nsig_sc?: string;sig_sc?: string;po_token?: string;constructor(player_id: string,signature_timestamp: number,sig_sc?: string,nsig_sc?: string){this.player_id=player_id;this.sts=signature_timestamp;this.nsig_sc=nsig_sc;this.sig_sc=sig_sc;}staticasynccreate(cache: ICache|undefined,fetch: FetchFunction=Platform.shim.fetch,po_token?: string): Promise<Player>{consturl=newURL('/iframe_api',Constants.URLS.YT_BASE);constres=awaitfetch(url);if(res.status!==200)thrownewPlayerError('Failed to request player id');constjs=awaitres.text();constplayer_id=getStringBetweenStrings(js,'player\\/','\\/');Log.info(TAG,`Got player id (${player_id}). Checking for cached players..`);if(!player_id)thrownewPlayerError('Failed to get player id');// We have the player id, now we can check if we have a cached player.if(cache){constcached_player=awaitPlayer.fromCache(cache,player_id);if(cached_player){Log.info(TAG,'Found up-to-date player data in cache.');cached_player.po_token=po_token;returncached_player;}}constplayer_url=newURL(`/s/player/${player_id}/player_ias.vflset/en_US/base.js`,Constants.URLS.YT_BASE);Log.info(TAG,`Could not find any cached player. Will download a new player from ${player_url}.`);constplayer_res=awaitfetch(player_url,{headers: {'user-agent': getRandomUserAgent('desktop')}});if(!player_res.ok){thrownewPlayerError(`Failed to get player data: ${player_res.status}`);}constplayer_js=awaitplayer_res.text();constsig_timestamp=this.extractSigTimestamp(player_js);constsig_sc=this.extractSigSourceCode(player_js);constnsig_sc=this.extractNSigSourceCode(player_js);Log.info(TAG,`Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`);constplayer=awaitPlayer.fromSource(player_id,sig_timestamp,cache,sig_sc,nsig_sc);player.po_token=po_token;returnplayer;}decipher(url?: string,signature_cipher?: string,cipher?: string,this_response_nsig_cache?: Map<string,string>): string{url=url||signature_cipher||cipher;if(!url)thrownewPlayerError('No valid URL to decipher');constargs=newURLSearchParams(url);consturl_components=newURL(args.get('url')||url);if(this.sig_sc&&(signature_cipher||cipher)){constsignature=Platform.shim.eval(this.sig_sc,{sig: args.get('s')});Log.info(TAG,`Transformed signature from ${args.get('s')} to ${signature}.`);if(typeofsignature!=='string')thrownewPlayerError('Failed to decipher signature');constsp=args.get('sp');sp ?
url_components.searchParams.set(sp,signature) :
url_components.searchParams.set('signature',signature);}constn=url_components.searchParams.get('n');if(this.nsig_sc&&n){letnsig;if(this_response_nsig_cache&&this_response_nsig_cache.has(n)){nsig=this_response_nsig_cache.get(n)asstring;}else{nsig=Platform.shim.eval(this.nsig_sc,{nsig: n});Log.info(TAG,`Transformed n signature from ${n} to ${nsig}.`);if(typeofnsig!=='string')thrownewPlayerError('Failed to decipher nsig');if(nsig.startsWith('enhanced_except_')){Log.warn(TAG,'Could not transform nsig, download may be throttled.');}elseif(this_response_nsig_cache){this_response_nsig_cache.set(n,nsig);}}url_components.searchParams.set('n',nsig);}// @NOTE: SABR requests should include the PoToken (not base64d, but as bytes!) in the payload.if(url_components.searchParams.get('sabr')!=='1'&&this.po_token)url_components.searchParams.set('pot',this.po_token);constclient=url_components.searchParams.get('c');switch(client){case'WEB':
url_components.searchParams.set('cver',Constants.CLIENTS.WEB.VERSION);break;case'WEB_REMIX':
url_components.searchParams.set('cver',Constants.CLIENTS.YTMUSIC.VERSION);break;case'WEB_KIDS':
url_components.searchParams.set('cver',Constants.CLIENTS.WEB_KIDS.VERSION);break;case'ANDROID':
url_components.searchParams.set('cver',Constants.CLIENTS.ANDROID.VERSION);break;case'ANDROID_MUSIC':
url_components.searchParams.set('cver',Constants.CLIENTS.YTMUSIC_ANDROID.VERSION);break;case'TVHTML5_SIMPLY_EMBEDDED_PLAYER':
url_components.searchParams.set('cver',Constants.CLIENTS.TV_EMBEDDED.VERSION);break;}constresult=url_components.toString();Log.info(TAG,`Deciphered URL: ${result}`);returnurl_components.toString();}staticasyncfromCache(cache: ICache,player_id: string): Promise<Player|null>{constbuffer=awaitcache.get(player_id);if(!buffer)returnnull;constview=newDataView(buffer);constversion=view.getUint32(0,true);if(version!==Player.LIBRARY_VERSION)returnnull;constsig_timestamp=view.getUint32(4,true);constsig_len=view.getUint32(8,true);constsig_buf=buffer.slice(12,12+sig_len);constnsig_buf=buffer.slice(12+sig_len);constsig_sc=LZW.decompress(newTextDecoder().decode(sig_buf));constnsig_sc=LZW.decompress(newTextDecoder().decode(nsig_buf));returnnewPlayer(player_id,sig_timestamp,sig_sc,nsig_sc);}staticasyncfromSource(player_id: string,sig_timestamp: number,cache?: ICache,sig_sc?: string,nsig_sc?: string): Promise<Player>{constplayer=newPlayer(player_id,sig_timestamp,sig_sc,nsig_sc);awaitplayer.cache(cache);returnplayer;}asynccache(cache?: ICache): Promise<void>{if(!cache||!this.sig_sc||!this.nsig_sc)return;constencoder=newTextEncoder();constsig_buf=encoder.encode(LZW.compress(this.sig_sc));constnsig_buf=encoder.encode(LZW.compress(this.nsig_sc));constbuffer=newArrayBuffer(12+sig_buf.byteLength+nsig_buf.byteLength);constview=newDataView(buffer);view.setUint32(0,Player.LIBRARY_VERSION,true);view.setUint32(4,this.sts,true);view.setUint32(8,sig_buf.byteLength,true);newUint8Array(buffer).set(sig_buf,12);newUint8Array(buffer).set(nsig_buf,12+sig_buf.byteLength);awaitcache.set(this.player_id,newUint8Array(buffer));}staticextractSigTimestamp(data: string): number{returnparseInt(getStringBetweenStrings(data,'signatureTimestamp:',',')||'0');}staticextractSigSourceCode(data: string): string{constcalls=getStringBetweenStrings(data,'function(a){a=a.split("")','return a.join("")}');constobj_name=calls?.split(/\.|\[/)?.[0]?.replace(';','')?.trim();constfunctions=getStringBetweenStrings(data,`var ${obj_name}={`,'};');if(!functions||!calls)Log.warn(TAG,'Failed to extract signature decipher algorithm.');return`function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`;}staticextractNSigSourceCode(data: string): string|undefined{constnsig_function=findFunction(data,{includes: 'enhanced_except'});if(nsig_function){return`${nsig_function.result}${nsig_function.name}(nsig);`;}}geturl(): string{returnnewURL(`/s/player/${this.player_id}/player_ias.vflset/en_US/base.js`,Constants.URLS.YT_BASE).toString();}staticgetLIBRARY_VERSION(): number{return11;}}
LuanRT/YouTube.js/blob/main/src/core/Session.ts:
importOAuth2from'./OAuth2.js';import{Log,EventEmitter,HTTPClient,LZW}from'../utils/index.js';import*asConstantsfrom'../utils/Constants.js';import*asProtofrom'../proto/index.js';importActionsfrom'./Actions.js';importPlayerfrom'./Player.js';import{generateRandomString,getRandomUserAgent,InnertubeError,Platform,SessionError}from'../utils/Utils.js';importtype{DeviceCategory}from'../utils/Utils.js';importtype{FetchFunction,ICache}from'../types/index.js';importtype{OAuth2Tokens,OAuth2AuthErrorEventHandler,OAuth2AuthPendingEventHandler,OAuth2AuthEventHandler}from'./OAuth2.js';exportenumClientType{WEB='WEB',KIDS='WEB_KIDS',MUSIC='WEB_REMIX',IOS='iOS',ANDROID='ANDROID',ANDROID_MUSIC='ANDROID_MUSIC',ANDROID_CREATOR='ANDROID_CREATOR',TV_EMBEDDED='TVHTML5_SIMPLY_EMBEDDED_PLAYER'}exporttypeContext={client: {hl: string;gl: string;remoteHost?: string;screenDensityFloat?: number;screenHeightPoints?: number;screenPixelDensity?: number;screenWidthPoints?: number;visitorData?: string;clientName: string;clientVersion: string;clientScreen?: string,androidSdkVersion?: number;osName: string;osVersion: string;platform: string;clientFormFactor: string;userInterfaceTheme?: string;timeZone: string;userAgent?: string;browserName?: string;browserVersion?: string;originalUrl?: string;deviceMake: string;deviceModel: string;utcOffsetMinutes: number;mainAppWebInfo?: {graftUrl: string;pwaInstallabilityStatus: string;webDisplayMode: string;isWebNativeShareAvailable: boolean;};memoryTotalKbytes?: string;configInfo?: {appInstallData: string;},kidsAppInfo?: {categorySettings: {enabledCategories: string[];};contentSettings: {corpusPreference: string;kidsNoSearchMode: string;};};};user: {enableSafetyMode: boolean;lockedSafetyMode: boolean;onBehalfOfUser?: string;};thirdParty?: {embedUrl: string;};request?: {useSsl: boolean;internalExperimentFlags: any[];};}typeContextData={hl: string;gl: string;remote_host?: string;visitor_data: string;client_name: string;client_version: string;os_name: string;os_version: string;device_category: string;time_zone: string;enable_safety_mode: boolean;browser_name?: string;browser_version?: string;app_install_data?: string;device_make: string;device_model: string;on_behalf_of_user?: string;}exporttypeSessionOptions={/** * Language. */lang?: string;/** * Geolocation. */location?: string;/** * The account index to use. This is useful if you have multiple accounts logged in. * * **NOTE:** Only works if you are signed in with cookies. */account_index?: number;/** * Specify the Page ID of the YouTube profile/channel to use, if the logged-in account has multiple profiles. */on_behalf_of_user?: string;/** * Specifies whether to retrieve the JS player. Disabling this will make session creation faster. * * **NOTE:** Deciphering formats is not possible without the JS player. */retrieve_player?: boolean;/** * Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content. */enable_safety_mode?: boolean;/** * Specifies whether to generate the session data locally or retrieve it from YouTube. * This can be useful if you need more performance. * * **NOTE:** If you are using the cache option and a session has already been generated, this will be ignored. * If you want to force a new session to be generated, you must clear the cache or disable session caching. */generate_session_locally?: boolean;/** * Specifies whether the session data should be cached. */enable_session_cache?: boolean;/** * Platform to use for the session. */device_category?: DeviceCategory;/** * InnerTube client type. */client_type?: ClientType;/** * The time zone. */timezone?: string;/** * Used to cache algorithms, session data, and OAuth2 tokens. */cache?: ICache;/** * YouTube cookies. */cookie?: string;/** * Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in. * A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint. */visitor_data?: string;/** * Fetch function to use. */fetch?: FetchFunction;/** * Proof of Origin Token. This is an attestation token generated by BotGuard/DroidGuard. It is used to confirm that the request is coming from a genuine device. */po_token?: string;}exporttypeSessionData={context: Context;api_key: string;api_version: string;}exporttypeSWSessionData={context_data: ContextData;api_key: string;api_version: string;}exporttypeSessionArgs={lang: string;location: string;time_zone: string;device_category: DeviceCategory;client_name: ClientType;enable_safety_mode: boolean;visitor_data: string;on_behalf_of_user: string|undefined;}constTAG='Session';/** * Represents an InnerTube session. This holds all the data needed to make requests to YouTube. */exportdefaultclassSessionextendsEventEmitter{context: Context;player?: Player;oauth: OAuth2;http: HTTPClient;logged_in: boolean;actions: Actions;cache?: ICache;key: string;api_version: string;account_index: number;po_token?: string;constructor(context: Context,api_key: string,api_version: string,account_index: number,player?: Player,cookie?: string,fetch?: FetchFunction,cache?: ICache,po_token?: string){super();this.http=newHTTPClient(this,cookie,fetch);this.actions=newActions(this);this.oauth=newOAuth2(this);this.logged_in=!!cookie;this.cache=cache;this.account_index=account_index;this.key=api_key;this.api_version=api_version;this.context=context;this.player=player;this.po_token=po_token;}on(type: 'auth',listener: OAuth2AuthEventHandler): void;on(type: 'auth-pending',listener: OAuth2AuthPendingEventHandler): void;on(type: 'auth-error',listener: OAuth2AuthErrorEventHandler): void;on(type: 'update-credentials',listener: OAuth2AuthEventHandler): void;on(type: string,listener: (...args: any[])=>void): void{super.on(type,listener);}once(type: 'auth',listener: OAuth2AuthEventHandler): void;once(type: 'auth-pending',listener: OAuth2AuthPendingEventHandler): void;once(type: 'auth-error',listener: OAuth2AuthErrorEventHandler): void;once(type: string,listener: (...args: any[])=>void): void{super.once(type,listener);}staticasynccreate(options: SessionOptions={}){const{ context, api_key, api_version, account_index }=awaitSession.getSessionData(options.lang,options.location,options.account_index,options.visitor_data,options.enable_safety_mode,options.generate_session_locally,options.device_category,options.client_type,options.timezone,options.fetch,options.on_behalf_of_user,options.cache,options.enable_session_cache,options.po_token);returnnewSession(context,api_key,api_version,account_index,options.retrieve_player===false ? undefined : awaitPlayer.create(options.cache,options.fetch,options.po_token),options.cookie,options.fetch,options.cache,options.po_token);}/** * Retrieves session data from cache. * @param cache - A valid cache implementation. * @param session_args - User provided session arguments. */staticasyncfromCache(cache: ICache,session_args: SessionArgs): Promise<SessionData|null>{constbuffer=awaitcache.get('innertube_session_data');if(!buffer)returnnull;constdata=newTextDecoder().decode(buffer.slice(4));try{constresult=JSON.parse(LZW.decompress(data))asSessionData;if(session_args.visitor_data){result.context.client.visitorData=session_args.visitor_data;}if(session_args.lang)result.context.client.hl=session_args.lang;if(session_args.location)result.context.client.gl=session_args.location;if(session_args.on_behalf_of_user)result.context.user.onBehalfOfUser=session_args.on_behalf_of_user;result.context.client.timeZone=session_args.time_zone;result.context.client.platform=session_args.device_category.toUpperCase();result.context.client.clientName=session_args.client_name;result.context.user.enableSafetyMode=session_args.enable_safety_mode;returnresult;}catch(error){Log.error(TAG,'Failed to parse session data from cache.',error);returnnull;}}staticasyncgetSessionData(lang='',location='',account_index=0,visitor_data='',enable_safety_mode=false,generate_session_locally=false,device_category: DeviceCategory='desktop',client_name: ClientType=ClientType.WEB,tz: string=Intl.DateTimeFormat().resolvedOptions().timeZone,fetch: FetchFunction=Platform.shim.fetch,on_behalf_of_user?: string,cache?: ICache,enable_session_cache=true,po_token?: string){constsession_args={ lang, location,time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user, po_token };letsession_data: SessionData|undefined;if(cache&&enable_session_cache){constcached_session_data=awaitthis.fromCache(cache,session_args);if(cached_session_data){Log.info(TAG,'Found session data in cache.');session_data=cached_session_data;}}if(!session_data){Log.info(TAG,'Generating session data.');letapi_key=Constants.CLIENTS.WEB.API_KEY;letapi_version=Constants.CLIENTS.WEB.API_VERSION;letcontext_data: ContextData={hl: lang||'en',gl: location||'US',remote_host: '',visitor_data: visitor_data||Proto.encodeVisitorData(generateRandomString(11),Math.floor(Date.now()/1000)),client_name: client_name,client_version: Constants.CLIENTS.WEB.VERSION,device_category: device_category.toUpperCase(),os_name: 'Windows',os_version: '10.0',time_zone: tz,browser_name: 'Chrome',browser_version: '125.0.0.0',device_make: '',device_model: '',enable_safety_mode: enable_safety_mode};if(!generate_session_locally){try{constsw_session_data=awaitthis.#getSessionData(session_args,fetch);api_key=sw_session_data.api_key;api_version=sw_session_data.api_version;context_data=sw_session_data.context_data;}catch(error){Log.error(TAG,'Failed to retrieve session data from server. Session data generated locally will be used instead.',error);}}session_data={
api_key,
api_version,context: this.#buildContext(context_data)};if(enable_session_cache)awaitthis.#storeSession(session_data,cache);}Log.debug(TAG,'Session data:',session_data);return{ ...session_data, account_index };}staticasync #storeSession(session_data: SessionData,cache?: ICache){if(!cache)return;Log.info(TAG,'Compressing and caching session data.');constcompressed_session_data=newTextEncoder().encode(LZW.compress(JSON.stringify(session_data)));constbuffer=newArrayBuffer(4+compressed_session_data.byteLength);newDataView(buffer).setUint32(0,compressed_session_data.byteLength,true);// (Luan) XX: Leave this here for debugging purposesnewUint8Array(buffer).set(compressed_session_data,4);awaitcache.set('innertube_session_data',newUint8Array(buffer));}staticasync #getSessionData(options: SessionArgs,fetch: FetchFunction=Platform.shim.fetch): Promise<SWSessionData>{letvisitor_id=generateRandomString(11);if(options.visitor_data)visitor_id=this.#getVisitorID(options.visitor_data);consturl=newURL('/sw.js_data',Constants.URLS.YT_BASE);constres=awaitfetch(url,{headers: {'Accept-Language': options.lang||'en-US','User-Agent': getRandomUserAgent('desktop'),'Accept': '*/*','Referer': `${Constants.URLS.YT_BASE}/sw.js`,'Cookie': `PREF=tz=${options.time_zone.replace('/','.')};VISITOR_INFO1_LIVE=${visitor_id};`}});if(!res.ok)thrownewSessionError(`Failed to retrieve session data: ${res.status}`);consttext=awaitres.text();if(!text.startsWith(')]}\''))thrownewSessionError('Invalid JSPB response');constdata=JSON.parse(text.replace(/^\)\]\}'/,''));constytcfg=data[0][2];constapi_version=Constants.CLIENTS.WEB.API_VERSION;const[[device_info],api_key]=ytcfg;constconfig_info=device_info[61];constapp_install_data=config_info[config_info.length-1];constcontext_info={hl: options.lang||device_info[0],gl: options.location||device_info[2],remote_host: device_info[3],visitor_data: options.visitor_data||device_info[13],client_name: options.client_name,client_version: device_info[16],os_name: device_info[17],os_version: device_info[18],time_zone: device_info[79]||options.time_zone,device_category: options.device_category,browser_name: device_info[86],browser_version: device_info[87],device_make: device_info[11],device_model: device_info[12],app_install_data: app_install_data,enable_safety_mode: options.enable_safety_mode};return{context_data: context_info, api_key, api_version };}static #buildContext(args: ContextData){constcontext: Context={client: {hl: args.hl,gl: args.gl,remoteHost: args.remote_host,screenDensityFloat: 1,screenHeightPoints: 1440,screenPixelDensity: 1,screenWidthPoints: 2560,visitorData: args.visitor_data,clientName: args.client_name,clientVersion: args.client_version,osName: args.os_name,osVersion: args.os_version,platform: args.device_category.toUpperCase(),clientFormFactor: 'UNKNOWN_FORM_FACTOR',userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',timeZone: args.time_zone,originalUrl: Constants.URLS.YT_BASE,deviceMake: args.device_make,deviceModel: args.device_model,browserName: args.browser_name,browserVersion: args.browser_version,utcOffsetMinutes: -Math.floor((newDate()).getTimezoneOffset()),memoryTotalKbytes: '8000000',mainAppWebInfo: {graftUrl: Constants.URLS.YT_BASE,pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',isWebNativeShareAvailable: true}},user: {enableSafetyMode: args.enable_safety_mode,lockedSafetyMode: false},request: {useSsl: true,internalExperimentFlags: []}};if(args.app_install_data)context.client.configInfo={appInstallData: args.app_install_data};if(args.on_behalf_of_user)context.user.onBehalfOfUser=args.on_behalf_of_user;returncontext;}static #getVisitorID(visitor_data: string){constdecoded_visitor_data=Proto.decodeVisitorData(visitor_data);returndecoded_visitor_data.id;}asyncsignIn(credentials?: OAuth2Tokens): Promise<void>{returnnewPromise(async(resolve,reject)=>{consterror_handler: OAuth2AuthErrorEventHandler=(err)=>reject(err);this.once('auth-error',error_handler);this.once('auth',()=>{this.off('auth-error',error_handler);this.logged_in=true;resolve();});try{awaitthis.oauth.init(credentials);}catch(err){reject(err);}});}/** * Signs out of the current account and revokes the credentials. */asyncsignOut(): Promise<Response|undefined>{if(!this.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constresponse=awaitthis.oauth.revokeCredentials();this.logged_in=false;returnresponse;}getclient_version(): string{returnthis.context.client.clientVersion;}getclient_name(): string{returnthis.context.client.clientName;}getlang(): string{returnthis.context.client.hl;}}
import{Parser}from'../../parser/index.js';import{Channel,HomeFeed,Search,VideoInfo}from'../../parser/ytkids/index.js';import{InnertubeError,generateRandomString}from'../../utils/Utils.js';importKidsBlocklistPickerItemfrom'../../parser/classes/ytkids/KidsBlocklistPickerItem.js';import{BrowseEndpoint,NextEndpoint,PlayerEndpoint,SearchEndpoint}from'../endpoints/index.js';import{BlocklistPickerEndpoint}from'../endpoints/kids/index.js';importtype{Session,ApiResponse}from'../index.js';exportdefaultclassKids{
#session: Session;constructor(session: Session){this.#session =session;}/** * Searches the given query. * @param query - The query. */asyncsearch(query: string): Promise<Search>{constresponse=awaitthis.#session.actions.execute(SearchEndpoint.PATH,SearchEndpoint.build({client: 'YTKIDS', query }));returnnewSearch(this.#session.actions,response);}/** * Retrieves video info. * @param video_id - The video id. */asyncgetInfo(video_id: string): Promise<VideoInfo>{constplayer_payload=PlayerEndpoint.build({sts: this.#session.player?.sts,client: 'YTKIDS',
video_id
});constnext_payload=NextEndpoint.build({
video_id,client: 'YTKIDS'});constplayer_response=this.#session.actions.execute(PlayerEndpoint.PATH,player_payload);constnext_response=this.#session.actions.execute(NextEndpoint.PATH,next_payload);constresponse=awaitPromise.all([player_response,next_response]);constcpn=generateRandomString(16);returnnewVideoInfo(response,this.#session.actions,cpn);}/** * Retrieves the contents of the given channel. * @param channel_id - The channel id. */asyncgetChannel(channel_id: string): Promise<Channel>{constresponse=awaitthis.#session.actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: channel_id,client: 'YTKIDS'}));returnnewChannel(this.#session.actions,response);}/** * Retrieves the home feed. */asyncgetHomeFeed(): Promise<HomeFeed>{constresponse=awaitthis.#session.actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: 'FEkids_home',client: 'YTKIDS'}));returnnewHomeFeed(this.#session.actions,response);}/** * Retrieves the list of supervised accounts that the signed-in user has * access to, and blocks the given channel for each of them. * @param channel_id - The channel id to block. * @returns A list of API responses. */asyncblockChannel(channel_id: string): Promise<ApiResponse[]>{if(!this.#session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constblocklist_payload=BlocklistPickerEndpoint.build({channel_id: channel_id});constresponse=awaitthis.#session.actions.execute(BlocklistPickerEndpoint.PATH,blocklist_payload);constpopup=response.data.command.confirmDialogEndpoint;constpopup_fragment={contents: popup.content,engagementPanels: []};constkid_picker=Parser.parseResponse(popup_fragment);constkids=kid_picker.contents_memo?.getType(KidsBlocklistPickerItem);if(!kids)thrownewInnertubeError('Could not find any kids profiles or supervised accounts.');// Iterate through the kids and block the channel if not already blocked.constresponses: ApiResponse[]=[];for(constkidofkids){if(!kid.block_button?.is_toggled){kid.setActions(this.#session.actions);// Block channel and add to the response list.responses.push(awaitkid.blockChannel());}}returnresponses;}}
importtype{Actions,ApiResponse}from'../index.js';importAccountInfofrom'../../parser/youtube/AccountInfo.js';importAnalyticsfrom'../../parser/youtube/Analytics.js';importSettingsfrom'../../parser/youtube/Settings.js';importTimeWatchedfrom'../../parser/youtube/TimeWatched.js';import*asProtofrom'../../proto/index.js';import{InnertubeError}from'../../utils/Utils.js';import{Account,BrowseEndpoint,Channel}from'../endpoints/index.js';exportdefaultclassAccountManager{
#actions: Actions;channel: {editName: (new_name: string)=>Promise<ApiResponse>;editDescription: (new_description: string)=>Promise<ApiResponse>;getBasicAnalytics: ()=>Promise<Analytics>;};constructor(actions: Actions){this.#actions =actions;this.channel={/** * Edits channel name. * @param new_name - The new channel name. */editName: (new_name: string)=>{if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');returnthis.#actions.execute(Channel.EditNameEndpoint.PATH,Channel.EditNameEndpoint.build({given_name: new_name}));},/** * Edits channel description. * @param new_description - The new description. */editDescription: (new_description: string)=>{if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');returnthis.#actions.execute(Channel.EditDescriptionEndpoint.PATH,Channel.EditDescriptionEndpoint.build({given_description: new_description}));},/** * Retrieves basic channel analytics. */getBasicAnalytics: ()=>this.getAnalytics()};}/** * Retrieves channel info. */asyncgetInfo(): Promise<AccountInfo>{if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constresponse=awaitthis.#actions.execute(Account.AccountListEndpoint.PATH,Account.AccountListEndpoint.build());returnnewAccountInfo(response);}/** * Retrieves time watched statistics. */asyncgetTimeWatched(): Promise<TimeWatched>{constresponse=awaitthis.#actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: 'SPtime_watched',client: 'ANDROID'}));returnnewTimeWatched(response);}/** * Opens YouTube settings. */asyncgetSettings(): Promise<Settings>{constresponse=awaitthis.#actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: 'SPaccount_overview'}));returnnewSettings(this.#actions,response);}/** * Retrieves basic channel analytics. */asyncgetAnalytics(): Promise<Analytics>{constinfo=awaitthis.getInfo();constresponse=awaitthis.#actions.execute(BrowseEndpoint.PATH,BrowseEndpoint.build({browse_id: 'FEanalytics_screen',params: Proto.encodeChannelAnalyticsParams(info.footers?.endpoint.payload.browseId),client: 'ANDROID'}));returnnewAnalytics(response);}}
import*asProtofrom'../../proto/index.js';import{throwIfMissing}from'../../utils/Utils.js';import{LikeEndpoint,DislikeEndpoint,RemoveLikeEndpoint}from'../endpoints/like/index.js';import{SubscribeEndpoint,UnsubscribeEndpoint}from'../endpoints/subscription/index.js';import{CreateCommentEndpoint,PerformCommentActionEndpoint}from'../endpoints/comment/index.js';import{ModifyChannelPreferenceEndpoint}from'../endpoints/notification/index.js';importtype{Actions,ApiResponse}from'../index.js';exportdefaultclassInteractionManager{
#actions: Actions;constructor(actions: Actions){this.#actions =actions;}/** * Likes a given video. * @param video_id - The video ID */asynclike(video_id: string): Promise<ApiResponse>{throwIfMissing({ video_id });if(!this.#actions.session.logged_in)thrownewError('You must be signed in to perform this operation.');constaction=awaitthis.#actions.execute(LikeEndpoint.PATH,LikeEndpoint.build({client: 'ANDROID',target: { video_id }}));returnaction;}/** * Dislikes a given video. * @param video_id - The video ID */asyncdislike(video_id: string): Promise<ApiResponse>{throwIfMissing({ video_id });if(!this.#actions.session.logged_in)thrownewError('You must be signed in to perform this operation.');constaction=awaitthis.#actions.execute(DislikeEndpoint.PATH,DislikeEndpoint.build({client: 'ANDROID',target: { video_id }}));returnaction;}/** * Removes a like/dislike. * @param video_id - The video ID */asyncremoveRating(video_id: string): Promise<ApiResponse>{throwIfMissing({ video_id });if(!this.#actions.session.logged_in)thrownewError('You must be signed in to perform this operation.');constaction=awaitthis.#actions.execute(RemoveLikeEndpoint.PATH,RemoveLikeEndpoint.build({client: 'ANDROID',target: { video_id }}));returnaction;}/** * Subscribes to a given channel. * @param channel_id - The channel ID */asyncsubscribe(channel_id: string): Promise<ApiResponse>{throwIfMissing({ channel_id });if(!this.#actions.session.logged_in)thrownewError('You must be signed in to perform this operation.');constaction=awaitthis.#actions.execute(SubscribeEndpoint.PATH,SubscribeEndpoint.build({client: 'ANDROID',channel_ids: [channel_id],params: 'EgIIAhgA'}));returnaction;}/** * Unsubscribes from a given channel. * @param channel_id - The channel ID */asyncunsubscribe(channel_id: string): Promise<ApiResponse>{throwIfMissing({ channel_id });if(!this.#actions.session.logged_in)thrownewError('You must be signed in to perform this operation.');constaction=awaitthis.#actions.execute(UnsubscribeEndpoint.PATH,UnsubscribeEndpoint.build({client: 'ANDROID',channel_ids: [channel_id],params: 'CgIIAhgA'}));returnaction;}/** * Posts a comment on a given video. * @param video_id - The video ID * @param text - The comment text */asynccomment(video_id: string,text: string): Promise<ApiResponse>{throwIfMissing({ video_id, text });if(!this.#actions.session.logged_in)thrownewError('You must be signed in to perform this operation.');constaction=awaitthis.#actions.execute(CreateCommentEndpoint.PATH,CreateCommentEndpoint.build({comment_text: text,create_comment_params: Proto.encodeCommentParams(video_id),client: 'ANDROID'}));returnaction;}/** * Translates a given text using YouTube's comment translate feature. * * @param target_language - an ISO language code * @param args - optional arguments */asynctranslate(text: string,target_language: string,args: {video_id?: string;comment_id?: string;}={}){throwIfMissing({ text, target_language });consttarget_action=Proto.encodeCommentActionParams(22,{ text, target_language, ...args});constresponse=awaitthis.#actions.execute(PerformCommentActionEndpoint.PATH,PerformCommentActionEndpoint.build({client: 'ANDROID',actions: [target_action]}));constmutation=response.data.frameworkUpdates.entityBatchUpdate.mutations[0].payload.commentEntityPayload;return{success: response.success,status_code: response.status_code,translated_content: mutation.translatedContent.content,data: response.data};}/** * Changes notification preferences for a given channel. * Only works with channels you are subscribed to. * @param channel_id - The channel ID. * @param type - The notification type. */asyncsetNotificationPreferences(channel_id: string,type: 'PERSONALIZED'|'ALL'|'NONE'): Promise<ApiResponse>{throwIfMissing({ channel_id, type });if(!this.#actions.session.logged_in)thrownewError('You must be signed in to perform this operation.');constpref_types={PERSONALIZED: 1,ALL: 2,NONE: 3};if(!Object.keys(pref_types).includes(type.toUpperCase()))thrownewError(`Invalid notification preference type: ${type}`);constaction=awaitthis.#actions.execute(ModifyChannelPreferenceEndpoint.PATH,ModifyChannelPreferenceEndpoint.build({client: 'WEB',params: Proto.encodeNotificationPref(channel_id,pref_types[type.toUpperCase()askeyoftypeofpref_types])}));returnaction;}}
import{InnertubeError,throwIfMissing}from'../../utils/Utils.js';import{EditPlaylistEndpoint}from'../endpoints/browse/index.js';import{BrowseEndpoint}from'../endpoints/index.js';import{CreateEndpoint,DeleteEndpoint}from'../endpoints/playlist/index.js';importPlaylistfrom'../../parser/youtube/Playlist.js';importtype{Actions}from'../index.js';importtype{Feed}from'../mixins/index.js';importtype{EditPlaylistEndpointOptions}from'../../types/index.js';exportdefaultclassPlaylistManager{
#actions: Actions;constructor(actions: Actions){this.#actions =actions;}/** * Creates a playlist. * @param title - The title of the playlist. * @param video_ids - An array of video IDs to add to the playlist. */asynccreate(title: string,video_ids: string[]): Promise<{success: boolean;status_code: number;playlist_id?: string;data: any}>{throwIfMissing({ title, video_ids });if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constresponse=awaitthis.#actions.execute(CreateEndpoint.PATH,CreateEndpoint.build({ids: video_ids,
title
}));return{success: response.success,status_code: response.status_code,playlist_id: response.data.playlistId,data: response.data};}/** * Deletes a given playlist. * @param playlist_id - The playlist ID. */asyncdelete(playlist_id: string): Promise<{playlist_id: string;success: boolean;status_code: number;data: any}>{throwIfMissing({ playlist_id });if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constresponse=awaitthis.#actions.execute(DeleteEndpoint.PATH,DeleteEndpoint.build({
playlist_id
}));return{
playlist_id,success: response.success,status_code: response.status_code,data: response.data};}/** * Adds videos to a given playlist. * @param playlist_id - The playlist ID. * @param video_ids - An array of video IDs to add to the playlist. */asyncaddVideos(playlist_id: string,video_ids: string[]): Promise<{playlist_id: string;action_result: any}>{throwIfMissing({ playlist_id, video_ids });if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constresponse=awaitthis.#actions.execute(EditPlaylistEndpoint.PATH,EditPlaylistEndpoint.build({actions: video_ids.map((id)=>({action: 'ACTION_ADD_VIDEO',added_video_id: id})),
playlist_id
}));return{
playlist_id,action_result: response.data.actions// TODO: implement actions in the parser};}/** * Removes videos from a given playlist. * @param playlist_id - The playlist ID. * @param video_ids - An array of video IDs to remove from the playlist. * @param use_set_video_ids - Option to remove videos using set video IDs. */asyncremoveVideos(playlist_id: string,video_ids: string[],use_set_video_ids=false): Promise<{playlist_id: string;action_result: any}>{throwIfMissing({ playlist_id, video_ids });if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constinfo=awaitthis.#actions.execute(BrowseEndpoint.PATH,{ ...BrowseEndpoint.build({browse_id: `VL${playlist_id}`}),parse: true});constplaylist=newPlaylist(this.#actions,info,true);if(!playlist.info.is_editable)thrownewInnertubeError('This playlist cannot be edited.',playlist_id);constpayload: EditPlaylistEndpointOptions={ playlist_id,actions: []};constgetSetVideoIds=async(pl: Feed): Promise<void>=>{constkey_id=use_set_video_ids ? 'set_video_id' : 'id';constvideos=pl.videos.filter((video)=>video_ids.includes(video.key(key_id).string()));videos.forEach((video)=>payload.actions.push({action: 'ACTION_REMOVE_VIDEO',set_video_id: video.key('set_video_id').string()}));if(payload.actions.length<video_ids.length){constnext=awaitpl.getContinuation();returngetSetVideoIds(next);}};awaitgetSetVideoIds(playlist);if(!payload.actions.length)thrownewInnertubeError('Given video ids were not found in this playlist.',video_ids);constresponse=awaitthis.#actions.execute(EditPlaylistEndpoint.PATH,EditPlaylistEndpoint.build(payload));return{
playlist_id,action_result: response.data.actions// TODO: implement actions in the parser};}/** * Moves a video to a new position within a given playlist. * @param playlist_id - The playlist ID. * @param moved_video_id - The video ID to move. * @param predecessor_video_id - The video ID to move the moved video before. */asyncmoveVideo(playlist_id: string,moved_video_id: string,predecessor_video_id: string): Promise<{playlist_id: string;action_result: any;}>{throwIfMissing({ playlist_id, moved_video_id, predecessor_video_id });if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constinfo=awaitthis.#actions.execute(BrowseEndpoint.PATH,{ ...BrowseEndpoint.build({browse_id: `VL${playlist_id}`}),parse: true});constplaylist=newPlaylist(this.#actions,info,true);if(!playlist.info.is_editable)thrownewInnertubeError('This playlist cannot be edited.',playlist_id);constpayload: EditPlaylistEndpointOptions={ playlist_id,actions: []};letset_video_id_0: string|undefined,set_video_id_1: string|undefined;constgetSetVideoIds=async(pl: Feed): Promise<void>=>{constvideo_0=pl.videos.find((video)=>moved_video_id===video.key('id').string());constvideo_1=pl.videos.find((video)=>predecessor_video_id===video.key('id').string());set_video_id_0=set_video_id_0||video_0?.key('set_video_id').string();set_video_id_1=set_video_id_1||video_1?.key('set_video_id').string();if(!set_video_id_0||!set_video_id_1){constnext=awaitpl.getContinuation();returngetSetVideoIds(next);}};awaitgetSetVideoIds(playlist);payload.actions.push({action: 'ACTION_MOVE_VIDEO_AFTER',set_video_id: set_video_id_0,moved_set_video_id_predecessor: set_video_id_1});constresponse=awaitthis.#actions.execute(EditPlaylistEndpoint.PATH,EditPlaylistEndpoint.build(payload));return{
playlist_id,action_result: response.data.actions// TODO: implement actions in the parser};}/** * Sets the name (title) for the given playlist. * @param playlist_id - The playlist ID. * @param name - The name / title to use for the playlist. */asyncsetName(playlist_id: string,name: string): Promise<{playlist_id: string;action_result: any;}>{throwIfMissing({ playlist_id, name });if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constpayload: EditPlaylistEndpointOptions={ playlist_id,actions: []};payload.actions.push({action: 'ACTION_SET_PLAYLIST_NAME',playlist_name: name});constresponse=awaitthis.#actions.execute(EditPlaylistEndpoint.PATH,EditPlaylistEndpoint.build(payload));return{
playlist_id,action_result: response.data.actions};}/** * Sets the description for the given playlist. * @param playlist_id - The playlist ID. * @param description - The description to use for the playlist. */asyncsetDescription(playlist_id: string,description: string): Promise<{playlist_id: string;action_result: any;}>{throwIfMissing({ playlist_id, description });if(!this.#actions.session.logged_in)thrownewInnertubeError('You must be signed in to perform this operation.');constpayload: EditPlaylistEndpointOptions={ playlist_id,actions: []};payload.actions.push({action: 'ACTION_SET_PLAYLIST_DESCRIPTION',playlist_description: description});constresponse=awaitthis.#actions.execute(EditPlaylistEndpoint.PATH,EditPlaylistEndpoint.build(payload));return{
playlist_id,action_result: response.data.actions};}}
import{Parser,ReloadContinuationItemsCommand}from'../../parser/index.js';import{concatMemos,InnertubeError}from'../../utils/Utils.js';importBackstagePostfrom'../../parser/classes/BackstagePost.js';importSharedPostfrom'../../parser/classes/SharedPost.js';importChannelfrom'../../parser/classes/Channel.js';importCompactVideofrom'../../parser/classes/CompactVideo.js';importGridChannelfrom'../../parser/classes/GridChannel.js';importGridPlaylistfrom'../../parser/classes/GridPlaylist.js';importGridVideofrom'../../parser/classes/GridVideo.js';importLockupViewfrom'../../parser/classes/LockupView.js';importPlaylistfrom'../../parser/classes/Playlist.js';importPlaylistPanelVideofrom'../../parser/classes/PlaylistPanelVideo.js';importPlaylistVideofrom'../../parser/classes/PlaylistVideo.js';importPostfrom'../../parser/classes/Post.js';importReelItemfrom'../../parser/classes/ReelItem.js';importReelShelffrom'../../parser/classes/ReelShelf.js';importRichShelffrom'../../parser/classes/RichShelf.js';importShelffrom'../../parser/classes/Shelf.js';importTabfrom'../../parser/classes/Tab.js';importVideofrom'../../parser/classes/Video.js';importAppendContinuationItemsActionfrom'../../parser/classes/actions/AppendContinuationItemsAction.js';importContinuationItemfrom'../../parser/classes/ContinuationItem.js';importTwoColumnBrowseResultsfrom'../../parser/classes/TwoColumnBrowseResults.js';importTwoColumnSearchResultsfrom'../../parser/classes/TwoColumnSearchResults.js';importWatchCardCompactVideofrom'../../parser/classes/WatchCardCompactVideo.js';importtype{ApiResponse,Actions}from'../index.js';importtype{Memo,ObservedArray,SuperParsedResult,YTNode}from'../../parser/helpers.js';importtypeMusicQueuefrom'../../parser/classes/MusicQueue.js';importtypeRichGridfrom'../../parser/classes/RichGrid.js';importtypeSectionListfrom'../../parser/classes/SectionList.js';importtype{IParsedResponse}from'../../parser/types/index.js';exportdefaultclassFeed<TextendsIParsedResponse=IParsedResponse>{
#page: T;
#continuation?: ObservedArray<ContinuationItem>;
#actions: Actions;
#memo: Memo;constructor(actions: Actions,response: ApiResponse|IParsedResponse,already_parsed=false){if(this.#isParsed(response)||already_parsed){this.#page =responseasT;}else{this.#page =Parser.parseResponse<T>(response.data);}constmemo=concatMemos(...[this.#page.contents_memo,this.#page.continuation_contents_memo,this.#page.on_response_received_commands_memo,this.#page.on_response_received_endpoints_memo,this.#page.on_response_received_actions_memo,this.#page.sidebar_memo,this.#page.header_memo]);if(!memo)thrownewInnertubeError('No memo found in feed');this.#memo =memo;this.#actions =actions;}
#isParsed(response: IParsedResponse|ApiResponse): response is IParsedResponse{return!('data'inresponse);}/** * Get all videos on a given page via memo */staticgetVideosFromMemo(memo: Memo){returnmemo.getType(Video,GridVideo,ReelItem,CompactVideo,PlaylistVideo,PlaylistPanelVideo,WatchCardCompactVideo);}/** * Get all playlists on a given page via memo */staticgetPlaylistsFromMemo(memo: Memo){constplaylists: ObservedArray<Playlist|GridPlaylist|LockupView>=memo.getType(Playlist,GridPlaylist);constlockup_views=memo.getType(LockupView).filter((lockup)=>{return['PLAYLIST','ALBUM','PODCAST'].includes(lockup.content_type);});if(lockup_views.length>0){playlists.push(...lockup_views);}returnplaylists;}/** * Get all the videos in the feed */getvideos(){returnFeed.getVideosFromMemo(this.#memo);}/** * Get all the community posts in the feed */getposts(){returnthis.#memo.getType(BackstagePost,Post,SharedPost);}/** * Get all the channels in the feed */getchannels(){returnthis.#memo.getType(Channel,GridChannel);}/** * Get all playlists in the feed */getplaylists(){returnFeed.getPlaylistsFromMemo(this.#memo);}getmemo(){returnthis.#memo;}/** * Returns contents from the page. */getpage_contents(): SectionList|MusicQueue|RichGrid|ReloadContinuationItemsCommand{consttab_content=this.#memo.getType(Tab)?.first().content;constreload_continuation_items=this.#memo.getType(ReloadContinuationItemsCommand).first();constappend_continuation_items=this.#memo.getType(AppendContinuationItemsAction).first();returntab_content||reload_continuation_items||append_continuation_items;}/** * Returns all segments/sections from the page. */getshelves(){returnthis.#memo.getType(Shelf,RichShelf,ReelShelf);}/** * Finds shelf by title. */getShelf(title: string){returnthis.shelves.get({ title });}/** * Returns secondary contents from the page. */getsecondary_contents(): SuperParsedResult<YTNode>|undefined{if(!this.#page.contents?.is_node)returnundefined;constnode=this.#page.contents?.item();if(!node.is(TwoColumnBrowseResults,TwoColumnSearchResults))returnundefined;returnnode.secondary_contents;}getactions(): Actions{returnthis.#actions;}/** * Get the original page data */getpage(): T{returnthis.#page;}/** * Checks if the feed has continuation. */gethas_continuation(): boolean{returnthis.#getBodyContinuations().length>0;}/** * Retrieves continuation data as it is. */asyncgetContinuationData(): Promise<T|undefined>{if(this.#continuation){if(this.#continuation.length===0)thrownewInnertubeError('There are no continuations.');constresponse=awaitthis.#continuation[0].endpoint.call<T>(this.#actions,{parse: true});returnresponse;}this.#continuation =this.#getBodyContinuations();if(this.#continuation)returnthis.getContinuationData();}/** * Retrieves next batch of contents and returns a new {@link Feed} object. */asyncgetContinuation(): Promise<Feed<T>>{constcontinuation_data=awaitthis.getContinuationData();if(!continuation_data)thrownewInnertubeError('Could not get continuation data');returnnewFeed<T>(this.actions,continuation_data,true);}
#getBodyContinuations(): ObservedArray<ContinuationItem>{if(this.#page.header_memo){constheader_continuations=this.#page.header_memo.getType(ContinuationItem);returnthis.#memo.getType(ContinuationItem).filter((continuation)=>!header_continuations.includes(continuation))asObservedArray<ContinuationItem>;}returnthis.#memo.getType(ContinuationItem);}}
importFeedfrom'./Feed.js';importChipCloudChipfrom'../../parser/classes/ChipCloudChip.js';importFeedFilterChipBarfrom'../../parser/classes/FeedFilterChipBar.js';import{InnertubeError}from'../../utils/Utils.js';importtype{ObservedArray}from'../../parser/helpers.js';importtype{IParsedResponse}from'../../parser/types/index.js';importtype{ApiResponse,Actions}from'../index.js';exportdefaultclassFilterableFeed<TextendsIParsedResponse>extendsFeed<T>{
#chips?: ObservedArray<ChipCloudChip>;constructor(actions: Actions,data: ApiResponse|T,already_parsed=false){super(actions,data,already_parsed);}/** * Returns the filter chips. */getfilter_chips(): ObservedArray<ChipCloudChip>{if(this.#chips)returnthis.#chips ||[];if(this.memo.getType(FeedFilterChipBar)?.length>1)thrownewInnertubeError('There are too many feed filter chipbars, you\'ll need to find the correct one yourself in this.page');if(this.memo.getType(FeedFilterChipBar)?.length===0)thrownewInnertubeError('There are no feed filter chipbars');this.#chips =this.memo.getType(ChipCloudChip);returnthis.#chips ||[];}/** * Returns available filters. */getfilters(): string[]{returnthis.filter_chips.map((chip)=>chip.text.toString())||[];}/** * Applies given filter and returns a new {@link Feed} object. */asyncgetFilteredFeed(filter: string|ChipCloudChip): Promise<Feed<T>>{lettarget_filter: ChipCloudChip|undefined;if(typeoffilter==='string'){if(!this.filters.includes(filter))thrownewInnertubeError('Filter not found',{available_filters: this.filters});target_filter=this.filter_chips.find((chip)=>chip.text.toString()===filter);}elseif(filter.type==='ChipCloudChip'){target_filter=filter;}else{thrownewInnertubeError('Invalid filter');}if(!target_filter)thrownewInnertubeError('Filter not found');if(target_filter.is_selected)returnthis;constresponse=awaittarget_filter.endpoint?.call(this.actions,{parse: true});if(!response)thrownewInnertubeError('Failed to get filtered feed');returnnewFeed(this.actions,response,true);}}
import{Constants,FormatUtils}from'../../utils/index.js';import{InnertubeError}from'../../utils/Utils.js';import{getStreamingInfo}from'../../utils/StreamingInfo.js';import{Parser}from'../../parser/index.js';import{TranscriptInfo}from'../../parser/youtube/index.js';importContinuationItemfrom'../../parser/classes/ContinuationItem.js';importPlayerMicroformatfrom'../../parser/classes/PlayerMicroformat.js';importMicroformatDatafrom'../../parser/classes/MicroformatData.js';importtype{ApiResponse,Actions}from'../index.js';importtype{INextResponse,IPlayabilityStatus,IPlaybackTracking,IPlayerConfig,IPlayerResponse,IStreamingData}from'../../parser/index.js';importtype{DownloadOptions,FormatFilter,FormatOptions,URLTransformer}from'../../types/FormatUtils.js';importtypeFormatfrom'../../parser/classes/misc/Format.js';importtype{DashOptions}from'../../types/DashOptions.js';importtype{ObservedArray}from'../../parser/helpers.js';importtypeCardCollectionfrom'../../parser/classes/CardCollection.js';importtypeEndscreenfrom'../../parser/classes/Endscreen.js';importtypePlayerAnnotationsExpandedfrom'../../parser/classes/PlayerAnnotationsExpanded.js';importtypePlayerCaptionsTracklistfrom'../../parser/classes/PlayerCaptionsTracklist.js';importtypePlayerLiveStoryboardSpecfrom'../../parser/classes/PlayerLiveStoryboardSpec.js';importtypePlayerStoryboardSpecfrom'../../parser/classes/PlayerStoryboardSpec.js';exportdefaultclassMediaInfo{
#page: [IPlayerResponse,INextResponse?];
#actions: Actions;
#cpn: string;
#playback_tracking?: IPlaybackTracking;basic_info;annotations?: ObservedArray<PlayerAnnotationsExpanded>;storyboards?: PlayerStoryboardSpec|PlayerLiveStoryboardSpec;endscreen?: Endscreen;captions?: PlayerCaptionsTracklist;cards?: CardCollection;streaming_data?: IStreamingData;playability_status?: IPlayabilityStatus;player_config?: IPlayerConfig;constructor(data: [ApiResponse,ApiResponse?],actions: Actions,cpn: string){this.#actions =actions;constinfo=Parser.parseResponse<IPlayerResponse>(data[0].data.playerResponse ? data[0].data.playerResponse : data[0].data);constnext=data[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;this.#page =[info,next];this.#cpn =cpn;if(info.playability_status?.status==='ERROR')thrownewInnertubeError('This video is unavailable',info.playability_status);if(info.microformat&&!info.microformat?.is(PlayerMicroformat,MicroformatData))thrownewInnertubeError('Unsupported microformat',info.microformat);this.basic_info={// This type is inferred so no need for an explicit type
...info.video_details,/** * Microformat is a bit redundant, so only * a few things there are interesting to us. */
...{embed: info.microformat?.is(PlayerMicroformat) ? info.microformat?.embed : null,channel: info.microformat?.is(PlayerMicroformat) ? info.microformat?.channel : null,is_unlisted: info.microformat?.is_unlisted,is_family_safe: info.microformat?.is_family_safe,category: info.microformat?.is(PlayerMicroformat) ? info.microformat?.category : null,has_ypc_metadata: info.microformat?.is(PlayerMicroformat) ? info.microformat?.has_ypc_metadata : null,start_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.start_timestamp : null,end_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.end_timestamp : null,view_count: info.microformat?.is(PlayerMicroformat)&&isNaN(info.video_details?.view_countasnumber) ? info.microformat.view_count : info.video_details?.view_count,url_canonical: info.microformat?.is(MicroformatData) ? info.microformat?.url_canonical : null,tags: info.microformat?.is(MicroformatData) ? info.microformat?.tags : null},like_count: undefinedasnumber|undefined,is_liked: undefinedasboolean|undefined,is_disliked: undefinedasboolean|undefined};this.annotations=info.annotations;this.storyboards=info.storyboards;this.endscreen=info.endscreen;this.captions=info.captions;this.cards=info.cards;this.streaming_data=info.streaming_data;this.playability_status=info.playability_status;this.player_config=info.player_config;this.#playback_tracking =info.playback_tracking;}/** * Generates a DASH manifest from the streaming data. * @param url_transformer - Function to transform the URLs. * @param format_filter - Function to filter the formats. * @param options - Additional options to customise the manifest generation * @returns DASH manifest */asynctoDash(url_transformer?: URLTransformer,format_filter?: FormatFilter,options: DashOptions={include_thumbnails: false}): Promise<string>{constplayer_response=this.#page[0];if(player_response.video_details&&(player_response.video_details.is_live)){thrownewInnertubeError('Generating DASH manifests for live videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.');}letstoryboards;letcaptions;if(options.include_thumbnails&&player_response.storyboards){storyboards=player_response.storyboards;}if(typeofoptions.captions_format==='string'&&player_response.captions?.caption_tracks){captions=player_response.captions.caption_tracks;}returnFormatUtils.toDash(this.streaming_data,this.page[0].video_details?.is_post_live_dvr,url_transformer,format_filter,this.#cpn,this.#actions.session.player,this.#actions,storyboards,captions,options);}/** * Get a cleaned up representation of the adaptive_formats */getStreamingInfo(url_transformer?: URLTransformer,format_filter?: FormatFilter){returngetStreamingInfo(this.streaming_data,this.page[0].video_details?.is_post_live_dvr,url_transformer,format_filter,this.cpn,this.#actions.session.player,this.#actions,this.#page[0].storyboards ? this.#page[0].storyboards : undefined);}/** * Selects the format that best matches the given options. * @param options - Options */chooseFormat(options: FormatOptions): Format{returnFormatUtils.chooseFormat(options,this.streaming_data);}/** * Downloads the video. * @param options - Download options. */asyncdownload(options: DownloadOptions={}): Promise<ReadableStream<Uint8Array>>{constplayer_response=this.#page[0];if(player_response.video_details&&(player_response.video_details.is_live||player_response.video_details.is_post_live_dvr)){thrownewInnertubeError('Downloading is not supported for live and Post-Live-DVR videos, as they are split up into 5 second segments that are individual files, which require using a tool such as ffmpeg to stitch them together, so they cannot be returned in a single stream.');}returnFormatUtils.download(options,this.#actions,this.playability_status,this.streaming_data,this.#actions.session.player,this.cpn);}/** * Retrieves the video's transcript. * @param video_id - The video id. */asyncgetTranscript(): Promise<TranscriptInfo>{constnext_response=this.page[1];if(!next_response)thrownewInnertubeError('Cannot get transcript from basic video info.');if(!next_response.engagement_panels)thrownewInnertubeError('Engagement panels not found. Video likely has no transcript.');consttranscript_panel=next_response.engagement_panels.get({panel_identifier: 'engagement-panel-searchable-transcript'});if(!transcript_panel)thrownewInnertubeError('Transcript panel not found. Video likely has no transcript.');consttranscript_continuation=transcript_panel.content?.as(ContinuationItem);if(!transcript_continuation)thrownewInnertubeError('Transcript continuation not found.');constresponse=awaittranscript_continuation.endpoint.call(this.actions);returnnewTranscriptInfo(this.actions,response);}/** * Adds video to the watch history. */asyncaddToWatchHistory(client_name=Constants.CLIENTS.WEB.NAME,client_version=Constants.CLIENTS.WEB.VERSION,replacement='https://www.'): Promise<Response>{if(!this.#playback_tracking)thrownewInnertubeError('Playback tracking not available');consturl_params={cpn: this.#cpn,fmt: 251,rtn: 0,rt: 0};consturl=this.#playback_tracking.videostats_playback_url.replace('https://s.',replacement);constresponse=awaitthis.#actions.stats(url,{
client_name,
client_version
},url_params);returnresponse;}/** * Actions instance. */getactions(): Actions{returnthis.#actions;}/** * Content Playback Nonce. */getcpn(): string{returnthis.#cpn;}/** * Original parsed InnerTube response. */getpage(): [IPlayerResponse,INextResponse?]{returnthis.#page;}}
# Parser
The parser is responsible for sanitizing and standardizing InnerTube responses while preserving the integrity of the data.
## Table of Contents-[Parser](#parser)-[Table of Contents](#table-of-contents)-[Structure](#structure)-[Core](#core)-[Clients](#clients)-[API](#api)-[`parse(data?: RawData, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[])`](#parsedata-rawdata-requirearray-boolean-validtypes-ytnodeconstructort--ytnodeconstructort)-[`parseResponse(data: IRawResponse): T`](#parseresponsedata-irawresponse-t)-[Usage](#usage)-[ObservedArray](#observedarray)-[SuperParsedResponse](#superparsedresponse)-[YTNode](#ytnode)-[Type Casting](#type-casting)-[Accessing properties without casting](#accessing-properties-without-casting)-[Memo](#memo)-[Adding new nodes](#adding-new-nodes)-[Generating nodes at runtime](#generating-nodes-at-runtime)-[How it works](#how-it-works)## Structure### Core*[`/types`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/types) - General response types.
*[`/classes`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/classes) - InnerTube nodes.
*[`generator.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/generator.ts) - Used to generate missing nodes at runtime.
*[`helpers.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/helpers.ts) - Helper functions/classes for the parser.
*[`parser.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/parser.ts) - The core of the parser.
*[`nodes.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/nodes.ts) - Contains a list of all the InnerTube nodes, which is used to determine the appropriate node for a given renderer. It's important to note that this file is automatically generated and should not be edited manually.
*[`misc.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/misc.ts) - Miscellaneous classes. Also automatically generated.
### Clients
The parser itself is not tied to any specific client. Therefore, we have a separate folder for each client that the library supports. These folders are responsible for arranging the parsed data into a format that can be easily consumed and understood. Additionally, the underlying data is also exposed for those who wish to access it.
*[`/youtube`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube)*[`/ytmusic`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytmusic)*[`/ytkids`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytkids)## API* Parser
*[.parse](#parse)*[.parseItem](#parse)*[.parseArray](#parse)*[.parseResponse](#parseresponse)
<aname="parse"></a>
### `parse(data?: RawData, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[])`
Responsible for parsing individual nodes.
| Param | Type | Description || --- | --- | --- || data |`RawData`| The data to parse || requireArray |`?boolean`| Whether the response should be an array || validTypes |`YTNodeConstructor<T> \| YTNodeConstructor<T>[] \| undefined`| Types of `YTNode` allowed |- If `requireArray` is `true`, the response will be an `ObservedArray<YTNodes>`.
- If `validTypes` is `undefined`, the response will be an array of YTNodes.
- If `validTypes` is an array, the response will be an array of YTNodes that are of the types specified in the array.
- If `validTypes` is a single type, the response will be an array of YTNodes that are of the type specified.
If you do not specify `requireArray`, the return type of the function will not be known at runtime. Therefore, to gain access to the response, we return it wrapped in a helper, `SuperParsedResponse`.
You may use the `Parser#parseArray` and `Parser#parseItem` methods to parse the response in a deterministic way.
<aname="parseresponse"></a>
### `parseResponse(data: IRawResponse): T`
Unlike `parse`, this can be used to parse the entire response object.
| Param | Type | Description || --- | --- | --- || data |`IRawResponse`| Raw InnerTube response |## Usage### ObservedArray
You can utilize an `ObservedArray<T extends YTNode>` as a regular array, but it also offers further methods for accessing and casting values in a type-safe manner.
```ts// For example, we have a feed, and want all the videos:const feed =newObservedArray<YTNode>([...feed.contents]);
// Here, we use the filterType method to retrieve only GridVideo items from the feed.const videos =feed.filterType(GridVideo);
// `videos` is now a GridVideo[] array.// Alternatively, we can use firstOfType to retrieve the first GridVideo item from the feed.const firstVideo =feed.firstOfType(GridVideo);
// If we want to make sure that all elements in the `feed` array are of the `GridVideo` type, we can use the `as` method to cast the entire array to a `GridVideo[]` type. If the cast fails because of non-GridVideo items, an exception is thrown.const allVideos =feed.as(GridVideo);
// Note that ObservedArray provides additional methods beyond what's shown here, which we use internally. For more information, see the source code or documentation.
SuperParsedResponse
Represents a parsed response in an unknown state. Either a YTNode, an ObservedArray<YTNode>, or null. To extract the actual value, you must first assert the type and unwrap the response.
// First, parse the data and store it in `response`.constresponse=Parser.parse(data);// Check whether `response` is a YTNode.if(response.is_item){// If so, we can assert that it is a YTNode and retrieve it.constnode=response.item();}// Check whether `response` is an ObservedArray<YTNode>.if(response.is_array){// If so, we can assert that it is an ObservedArray<YTNode> and retrieve its contents as an array of YTNode objects.constnodes=response.array();}// Finally, to check if `response` is a null value, use the `is_null` getter.constis_null=response.is_null;
YTNode
All renderers returned by InnerTube are converted to this generic class and then extended for the specific renderers. This class is what allows us a type-safe way to use data returned by the InnerTube API.
Here's how to use this class to access returned data:
Type Casting
// We can cast a YTNode to a child class of YTNodeconstresults=node.as(TwoColumnSearchResults);// This will throw an error if the node is not a TwoColumnSearchResults.// Therefore, we may want to check for the type of the node before casting.if(node.is(TwoColumnSearchResults)){// We do not need to recast the node; it is already a TwoColumnSearchResults after calling is() and using it in the branch where is() returns true.constresults=node;}// Sometimes we can expect multiple types of nodes, we can just pass all possible types as params.constresults=node.as(TwoColumnSearchResults,VideoList);// The type of `results` will now be `TwoColumnSearchResults | VideoList`// Similarly, we can check if the node is of a certain type.if(node.is(TwoColumnSearchResults,VideoList)){// // Again, no casting is needed; the node is already of the correct type.constresults=node;}
Accessing properties without casting
Sometimes multiple nodes have the same properties, and we don't want to check the type of the node before accessing the property. For example, the property 'contents' is used by many node types, and we may add more in the future. As such, we want to only assert the property instead of casting to a specific type.
// Accessing a property on a node when you aren't sure if it exists.constprop=node.key("contents");// This returns the value wrapped into a `Maybe` type, which you can use to determine the type of the value.// However, this throws an error if the key doesn't exist, so we may want to check for the key before accessing it.if(node.hasKey("contents")){constprop=node.key("contents");}// We can assert the type of the value.constprop=node.key("contents");if(prop.isString()){constvalue=prop.string();}// We can do more complex assertions, like checking for instanceof.constprop=node.key("contents");if(prop.isInstanceOf(Text)){consttext=prop.instanceOf(Text);// Then use the value as the given type.text.runs.forEach(run=>{console.log(run.text);});}// There are special methods for use with the parser, such as getting the value as a YTNode.constprop=node.key("contents");if(prop.isNode()){constnode=prop.node();}// Like with YTNode, keys can also be checked for YTNode child class types.constprop=node.key("contents");if(prop.isNodeOfType(TwoColumnSearchResults)){constresults=prop.nodeOfType(TwoColumnSearchResults);}// Or we can check for multiple types of nodes.constprop=node.key("contents");if(prop.isNodeOfType([TwoColumnSearchResults,VideoList])){constresults=prop.nodeOfType<TwoColumnSearchResults|VideoList>([TwoColumnSearchResults,VideoList]);}// Sometimes an ObservedArray is returned when working with parsed data.// We also have a helper for this.constprop=node.key("contents");if(prop.isObserved()){constarray=prop.observed();// Now we can use all the ObservedArray methods as normal, such as finding nodes of a certain type.constresults=array.filterType(GridVideo);}// Other times a SuperParsedResult is returned, like when using the `Parser#parse` method.constprop=node.key("contents");if(prop.isParsed()){constresult=prop.parsed();// SuperParsedResult is another helper for type-safe access to the parsed data.// It is explained above with the `Parser#parse` method.constresults=results.array();constvideos=results.filterType(Video);}// Sometimes we just want to debug something and are not interested in finding the type.// This will, however, warn you when being used.constprop=node.key("contents");constvalue=prop.any();// Arrays are a special case, as every element may be of a different type.// The `arrayOfMaybe` method will return an array of `Maybe`s.constprop=node.key("contents");if(prop.isArray()){constarray=prop.arrayOfMaybe();// This will return `Maybe[]`.}// Or, if you don't need type safety, you can use the `array` method.constprop=node.key("contents");if(prop.isArray()){constarray=prop.array();// This will return any[].}
Memo
The Memo class is a helper class for memoizing values in the Parser#parseResponse method. It can be used to conveniently access nodes after parsing the response.
For example, if we'd like to obtain all of the videos from a search result, we can use the Memo#getType method to find them quickly without needing to traverse the entire response.
constresponse=Parser.parseResponse(data);constvideos=response.contents_memo.getType(Video);// This returns the nodes as an `ObservedArray<Video>`.
Memo extends Map<string, YTNode[]> and can be used as a regular Map if desired.
YouTube constantly updates their client, and sometimes they add new nodes to the response. The parser needs to know about these new nodes in order to parse them correctly. Once a new node is dicovered by the parser, it will attempt to generate a new node class for it.
Using the existing YTNode class, you may interact with these new nodes in a type-safe way. However, you will not be able to cast them to the node's specific type, as this requires the node to be defined at compile-time.
The current implementation recognises the following values:
Renderers
Renderer arrays
Text
Navigation endpoints
Author (does not currently detect the author thumbnails)
Thumbnails
Objects (key-value pairs)
Primatives (string, number, boolean, etc.)
This may be expanded in the future.
At runtime, these JIT-generated nodes will revalidate themselves when constructed so that when the types change, the node will be re-generated.
To access these nodes that have been generated at runtime, you may use the Parser.getParserByName(name: string) method. You may also check if a parser has been generated for a node by using the Parser.hasParser(name: string) method.
import{Parser}from"youtubei.js";// We may check if we have a parser for a node.if(Parser.hasParser('Example')){// Then retrieve it.constExample=Parser.getParserByName('Example');// We may then use the parser as normal.constexample=newExample(data);}
You may also generate your own nodes ahead of time, given you have an example of one of the nodes.
import{Generator}from"youtubei.js";// Provided you have an example of the node `Example`constexample_data={"title": {"runs": [{"text": "Example"}]}}// The first argument is the name of the class, the second is the data you have for the node.// It will return a class that extends YTNode.constExample=Generator.generateRuntimeClass('Example',example_data);// You may now use this class as you would any other node.constexample=newExample(example_data);consttitle=example.key('title').instanceof(Text).toString();
How it works
If you decompile a YouTube client and analyze it, it becomes apparent that it uses classes such as ../youtube/api/innertube/MusicItemRenderer and ../youtube/api/innertube/SectionListRenderer to parse objects from the response, map them into models, and generate the UI. The website operates similarly, but instead uses plain JSON. You can think of renderers as components in a web framework.
Our approach is similar to YouTube's: our parser goes through all the renderers and parses their inner element(s). The final result is a nicely structured JSON, it even parses navigation endpoints, which allow us to make an API call with all required parameters in one line and emulate client actions, such as clicking a button.
To illustrate the transformation we make, let's take an unstructured InnerTube response and parse it into a cleaner format:
import{Parser}from'../index.js';importAccountItemSectionHeaderfrom'./AccountItemSectionHeader.js';importNavigationEndpointfrom'./NavigationEndpoint.js';importTextfrom'./misc/Text.js';importThumbnailfrom'./misc/Thumbnail.js';import{YTNode,observe,typeObservedArray}from'../helpers.js';importtype{RawNode}from'../index.js';/** * Not a real renderer but we treat it as one to keep things organized. */exportclassAccountItemextendsYTNode{statictype='AccountItem';account_name: Text;account_photo: Thumbnail[];is_selected: boolean;is_disabled: boolean;has_channel: boolean;endpoint: NavigationEndpoint;account_byline: Text;constructor(data: RawNode){super();this.account_name=newText(data.accountName);this.account_photo=Thumbnail.fromResponse(data.accountPhoto);this.is_selected=!!data.isSelected;this.is_disabled=!!data.isDisabled;this.has_channel=!!data.hasChannel;this.endpoint=newNavigationEndpoint(data.serviceEndpoint);this.account_byline=newText(data.accountByline);}}exportdefaultclassAccountItemSectionextendsYTNode{statictype='AccountItemSection';contents: ObservedArray<AccountItem>;header: AccountItemSectionHeader|null;constructor(data: RawNode){super();this.contents=observe<AccountItem>(data.contents.map((ac: RawNode)=>newAccountItem(ac.accountItem)));this.header=Parser.parseItem(data.header,AccountItemSectionHeader);}}
import{Log}from'../../utils/index.js';import{YTNode}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';importButtonfrom'./Button.js';importNavigationEndpointfrom'./NavigationEndpoint.js';importSubscribeButtonfrom'./SubscribeButton.js';importAuthorfrom'./misc/Author.js';importTextfrom'./misc/Text.js';exportdefaultclassChannelextendsYTNode{statictype='Channel';id: string;author: Author;subscriber_count: Text;video_count: Text;long_byline: Text;short_byline: Text;endpoint: NavigationEndpoint;subscribe_button: SubscribeButton|Button|null;description_snippet: Text;constructor(data: RawNode){super();this.id=data.channelId;this.author=newAuthor({
...data.title,navigationEndpoint: data.navigationEndpoint},data.ownerBadges,data.thumbnail);// XXX: `subscriberCountText` is now the channel's handle and `videoCountText` is the subscriber count.this.subscriber_count=newText(data.subscriberCountText);this.video_count=newText(data.videoCountText);this.long_byline=newText(data.longBylineText);this.short_byline=newText(data.shortBylineText);this.endpoint=newNavigationEndpoint(data.navigationEndpoint);this.subscribe_button=Parser.parseItem(data.subscribeButton,[SubscribeButton,Button]);this.description_snippet=newText(data.descriptionSnippet);}/** * @deprecated * This will be removed in a future release. * Please use {@link Channel.subscriber_count} instead. */getsubscribers(): Text{Log.warnOnce(Channel.type,'Channel#subscribers is deprecated. Please use Channel#subscriber_count instead.');returnthis.subscriber_count;}/** * @deprecated * This will be removed in a future release. * Please use {@link Channel.video_count} instead. */getvideos(): Text{Log.warnOnce(Channel.type,'Channel#videos is deprecated. Please use Channel#video_count instead.');returnthis.video_count;}}
import{Log}from'../../utils/index.js';import{YTNode,typeObservedArray}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';importButtonfrom'./Button.js';importNavigationEndpointfrom'./NavigationEndpoint.js';importTextfrom'./misc/Text.js';importThumbnailfrom'./misc/Thumbnail.js';exportdefaultclassChannelAboutFullMetadataextendsYTNode{statictype='ChannelAboutFullMetadata';id: string;name: Text;avatar: Thumbnail[];canonical_channel_url: string;primary_links: {endpoint: NavigationEndpoint;icon: Thumbnail[];title: Text;}[];view_count: Text;joined_date: Text;description: Text;email_reveal: NavigationEndpoint;can_reveal_email: boolean;country: Text;buttons: ObservedArray<Button>;constructor(data: RawNode){super();this.id=data.channelId;this.name=newText(data.title);this.avatar=Thumbnail.fromResponse(data.avatar);this.canonical_channel_url=data.canonicalChannelUrl;this.primary_links=data.primaryLinks?.map((link: any)=>({endpoint: newNavigationEndpoint(link.navigationEndpoint),icon: Thumbnail.fromResponse(link.icon),title: newText(link.title)}))??[];this.view_count=newText(data.viewCountText);this.joined_date=newText(data.joinedDateText);this.description=newText(data.description);this.email_reveal=newNavigationEndpoint(data.onBusinessEmailRevealClickCommand);this.can_reveal_email=!data.signInForBusinessEmail;this.country=newText(data.country);this.buttons=Parser.parseArray(data.actionButtons,Button);}/** * @deprecated * This will be removed in a future release. * Please use {@link Channel.view_count} instead. */getviews(){Log.warnOnce(ChannelAboutFullMetadata.type,'ChannelAboutFullMetadata#views is deprecated. Please use ChannelAboutFullMetadata#view_count instead.');returnthis.view_count;}/** * @deprecated * This will be removed in a future release. * Please use {@link Channel.joined_date} instead. */getjoined(): Text{Log.warnOnce(ChannelAboutFullMetadata.type,'ChannelAboutFullMetadata#joined is deprecated. Please use ChannelAboutFullMetadata#joined_date instead.');returnthis.joined_date;}}
import{YTNode,observe,typeObservedArray}from'../helpers.js';importtype{RawNode}from'../index.js';importNavigationEndpointfrom'./NavigationEndpoint.js';importTextfrom'./misc/Text.js';importThumbnailfrom'./misc/Thumbnail.js';// XXX (LuanRT): This is not a real YTNode, but we treat it as one to keep things clean.exportclassHeaderLinkextendsYTNode{statictype='HeaderLink';endpoint: NavigationEndpoint;icon: Thumbnail[];title: Text;constructor(data: RawNode){super();this.endpoint=newNavigationEndpoint(data.navigationEndpoint);this.icon=Thumbnail.fromResponse(data.icon);this.title=newText(data.title);}}exportdefaultclassChannelHeaderLinksextendsYTNode{statictype='ChannelHeaderLinks';primary: ObservedArray<HeaderLink>;secondary: ObservedArray<HeaderLink>;constructor(data: RawNode){super();this.primary=observe(data.primaryLinks?.map((link: RawNode)=>newHeaderLink(link))||[]);this.secondary=observe(data.secondaryLinks?.map((link: RawNode)=>newHeaderLink(link))||[]);}}
importThumbnailfrom'./misc/Thumbnail.js';import{YTNode}from'../helpers.js';importtype{RawNode}from'../index.js';exportdefaultclassChannelMetadataextendsYTNode{statictype='ChannelMetadata';title: string;description: string;url: string;rss_url: string;vanity_channel_url: string;external_id: string;is_family_safe: boolean;keywords: string[];avatar: Thumbnail[];music_artist_name?: string;available_countries: string[];android_deep_link: string;android_appindexing_link: string;ios_appindexing_link: string;constructor(data: RawNode){super();this.title=data.title;this.description=data.description;this.url=data.channelUrl;this.rss_url=data.rssUrl;this.vanity_channel_url=data.vanityChannelUrl;this.external_id=data.externalId;this.is_family_safe=data.isFamilySafe;this.keywords=data.keywords;this.avatar=Thumbnail.fromResponse(data.avatar);// Can be an empty string sometimes, so we need the extra length checkthis.music_artist_name=typeofdata.musicArtistName==='string'&&data.musicArtistName.length>0 ? data.musicArtistName : undefined;this.available_countries=data.availableCountryCodes;this.android_deep_link=data.androidDeepLink;this.android_appindexing_link=data.androidAppindexingLink;this.ios_appindexing_link=data.iosAppindexingLink;}}
importTextfrom'./misc/Text.js';import{YTNode}from'../helpers.js';importtype{RawNode}from'../index.js';import{Log}from'../../utils/index.js';exportdefaultclassChannelVideoPlayerextendsYTNode{statictype='ChannelVideoPlayer';id: string;title: Text;description: Text;view_count: Text;published_time: Text;constructor(data: RawNode){super();this.id=data.videoId;this.title=newText(data.title);this.description=newText(data.description);this.view_count=newText(data.viewCountText);this.published_time=newText(data.publishedTimeText);}/** * @deprecated * This will be removed in a future release. * Please use {@link ChannelVideoPlayer.view_count} instead. */getviews(): Text{Log.warnOnce(ChannelVideoPlayer.type,'ChannelVideoPlayer#views is deprecated. Please use ChannelVideoPlayer#view_count instead.');returnthis.view_count;}/** * @deprecated * This will be removed in a future release. * Please use {@link ChannelVideoPlayer.published_time} instead. */getpublished(): Text{Log.warnOnce(ChannelVideoPlayer.type,'ChannelVideoPlayer#published is deprecated. Please use ChannelVideoPlayer#published_time instead.');returnthis.published_time;}}
import{Parser,typeRawNode}from'../index.js';import{typeObservedArray,YTNode}from'../helpers.js';exportdefaultclassExpandedShelfContentsextendsYTNode{statictype='ExpandedShelfContents';items: ObservedArray<YTNode>;constructor(data: RawNode){super();this.items=Parser.parseArray(data.items);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{Parser,typeRawNode}from'../index.js';import{typeObservedArray,YTNode}from'../helpers.js';exportdefaultclassHorizontalListextendsYTNode{statictype='HorizontalList';visible_item_count: string;items: ObservedArray<YTNode>;constructor(data: RawNode){super();this.visible_item_count=data.visibleItemCount;this.items=Parser.parseArray(data.items);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{Parser,typeRawNode}from'../index.js';import{typeObservedArray,YTNode}from'../helpers.js';importButtonfrom'./Button.js';exportdefaultclassHorizontalMovieListextendsYTNode{statictype='HorizontalMovieList';items: ObservedArray<YTNode>;previous_button: Button|null;next_button: Button|null;constructor(data: RawNode){super();this.items=Parser.parseArray(data.items);this.previous_button=Parser.parseItem(data.previousButton,Button);this.next_button=Parser.parseItem(data.nextButton,Button);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{YTNode,typeObservedArray}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';exportdefaultclassMerchandiseShelfextendsYTNode{statictype='MerchandiseShelf';title: string;menu: YTNode;items: ObservedArray<YTNode>;constructor(data: RawNode){super();this.title=data.title;this.menu=Parser.parseItem(data.actionButton);this.items=Parser.parseArray(data.items);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
importtypeActionsfrom'../../core/Actions.js';importtype{ApiResponse}from'../../core/Actions.js';import{YTNode}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';importtype{IParsedResponse}from'../types/ParsedResponse.js';importCreatePlaylistDialogfrom'./CreatePlaylistDialog.js';importtypeModalWithTitleAndButtonfrom'./ModalWithTitleAndButton.js';importOpenPopupActionfrom'./actions/OpenPopupAction.js';exportdefaultclassNavigationEndpointextendsYTNode{statictype='NavigationEndpoint';payload;dialog?: CreatePlaylistDialog|YTNode|null;modal?: ModalWithTitleAndButton|YTNode|null;open_popup?: OpenPopupAction|null;next_endpoint?: NavigationEndpoint;metadata: {url?: string;api_url?: string;page_type?: string;send_post?: boolean;};constructor(data: RawNode){super();if(data&&(data.innertubeCommand||data.command))data=data.innertubeCommand||data.command;if(Reflect.has(data||{},'openPopupAction'))this.open_popup=newOpenPopupAction(data.openPopupAction);constname=Object.keys(data||{}).find((item)=>item.endsWith('Endpoint')||item.endsWith('Command'));this.payload=name ? Reflect.get(data,name) : {};if(Reflect.has(this.payload,'dialog')||Reflect.has(this.payload,'content')){this.dialog=Parser.parseItem(this.payload.dialog||this.payload.content);}if(Reflect.has(this.payload,'modal')){this.modal=Parser.parseItem(this.payload.modal);}if(Reflect.has(this.payload,'nextEndpoint')){this.next_endpoint=newNavigationEndpoint(this.payload.nextEndpoint);}if(data?.serviceEndpoint){data=data.serviceEndpoint;}this.metadata={};if(data?.commandMetadata?.webCommandMetadata?.url){this.metadata.url=data.commandMetadata.webCommandMetadata.url;}if(data?.commandMetadata?.webCommandMetadata?.webPageType){this.metadata.page_type=data.commandMetadata.webCommandMetadata.webPageType;}if(data?.commandMetadata?.webCommandMetadata?.apiUrl){this.metadata.api_url=data.commandMetadata.webCommandMetadata.apiUrl.replace('/youtubei/v1/','');}elseif(name){this.metadata.api_url=this.getEndpoint(name);}if(data?.commandMetadata?.webCommandMetadata?.sendPost){this.metadata.send_post=data.commandMetadata.webCommandMetadata.sendPost;}if(data?.createPlaylistEndpoint){if(data?.createPlaylistEndpoint.createPlaylistDialog){this.dialog=Parser.parseItem(data?.createPlaylistEndpoint.createPlaylistDialog,CreatePlaylistDialog);}}}/** * Sometimes InnerTube does not return an API url, in that case the library should set it based on the name of the payload object. */getEndpoint(name: string){switch(name){case'browseEndpoint':
return'/browse';case'watchEndpoint':
case'reelWatchEndpoint':
return'/player';case'searchEndpoint':
return'/search';case'watchPlaylistEndpoint':
return'/next';case'liveChatItemContextMenuEndpoint':
return'/live_chat/get_item_context_menu';}}call<TextendsIParsedResponse>(actions: Actions,args: {[key: string]: any;parse: true}): Promise<T>;call(actions: Actions,args?: {[key: string]: any;parse?: false}): Promise<ApiResponse>;call(actions: Actions,args?: {[key: string]: any;parse?: boolean}): Promise<IParsedResponse|ApiResponse>{if(!actions)thrownewError('An active caller must be provided');if(!this.metadata.api_url)thrownewError('Expected an api_url, but none was found, this is a bug.');returnactions.execute(this.metadata.api_url,{ ...this.payload, ...args});}toURL(): string|undefined{if(!this.metadata.url)returnundefined;if(!this.metadata.page_type)returnundefined;return(this.metadata.page_type==='WEB_PAGE_TYPE_UNKNOWN' ?
this.metadata.url : `https://www.youtube.com${this.metadata.url}`);}}
import{YTNode}from'../helpers.js';importtype{RawNode}from'../index.js';exportdefaultclassPlaylistMetadataextendsYTNode{statictype='PlaylistMetadata';title: string;description: string;constructor(data: RawNode){super();this.title=data.title;this.description=data.description||null;// XXX: Appindexing should be in microformat.}}
import{Parser,typeRawNode}from'../index.js';import{typeObservedArray,YTNode}from'../helpers.js';exportdefaultclassPlaylistSidebarextendsYTNode{statictype='PlaylistSidebar';items: ObservedArray<YTNode>;constructor(data: RawNode){super();this.items=Parser.parseArray(data.items);}// XXX: alias for consistencygetcontents(){returnthis.items;}}
import{YTNode,typeObservedArray}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';exportdefaultclassProfileColumnextendsYTNode{statictype='ProfileColumn';items: ObservedArray<YTNode>;constructor(data: RawNode){super();this.items=Parser.parseArray(data.items);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{Parser,typeRawNode}from'../index.js';import{typeObservedArray,YTNode}from'../helpers.js';exportdefaultclassProfileColumnStatsextendsYTNode{statictype='ProfileColumnStats';items: ObservedArray<YTNode>;constructor(data: RawNode){super();this.items=Parser.parseArray(data.items);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{YTNode,typeObservedArray}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';importNavigationEndpointfrom'./NavigationEndpoint.js';importTextfrom'./misc/Text.js';exportdefaultclassReelShelfextendsYTNode{statictype='ReelShelf';title: Text;items: ObservedArray<YTNode>;endpoint?: NavigationEndpoint;constructor(data: RawNode){super();this.title=newText(data.title);this.items=Parser.parseArray(data.items);if(Reflect.has(data,'endpoint')){this.endpoint=newNavigationEndpoint(data.endpoint);}}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{Parser,typeRawNode}from'../index.js';import{typeObservedArray,YTNode}from'../helpers.js';exportdefaultclassRichGridextendsYTNode{statictype='RichGrid';header: YTNode;contents: ObservedArray<YTNode>;constructor(data: RawNode){super();// (Daniel Wykerd) XXX: we don't parse the masthead since it is usually an advertisement// (Daniel Wykerd) XXX: reflowOptions aren't parsed, I think its only used internally for layoutthis.header=Parser.parseItem(data.header);this.contents=Parser.parseArray(data.contents);}}
import{YTNode,typeObservedArray}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';importCompactLinkfrom'./CompactLink.js';importTextfrom'./misc/Text.js';exportdefaultclassSettingsSidebarextendsYTNode{statictype='SettingsSidebar';title: Text;items: ObservedArray<CompactLink>;constructor(data: RawNode){super();this.title=newText(data.title);this.items=Parser.parseArray(data.items,CompactLink);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{YTNode,typeObservedArray}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';importTextfrom'./misc/Text.js';exportdefaultclassVerticalListextendsYTNode{statictype='VerticalList';items: ObservedArray<YTNode>;collapsed_item_count: string;// Number?collapsed_state_button_text: Text;constructor(data: RawNode){super();this.items=Parser.parseArray(data.items);this.collapsed_item_count=data.collapsedItemCount;this.collapsed_state_button_text=newText(data.collapsedStateButtonText);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{YTNode,typeObservedArray}from'../helpers.js';import{Parser,typeRawNode}from'../index.js';importNavigationEndpointfrom'./NavigationEndpoint.js';importTextfrom'./misc/Text.js';exportdefaultclassVerticalWatchCardListextendsYTNode{statictype='VerticalWatchCardList';items: ObservedArray<YTNode>;view_all_text: Text;view_all_endpoint: NavigationEndpoint;constructor(data: RawNode){super();this.items=Parser.parseArray(data.items);this.view_all_text=newText(data.viewAllText);this.view_all_endpoint=newNavigationEndpoint(data.viewAllEndpoint);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{Parser}from'../../index.js';importAuthorfrom'../misc/Author.js';importTextfrom'../misc/Text.js';importThumbnailfrom'../misc/Thumbnail.js';importMenufrom'../menus/Menu.js';importAuthorCommentBadgefrom'./AuthorCommentBadge.js';importCommentActionButtonsfrom'./CommentActionButtons.js';importCommentReplyDialogfrom'./CommentReplyDialog.js';importPdgCommentChipfrom'./PdgCommentChip.js';importSponsorCommentBadgefrom'./SponsorCommentBadge.js';import*asProtofrom'../../../proto/index.js';import{InnertubeError}from'../../../utils/Utils.js';import{YTNode}from'../../helpers.js';importtypeActionsfrom'../../../core/Actions.js';importtype{ApiResponse}from'../../../core/Actions.js';importtype{RawNode}from'../../index.js';exportdefaultclassCommentextendsYTNode{statictype='Comment';
#actions?: Actions;content: Text;published: Text;author_is_channel_owner: boolean;current_user_reply_thumbnail: Thumbnail[];sponsor_comment_badge: SponsorCommentBadge|null;paid_comment_chip: PdgCommentChip|null;author_badge: AuthorCommentBadge|null;author: Author;action_menu: Menu|null;action_buttons: CommentActionButtons|null;comment_id: string;vote_status: string;vote_count: string;reply_count: number;is_liked: boolean;is_disliked: boolean;is_hearted: boolean;is_pinned: boolean;is_member: boolean;constructor(data: RawNode){super();this.content=newText(data.contentText);this.published=newText(data.publishedTimeText);this.author_is_channel_owner=data.authorIsChannelOwner;this.current_user_reply_thumbnail=Thumbnail.fromResponse(data.currentUserReplyThumbnail);this.sponsor_comment_badge=Parser.parseItem(data.sponsorCommentBadge,SponsorCommentBadge);this.paid_comment_chip=Parser.parseItem(data.paidCommentChipRenderer,PdgCommentChip);this.author_badge=Parser.parseItem(data.authorCommentBadge,AuthorCommentBadge);this.author=newAuthor({
...data.authorText,navigationEndpoint: data.authorEndpoint},this.author_badge ? [{metadataBadgeRenderer: this.author_badge?.orig_badge}] : null,data.authorThumbnail);this.action_menu=Parser.parseItem(data.actionMenu,Menu);this.action_buttons=Parser.parseItem(data.actionButtons,CommentActionButtons);this.comment_id=data.commentId;this.vote_status=data.voteStatus;this.vote_count=data.voteCount ? newText(data.voteCount).toString() : '0';this.reply_count=data.replyCount||0;this.is_liked=!!this.action_buttons?.like_button?.is_toggled;this.is_disliked=!!this.action_buttons?.dislike_button?.is_toggled;this.is_hearted=!!this.action_buttons?.creator_heart?.is_hearted;this.is_pinned=!!data.pinnedCommentBadge;this.is_member=!!data.sponsorCommentBadge;}/** * Likes the comment. */asynclike(): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('An active caller must be provide to perform this operation.');constbutton=this.action_buttons?.like_button;if(!button)thrownewInnertubeError('Like button was not found.',{comment_id: this.comment_id});if(button.is_toggled)thrownewInnertubeError('This comment is already liked',{comment_id: this.comment_id});constresponse=awaitbutton.endpoint.call(this.#actions,{parse: false});returnresponse;}/** * Dislikes the comment. */asyncdislike(): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('An active caller must be provide to perform this operation.');constbutton=this.action_buttons?.dislike_button;if(!button)thrownewInnertubeError('Dislike button was not found.',{comment_id: this.comment_id});if(button.is_toggled)thrownewInnertubeError('This comment is already disliked',{comment_id: this.comment_id});constresponse=awaitbutton.endpoint.call(this.#actions,{parse: false});returnresponse;}/** * Creates a reply to the comment. */asyncreply(text: string): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('An active caller must be provide to perform this operation.');if(!this.action_buttons?.reply_button)thrownewInnertubeError('Cannot reply to another reply. Try mentioning the user instead.',{comment_id: this.comment_id});constbutton=this.action_buttons?.reply_button;if(!button.endpoint?.dialog)thrownewInnertubeError('Reply button endpoint did not have a dialog.');constdialog=button.endpoint.dialog.as(CommentReplyDialog);constdialog_button=dialog.reply_button;if(!dialog_button)thrownewInnertubeError('Reply button was not found in the dialog.',{comment_id: this.comment_id});if(!dialog_button.endpoint)thrownewInnertubeError('Reply button endpoint was not found.',{comment_id: this.comment_id});constresponse=awaitdialog_button.endpoint.call(this.#actions,{commentText: text});returnresponse;}/** * Translates the comment to a given language. * @param target_language - Ex; en, ja */asynctranslate(target_language: string): Promise<{content: any;success: boolean;status_code: number;data: any;}>{if(!this.#actions)thrownewInnertubeError('An active caller must be provide to perform this operation.');// Emojis must be removed otherwise InnerTube throws a 400 status code at us.consttext=this.content.toString().replace(/[^\p{L}\p{N}\p{P}\p{Z}]/gu,'');constpayload={
text,
target_language,comment_id: this.comment_id};constaction=Proto.encodeCommentActionParams(22,payload);constresponse=awaitthis.#actions.execute('comment/perform_comment_action',{ action,client: 'ANDROID'});// XXX: Should move this to Parser#parseResponseconstmutations=response.data.frameworkUpdates?.entityBatchUpdate?.mutations;constcontent=mutations?.[0]?.payload?.commentEntityPayload?.translatedContent?.content;return{ ...response, content };}setActions(actions: Actions|undefined){this.#actions =actions;}}
import{Parser}from'../../index.js';importButtonfrom'../Button.js';importContinuationItemfrom'../ContinuationItem.js';importCommentfrom'./Comment.js';importCommentViewfrom'./CommentView.js';importCommentRepliesfrom'./CommentReplies.js';import{InnertubeError}from'../../../utils/Utils.js';import{observe,YTNode}from'../../helpers.js';importtypeActionsfrom'../../../core/Actions.js';importtype{ObservedArray}from'../../helpers.js';importtype{RawNode}from'../../index.js';exportdefaultclassCommentThreadextendsYTNode{statictype='CommentThread';
#actions?: Actions;
#continuation?: ContinuationItem;comment: Comment|CommentView|null;replies?: ObservedArray<Comment|CommentView>;comment_replies_data: CommentReplies|null;is_moderated_elq_comment: boolean;has_replies: boolean;constructor(data: RawNode){super();if(Reflect.has(data,'commentViewModel')){this.comment=Parser.parseItem(data.commentViewModel,CommentView);}else{this.comment=Parser.parseItem(data.comment,Comment);}this.comment_replies_data=Parser.parseItem(data.replies,CommentReplies);this.is_moderated_elq_comment=data.isModeratedElqComment;this.has_replies=!!this.comment_replies_data;}/** * Retrieves replies to this comment thread. */asyncgetReplies(): Promise<CommentThread>{if(!this.#actions)thrownewInnertubeError('Actions instance not set for this thread.');if(!this.comment_replies_data)thrownewInnertubeError('This comment has no replies.',this);constcontinuation=this.comment_replies_data.contents?.firstOfType(ContinuationItem);if(!continuation)thrownewInnertubeError('Replies continuation not found.');constresponse=awaitcontinuation.endpoint.call(this.#actions,{parse: true});if(!response.on_response_received_endpoints_memo)thrownewInnertubeError('Unexpected response.',response);this.replies=observe(response.on_response_received_endpoints_memo.getType(Comment,CommentView).map((comment)=>{comment.setActions(this.#actions);returncomment;}));this.#continuation =response?.on_response_received_endpoints_memo.getType(ContinuationItem).first();returnthis;}/** * Retrieves next batch of replies. */asyncgetContinuation(): Promise<CommentThread>{if(!this.replies)thrownewInnertubeError('Cannot retrieve continuation because this thread\'s replies have not been loaded.');if(!this.#continuation)thrownewInnertubeError('Continuation not found.');if(!this.#actions)thrownewInnertubeError('Actions instance not set for this thread.');constload_more_button=this.#continuation.button?.as(Button);if(!load_more_button)thrownewInnertubeError('"Load more" button not found.');constresponse=awaitload_more_button.endpoint.call(this.#actions,{parse: true});if(!response.on_response_received_endpoints_memo)thrownewInnertubeError('Unexpected response.',response);this.replies=observe(response.on_response_received_endpoints_memo.getType(Comment,CommentView).map((comment)=>{comment.setActions(this.#actions);returncomment;}));this.#continuation =response.on_response_received_endpoints_memo.getType(ContinuationItem).first();returnthis;}gethas_continuation(): boolean{if(!this.replies)thrownewInnertubeError('Cannot determine if there is a continuation because this thread\'s replies have not been loaded.');return!!this.#continuation;}setActions(actions: Actions){this.#actions =actions;}}
import{YTNode}from'../../helpers.js';importNavigationEndpointfrom'../NavigationEndpoint.js';importAuthorfrom'../misc/Author.js';importTextfrom'../misc/Text.js';importCommentReplyDialogfrom'./CommentReplyDialog.js';import{InnertubeError}from'../../../utils/Utils.js';import*asProtofrom'../../../proto/index.js';importtypeActionsfrom'../../../core/Actions.js';importtype{ApiResponse}from'../../../core/Actions.js';importtype{RawNode}from'../../index.js';exportdefaultclassCommentViewextendsYTNode{statictype='CommentView';
#actions?: Actions;like_command?: NavigationEndpoint;dislike_command?: NavigationEndpoint;unlike_command?: NavigationEndpoint;undislike_command?: NavigationEndpoint;reply_command?: NavigationEndpoint;comment_id: string;is_pinned: boolean;keys: {comment: string;comment_surface: string;toolbar_state: string;toolbar_surface: string;shared: string;};content?: Text;published_time?: string;author_is_channel_owner?: boolean;like_count?: string;reply_count?: string;is_member?: boolean;member_badge?: {url: string,a11y: string;};author?: Author;is_liked?: boolean;is_disliked?: boolean;is_hearted?: boolean;constructor(data: RawNode){super();this.comment_id=data.commentId;this.is_pinned=!!data.pinnedText;this.keys={comment: data.commentKey,comment_surface: data.commentSurfaceKey,toolbar_state: data.toolbarStateKey,toolbar_surface: data.toolbarSurfaceKey,shared: data.sharedKey};}applyMutations(comment?: RawNode,toolbar_state?: RawNode,toolbar_surface?: RawNode){if(comment){this.content=Text.fromAttributed(comment.properties.content);this.published_time=comment.properties.publishedTime;this.author_is_channel_owner=!!comment.author.isCreator;this.like_count=comment.toolbar.likeCountNotliked ? comment.toolbar.likeCountNotliked : '0';this.reply_count=comment.toolbar.replyCount ? comment.toolbar.replyCount : '0';this.is_member=!!comment.author.sponsorBadgeUrl;if(Reflect.has(comment.author,'sponsorBadgeUrl')){this.member_badge={url: comment.author.sponsorBadgeUrl,a11y: comment.author.A11y};}this.author=newAuthor({simpleText: comment.author.displayName,navigationEndpoint: comment.avatar.endpoint},comment.author,comment.avatar.image,comment.author.channelId);}if(toolbar_state){this.is_hearted=toolbar_state.heartState==='TOOLBAR_HEART_STATE_HEARTED';this.is_liked=toolbar_state.likeState==='TOOLBAR_LIKE_STATE_LIKED';this.is_disliked=toolbar_state.likeState==='TOOLBAR_LIKE_STATE_DISLIKED';}if(toolbar_surface&&!Reflect.has(toolbar_surface,'prepareAccountCommand')){this.like_command=newNavigationEndpoint(toolbar_surface.likeCommand);this.dislike_command=newNavigationEndpoint(toolbar_surface.dislikeCommand);this.unlike_command=newNavigationEndpoint(toolbar_surface.unlikeCommand);this.undislike_command=newNavigationEndpoint(toolbar_surface.undislikeCommand);this.reply_command=newNavigationEndpoint(toolbar_surface.replyCommand);}}/** * Likes the comment. * @returns A promise that resolves to the API response. * @throws If the Actions instance is not set for this comment or if the like command is not found. */asynclike(): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('Actions instance not set for this comment.');if(!this.like_command)thrownewInnertubeError('Like command not found.');if(this.is_liked)thrownewInnertubeError('This comment is already liked.',{comment_id: this.comment_id});returnthis.like_command.call(this.#actions);}/** * Dislikes the comment. * @returns A promise that resolves to the API response. * @throws If the Actions instance is not set for this comment or if the dislike command is not found. */asyncdislike(): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('Actions instance not set for this comment.');if(!this.dislike_command)thrownewInnertubeError('Dislike command not found.');if(this.is_disliked)thrownewInnertubeError('This comment is already disliked.',{comment_id: this.comment_id});returnthis.dislike_command.call(this.#actions);}/** * Unlikes the comment. * @returns A promise that resolves to the API response. * @throws If the Actions instance is not set for this comment or if the unlike command is not found. */asyncunlike(): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('Actions instance not set for this comment.');if(!this.unlike_command)thrownewInnertubeError('Unlike command not found.');if(!this.is_liked)thrownewInnertubeError('This comment is not liked.',{comment_id: this.comment_id});returnthis.unlike_command.call(this.#actions);}/** * Undislikes the comment. * @returns A promise that resolves to the API response. * @throws If the Actions instance is not set for this comment or if the undislike command is not found. */asyncundislike(): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('Actions instance not set for this comment.');if(!this.undislike_command)thrownewInnertubeError('Undislike command not found.');if(!this.is_disliked)thrownewInnertubeError('This comment is not disliked.',{comment_id: this.comment_id});returnthis.undislike_command.call(this.#actions);}/** * Replies to the comment. * @param comment_text - The text of the reply. * @returns A promise that resolves to the API response. * @throws If the Actions instance is not set for this comment or if the reply command is not found. */asyncreply(comment_text: string): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('Actions instance not set for this comment.');if(!this.reply_command)thrownewInnertubeError('Reply command not found.');constdialog=this.reply_command.dialog?.as(CommentReplyDialog);if(!dialog)thrownewInnertubeError('Reply dialog not found.');constreply_button=dialog.reply_button;if(!reply_button)thrownewInnertubeError('Reply button not found in the dialog.');if(!reply_button.endpoint)thrownewInnertubeError('Reply button endpoint not found.');returnreply_button.endpoint.call(this.#actions,{commentText: comment_text});}/** * Translates the comment to the specified target language. * @param target_language - The target language to translate the comment to, e.g. 'en', 'ja'. * @returns A Promise that resolves to an ApiResponse object with the translated content, if available. * @throws if the Actions instance is not set for this comment or if the comment content is not found. */asynctranslate(target_language: string): Promise<ApiResponse&{content?: string}>{if(!this.#actions)thrownewInnertubeError('Actions instance not set for this comment.');if(!this.content)thrownewInnertubeError('Comment content not found.',{comment_id: this.comment_id});// Emojis must be removed otherwise InnerTube throws a 400 status code at us.consttext=this.content.toString().replace(/[^\p{L}\p{N}\p{P}\p{Z}]/gu,'');constpayload={
text,
target_language
};constaction=Proto.encodeCommentActionParams(22,payload);constresponse=awaitthis.#actions.execute('comment/perform_comment_action',{ action,client: 'ANDROID'});// XXX: Should move this to Parser#parseResponseconstmutations=response.data.frameworkUpdates?.entityBatchUpdate?.mutations;constcontent=mutations?.[0]?.payload?.commentEntityPayload?.translatedContent?.content;return{ ...response, content };}setActions(actions: Actions|undefined){this.#actions =actions;}}
import{YTNode}from'../../../helpers.js';importtype{RawNode}from'../../../index.js';import{Parser}from'../../../index.js';importButtonfrom'../../Button.js';importTextfrom'../../misc/Text.js';importThumbnailfrom'../../misc/Thumbnail.js';exportdefaultclassLiveChatBannerPollextendsYTNode{statictype='LiveChatBannerPoll';poll_question: Text;author_photo: Thumbnail[];choices: {option_id: string;text: string;}[];collapsed_state_entity_key: string;live_chat_poll_state_entity_key: string;context_menu_button: Button|null;constructor(data: RawNode){super();this.poll_question=newText(data.pollQuestion);this.author_photo=Thumbnail.fromResponse(data.authorPhoto);this.choices=data.pollChoices.map((choice: RawNode)=>({option_id: choice.pollOptionId,text: newText(choice.text).toString()// XXX: This toString should probably not be used here.}));this.collapsed_state_entity_key=data.collapsedStateEntityKey;this.live_chat_poll_state_entity_key=data.liveChatPollStateEntityKey;this.context_menu_button=Parser.parseItem(data.contextMenuButton,Button);}}
import{Parser}from'../../index.js';importtype{ObservedArray}from'../../helpers.js';import{YTNode}from'../../helpers.js';importtype{RawNode}from'../../index.js';exportdefaultclassMenuextendsYTNode{statictype='Menu';items: ObservedArray<YTNode>;top_level_buttons: ObservedArray<YTNode>;label?: string;constructor(data: RawNode){super();this.items=Parser.parseArray(data.items);this.top_level_buttons=Parser.parseArray(data.topLevelButtons);if(Reflect.has(data,'accessibility')&&Reflect.has(data.accessibility,'accessibilityData')){this.label=data.accessibility.accessibilityData.label;}}// XXX: alias for consistencygetcontents(){returnthis.items;}}
import{Parser}from'../../index.js';import{typeSuperParsedResult,YTNode}from'../../helpers.js';importtype{RawNode}from'../../index.js';exportdefaultclassMultiPageMenuNotificationSectionextendsYTNode{statictype='MultiPageMenuNotificationSection';items: SuperParsedResult<YTNode>;constructor(data: RawNode){super();this.items=Parser.parse(data.items);}// XXX: Alias for consistency.getcontents(){returnthis.items;}}
import{YTNode}from'../../helpers.js';importtype{RawNode}from'../../index.js';exportdefaultclassMusicMenuItemDividerextendsYTNode{statictype='MusicMenuItemDivider';// eslint-disable-next-lineconstructor(_data: RawNode){super();// XXX: Should check if this ever has any data.}}
import{YTNode}from'../../helpers.js';importtype{RawNode}from'../../index.js';importNavigationEndpointfrom'../NavigationEndpoint.js';importTextfrom'../misc/Text.js';exportdefaultclassMusicMultiSelectMenuItemextendsYTNode{statictype='MusicMultiSelectMenuItem';title: string;form_item_entity_key: string;selected_icon_type?: string;endpoint?: NavigationEndpoint;selected: boolean;constructor(data: RawNode){super();this.title=newText(data.title).toString();this.form_item_entity_key=data.formItemEntityKey;if(Reflect.has(data,'selectedIcon')){this.selected_icon_type=data.selectedIcon.iconType;}// @TODO: Check if there any other endpoints we can parse.if(Reflect.has(data,'selectedCommand')){this.endpoint=newNavigationEndpoint(data.selectedCommand);}this.selected=!!this.endpoint;}}
import{Log}from'../../../utils/index.js';importtype{RawNode}from'../../index.js';importNavigationEndpointfrom'../NavigationEndpoint.js';importEmojiRunfrom'./EmojiRun.js';importTextRunfrom'./TextRun.js';exportinterfaceRun{text: string;toString(): string;toHTML(): string;}exportfunctionescape(text: string){returntext.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}// Place this here, instead of in a private static property,// To avoid the performance penalty of the private field polyfillconstTAG='Text';exportdefaultclassText{text?: string;runs?: (EmojiRun|TextRun)[];endpoint?: NavigationEndpoint;constructor(data: RawNode){if(typeofdata==='object'&&data!==null&&Reflect.has(data,'runs')&&Array.isArray(data.runs)){this.runs=data.runs.map((run: RawNode)=>run.emoji ?
newEmojiRun(run) :
newTextRun(run));this.text=this.runs.map((run)=>run.text).join('');}else{this.text=data?.simpleText;}if(typeofdata==='object'&&data!==null&&Reflect.has(data,'navigationEndpoint')){this.endpoint=newNavigationEndpoint(data.navigationEndpoint);}if(typeofdata==='object'&&data!==null&&Reflect.has(data,'titleNavigationEndpoint')){this.endpoint=newNavigationEndpoint(data.titleNavigationEndpoint);}if(!this.endpoint){if((this.runs?.[0]asTextRun)?.endpoint){this.endpoint=(this.runs?.[0]asTextRun)?.endpoint;}}}staticfromAttributed(data: AttributedText){const{
content,styleRuns: style_runs,commandRuns: command_runs,attachmentRuns: attachment_runs}=data;construns: RawRun[]=[{text: content,startIndex: 0}];if(style_runs||command_runs||attachment_runs){if(style_runs){for(conststyle_runofstyle_runs){if(style_run.italic||style_run.strikethrough==='LINE_STYLE_SINGLE'||style_run.weightLabel==='FONT_WEIGHT_MEDIUM'||style_run.weightLabel==='FONT_WEIGHT_BOLD'){constmatching_run=findMatchingRun(runs,style_run);if(!matching_run){Log.warn(TAG,'Unable to find matching run for style run. Skipping...',{
style_run,input_data: data,// For performance reasons, web browser consoles only expand an object, when the user clicks on it,// So if we log the original runs object, it might have changed by the time the user looks at it.// Deep clone, so that we log the exact state of the runs at this point.parsed_runs: JSON.parse(JSON.stringify(runs))});continue;}// Comments use MEDIUM for bold text and video descriptions use BOLD for bold textinsertSubRun(runs,matching_run,style_run,{bold: style_run.weightLabel==='FONT_WEIGHT_MEDIUM'||style_run.weightLabel==='FONT_WEIGHT_BOLD',italics: style_run.italic,strikethrough: style_run.strikethrough==='LINE_STYLE_SINGLE'});}else{Log.debug(TAG,'Skipping style run as it is doesn\'t have any information that we parse.',{
style_run,input_data: data});}}}if(command_runs){for(constcommand_runofcommand_runs){if(command_run.onTap){constmatching_run=findMatchingRun(runs,command_run);if(!matching_run){Log.warn(TAG,'Unable to find matching run for command run. Skipping...',{
command_run,input_data: data,// For performance reasons, web browser consoles only expand an object, when the user clicks on it,// So if we log the original runs object, it might have changed by the time the user looks at it.// Deep clone, so that we log the exact state of the runs at this point.parsed_runs: JSON.parse(JSON.stringify(runs))});continue;}insertSubRun(runs,matching_run,command_run,{navigationEndpoint: command_run.onTap});}else{Log.debug(TAG,'Skipping command run as it is missing the "doTap" property.',{
command_run,input_data: data});}}}if(attachment_runs){for(constattachment_runofattachment_runs){constmatching_run=findMatchingRun(runs,attachment_run);if(!matching_run){Log.warn(TAG,'Unable to find matching run for attachment run. Skipping...',{
attachment_run,input_data: data,// For performance reasons, web browser consoles only expand an object, when the user clicks on it,// So if we log the original runs object, it might have changed by the time the user looks at it.// Deep clone, so that we log the exact state of the runs at this point.parsed_runs: JSON.parse(JSON.stringify(runs))});continue;}if(attachment_run.length===0){matching_run.attachment=attachment_run;}else{constoffset_start_index=attachment_run.startIndex-matching_run.startIndex;consttext=matching_run.text.substring(offset_start_index,offset_start_index+attachment_run.length);constis_custom_emoji=(/^:[^:]+:$/).test(text);if(attachment_run.element?.type?.imageType?.image&&(is_custom_emoji||(/^(?:\p{Emoji}|\u200d)+$/u).test(text))){constemoji={image: attachment_run.element.type.imageType.image,isCustomEmoji: is_custom_emoji,shortcuts: is_custom_emoji ? [text] : undefined};insertSubRun(runs,matching_run,attachment_run,{ emoji });}else{insertSubRun(runs,matching_run,attachment_run,{attachment: attachment_run});}}}}}returnnewText({ runs });}/** * Converts the text to HTML. * @returns The HTML. */toHTML(): string|undefined{returnthis.runs ? this.runs.map((run)=>run.toHTML()).join('') : this.text;}/** * Checks if the text is empty. * @returns Whether the text is empty. */isEmpty(): boolean{returnthis.text===undefined;}/** * Converts the text to a string. * @returns The text. */toString(): string{returnthis.text||'N/A';}}functionfindMatchingRun(runs: RawRun[],response_run: ResponseRun){returnruns.find((run)=>{returnrun.startIndex<=response_run.startIndex&&response_run.startIndex+response_run.length<=run.startIndex+run.text.length;});}functioninsertSubRun(runs: RawRun[],original_run: RawRun,response_run: ResponseRun,properties_to_add: Omit<RawRun,'text'|'startIndex'>){constreplace_index=runs.indexOf(original_run);constreplacement_runs=[];constoffset_start_index=response_run.startIndex-original_run.startIndex;// Stuff before the runif(response_run.startIndex>original_run.startIndex){replacement_runs.push({
...original_run,text: original_run.text.substring(0,offset_start_index)});}replacement_runs.push({
...original_run,text: original_run.text.substring(offset_start_index,offset_start_index+response_run.length),startIndex: response_run.startIndex,
...properties_to_add});// Stuff after the runif(response_run.startIndex+response_run.length<original_run.startIndex+original_run.text.length){replacement_runs.push({
...original_run,text: original_run.text.substring(offset_start_index+response_run.length),startIndex: response_run.startIndex+response_run.length});}runs.splice(replace_index,1, ...replacement_runs);}interfaceRawRun{text: string,bold?: boolean;italics?: boolean;strikethrough?: boolean;navigationEndpoint?: RawNode;attachment?: RawNode;emoji?: RawNode;startIndex: number;}exportinterfaceAttributedText{content: string;styleRuns?: StyleRun[];commandRuns?: CommandRun[];attachmentRuns?: AttachmentRun[];decorationRuns?: ResponseRun[];}interfaceResponseRun{startIndex: number;length: number;}interfaceStyleRunextendsResponseRun{italic?: boolean;weightLabel?: string;strikethrough?: string;fontFamilyName?: string;styleRunExtensions?: {styleRunColorMapExtension?: {colorMap?: {key: string,value: number}[]}}}interfaceCommandRunextendsResponseRun{onTap?: RawNode;}interfaceAttachmentRunextendsResponseRun{alignment?: string;element?: {type?: {imageType?: {image: RawNode,playbackState?: string;}};properties?: RawNode};}
importTextfrom'../misc/Text.js';import{YTNode}from'../../helpers.js';import{Parser,typeRawNode}from'../../index.js';importToggleButtonfrom'../ToggleButton.js';importThumbnailfrom'../misc/Thumbnail.js';importtypeActionsfrom'../../../core/Actions.js';import{InnertubeError}from'../../../utils/Utils.js';import{typeApiResponse}from'../../../core/Actions.js';exportdefaultclassKidsBlocklistPickerItemextendsYTNode{statictype='KidsBlocklistPickerItem';
#actions?: Actions;child_display_name: Text;child_account_description: Text;avatar: Thumbnail[];block_button: ToggleButton|null;blocked_entity_key: string;constructor(data: RawNode){super();this.child_display_name=newText(data.childDisplayName);this.child_account_description=newText(data.childAccountDescription);this.avatar=Thumbnail.fromResponse(data.avatar);this.block_button=Parser.parseItem(data.blockButton,[ToggleButton]);this.blocked_entity_key=data.blockedEntityKey;}asyncblockChannel(): Promise<ApiResponse>{if(!this.#actions)thrownewInnertubeError('An active caller must be provide to perform this operation.');constbutton=this.block_button;if(!button)thrownewInnertubeError('Block button was not found.',{child_display_name: this.child_display_name});if(button.is_toggled)thrownewInnertubeError('This channel is already blocked.',{child_display_name: this.child_display_name});constresponse=awaitbutton.endpoint.call(this.#actions,{parse: false});returnresponse;}setActions(actions: Actions|undefined){this.#actions =actions;}}
/* eslint-disable no-cond-assign */import{YTNode}from'./helpers.js';import*asParserfrom'./parser.js';import{InnertubeError}from'../utils/Utils.js';importAuthorfrom'./classes/misc/Author.js';importTextfrom'./classes/misc/Text.js';importThumbnailfrom'./classes/misc/Thumbnail.js';importNavigationEndpointfrom'./classes/NavigationEndpoint.js';importtype{YTNodeConstructor}from'./helpers.js';exporttypeMiscInferenceType={type: 'misc',misc_type: 'NavigationEndpoint',optional: boolean,endpoint: NavigationEndpoint}|{type: 'misc',misc_type: 'Text',optional: boolean,text: string,endpoint?: NavigationEndpoint}|{type: 'misc',misc_type: 'Thumbnail',optional: boolean,}|{type: 'misc',misc_type: 'Author',optional: boolean,params: [string,string?],}exportinterfaceObjectInferenceType{type: 'object',keys: KeyInfo,optional: boolean,}exportinterfaceRendererInferenceType{type: 'renderer',renderers: string[],optional: boolean}exportinterfacePrimativeInferenceType{type: 'primative',typeof: ('string'|'number'|'boolean'|'bigint'|'symbol'|'undefined'|'function'|'never'|'unknown')[],optional: boolean,}exporttypeArrayInferenceType={type: 'array',array_type: 'primitive',items: PrimativeInferenceType,optional: boolean,}|{type: 'array',array_type: 'object',items: ObjectInferenceType,optional: boolean,}|{type: 'array',array_type: 'renderer',renderers: string[],optional: boolean,};exporttypeInferenceType=RendererInferenceType|MiscInferenceType|ObjectInferenceType|PrimativeInferenceType|ArrayInferenceType;exporttypeKeyInfo=(readonly[string,InferenceType])[];constIGNORED_KEYS=newSet(['trackingParams','accessibility','accessibilityData']);constRENDERER_EXAMPLES: Record<string,unknown>={};exportfunctioncamelToSnake(str: string){returnstr.replace(/[A-Z]/g,(letter)=>`_${letter.toLowerCase()}`);}/** * Infer the type of a key given its value * @param key - The key to infer the type of * @param value - The value of the key * @returns The inferred type */exportfunctioninferType(key: string,value: unknown): InferenceType{letreturn_value: string|Record<string,any>|false|MiscInferenceType|ArrayInferenceType=false;if(typeofvalue==='object'&&value!=null){if(return_value=isRenderer(value)){RENDERER_EXAMPLES[return_value]=Reflect.get(value,Reflect.ownKeys(value)[0]);return{type: 'renderer',renderers: [return_value],optional: false};}if(return_value=isRendererList(value)){for(const[key,value]ofObject.entries(return_value)){RENDERER_EXAMPLES[key]=value;}return{type: 'array',array_type: 'renderer',renderers: Object.keys(return_value),optional: false};}if(return_value=isMiscType(key,value)){returnreturn_valueasMiscInferenceType;}if(return_value=isArrayType(value)){returnreturn_valueasArrayInferenceType;}}constprimative_type=typeofvalue;if(primative_type==='object')return{type: 'object',keys: Object.entries(valueasobject).map(([key,value])=>[key,inferType(key,value)]),optional: false};return{type: 'primative',typeof: [primative_type],optional: false};}/** * Checks if the given value is an array of renderers * @param value - The value to check * @returns If it is a renderer list, return an object with keys being the classnames, and values being an example of that class. * Otherwise, return false. */exportfunctionisRendererList(value: unknown){constarr=Array.isArray(value);if(arr&&value.length===0)returnfalse;constis_list=arr&&value.every((item)=>isRenderer(item));return(is_list ?
Object.fromEntries(value.map((item)=>{constkey=Reflect.ownKeys(item)[0].toString();return[Parser.sanitizeClassName(key),item[key]];})) :
false);}/** * Check if the given value is a misc type. * @param key - The key of the value * @param value - The value to check * @returns If it is a misc type, return the InferenceType. Otherwise, return false. */exportfunctionisMiscType(key: string,value: unknown): MiscInferenceType|false{if(typeofvalue==='object'&&value!==null){// NavigationEndpointif(key.endsWith('Endpoint')||key.endsWith('Command')||key==='endpoint'){return{type: 'misc',endpoint: newNavigationEndpoint(value),optional: false,misc_type: 'NavigationEndpoint'};}// Textif(Reflect.has(value,'simpleText')||Reflect.has(value,'runs')){consttextNode=newText(value);return{type: 'misc',misc_type: 'Text',optional: false,endpoint: textNode.endpoint,text: textNode.toString()};}// Thumbnailif(Reflect.has(value,'thumbnails')&&Array.isArray(Reflect.get(value,'thumbnails'))){return{type: 'misc',misc_type: 'Thumbnail',optional: false};}}returnfalse;}/** * Check if the given value is a renderer * @param value - The value to check * @returns If it is a renderer, return the class name. Otherwise, return false. */exportfunctionisRenderer(value: unknown){constis_object=typeofvalue==='object';if(!is_object)returnfalse;constkeys=Reflect.ownKeys(valueasobject);if(keys.length===1){constfirst_key=keys[0].toString();if(first_key.endsWith('Renderer')||first_key.endsWith('Model')){returnParser.sanitizeClassName(first_key);}}returnfalse;}/** * Checks if the given value is an array * @param value - The value to check * @returns If it is an array, return the InferenceType. Otherwise, return false. */exportfunctionisArrayType(value: unknown): false|ArrayInferenceType{if(!Array.isArray(value))returnfalse;// If the array is empty, we can't infer anythingif(value.length===0)return{type: 'array',array_type: 'primitive',items: {type: 'primative',typeof: ['never'],optional: false},optional: false};// We'll infer the primative type of the array entriesconstarray_entry_types=value.map((item)=>typeofitem);// We only support arrays that have the same primative type throughoutconstall_same_type=array_entry_types.every((type)=>type===array_entry_types[0]);if(!all_same_type)return{type: 'array',array_type: 'primitive',items: {type: 'primative',typeof: ['unknown'],optional: false},optional: false};consttype=array_entry_types[0];if(type!=='object')return{type: 'array',array_type: 'primitive',items: {type: 'primative',typeof: [type],optional: false},optional: false};letkey_type: KeyInfo=[];for(leti=0;i<value.length;i++){constcurrent_keys=Object.entries(value[i]asobject).map(([key,value])=>[key,inferType(key,value)]asconst);if(i===0){key_type=current_keys;continue;}key_type=mergeKeyInfo(key_type,current_keys).resolved_key_info;}return{type: 'array',array_type: 'object',items: {type: 'object',keys: key_type,optional: false},optional: false};}functionintrospectKeysFirstPass(classdata: unknown): KeyInfo{if(typeofclassdata!=='object'||classdata===null){thrownewInnertubeError('Generator: Cannot introspect non-object',{
classdata
});}constkeys=Reflect.ownKeys(classdata).filter((key)=>!isIgnoredKey(key)).filter((key): key is string=>typeofkey==='string');returnkeys.map((key)=>{constvalue=Reflect.get(classdata,key)asunknown;constinferred_type=inferType(key,value);return[key,inferred_type]asconst;});}functionintrospectKeysSecondPass(key_info: KeyInfo){// The second pass will detect Authorconstchannel_nav=key_info.filter(([,value])=>{if(value.type!=='misc')returnfalse;if(!(value.misc_type==='NavigationEndpoint'||value.misc_type==='Text'))returnfalse;returnvalue.endpoint?.metadata.page_type==='WEB_PAGE_TYPE_CHANNEL';});// Whichever one has the longest text is the most probable matchconstmost_probable_match=channel_nav.sort(([,a],[,b])=>{if(a.type!=='misc'||b.type!=='misc')return0;if(a.misc_type!=='Text'||b.misc_type!=='Text')return0;returnb.text.length-a.text.length;});constexcluded_keys=newSet<string>();constcannonical_channel_nav=most_probable_match[0];letauthor: MiscInferenceType|undefined;// We've found an authorif(cannonical_channel_nav){excluded_keys.add(cannonical_channel_nav[0]);// Now to locate its metadata// We'll first get all the keys in the classdataconstkeys=key_info.map(([key])=>key);// Check for anything ending in 'Badges' equals 'badges'constbadges=keys.filter((key)=>key.endsWith('Badges')||key==='badges');// The likely candidate is the one with some prefix (owner, author)constlikely_badges=badges.filter((key)=>key.startsWith('owner')||key.startsWith('author'));// If we have a likely candidate, we'll use thatconstcannonical_badges=likely_badges[0]??badges[0];// Now we have the author and its badges// Verify that its actually badgesconstbadge_key_info=key_info.find(([key])=>key===cannonical_badges);constis_badges=badge_key_info ?
badge_key_info[1].type==='array'&&badge_key_info[1].array_type==='renderer'&&Reflect.has(badge_key_info[1].renderers,'MetadataBadge') :
false;if(is_badges&&cannonical_badges)excluded_keys.add(cannonical_badges);// TODO: next we check for the author's thumbnailauthor={type: 'misc',misc_type: 'Author',optional: false,params: [cannonical_channel_nav[0],is_badges ? cannonical_badges : undefined]};}if(author){key_info.push(['author',author]);}returnkey_info.filter(([key])=>!excluded_keys.has(key));}functionintrospect2(classdata: unknown){constkey_info=introspectKeysFirstPass(classdata);returnintrospectKeysSecondPass(key_info);}/** * Introspect an example of a class in order to determine its key info and dependencies * @param classdata - The example of the class * @returns The key info and any unimplemented dependencies */exportfunctionintrospect(classdata: unknown){constkey_info=introspect2(classdata);constdependencies=newMap<string,any>();for(const[,value]ofkey_info){if(value.type==='renderer'||(value.type==='array'&&value.array_type==='renderer'))for(constrendererofvalue.renderers){constexample=RENDERER_EXAMPLES[renderer];if(example)dependencies.set(renderer,example);}}constunimplemented_dependencies=Array.from(dependencies).filter(([classname])=>!Parser.hasParser(classname));return{
key_info,
unimplemented_dependencies
};}/** * Is this key ignored by the parser? * @param key - The key to check * @returns Whether or not the key is ignored */exportfunctionisIgnoredKey(key: string|symbol){returntypeofkey==='string'&&IGNORED_KEYS.has(key);}/** * Given a classname and its resolved key info, create a new class * @param classname - The name of the class * @param key_info - The resolved key info * @returns Class based on the key info extending YTNode */exportfunctioncreateRuntimeClass(classname: string,key_info: KeyInfo,logger: Parser.ParserErrorHandler): YTNodeConstructor{logger({error_type: 'class_not_found',
classname,
key_info
});constnode=classextendsYTNode{statictype=classname;static #key_info =newMap<string,InferenceType>();staticsetkey_info(key_info: KeyInfo){this.#key_info =newMap(key_info);}staticgetkey_info(){return[ ...this.#key_info.entries()];}constructor(data: unknown){super();const{
key_info,
unimplemented_dependencies
}=introspect(data);const{
resolved_key_info,
changed_keys
}=mergeKeyInfo(node.key_info,key_info);constdid_change=changed_keys.length>0;if(did_change){node.key_info=resolved_key_info;logger({error_type: 'class_changed',
classname,key_info: node.key_info,
changed_keys
});}for(const[name,data]ofunimplemented_dependencies)generateRuntimeClass(name,data,logger);for(const[key,value]ofkey_info){letsnake_key=camelToSnake(key);if(value.type==='misc'&&value.misc_type==='NavigationEndpoint')snake_key='endpoint';Reflect.set(this,snake_key,parse(key,value,data));}}};node.key_info=key_info;Object.defineProperty(node,'name',{value: classname,writable: false});returnnode;}/** * Given example data for a class, introspect, implement dependencies, and create a new class * @param classname - The name of the class * @param classdata - The example of the class * @returns Class based on the example classdata extending YTNode */exportfunctiongenerateRuntimeClass(classname: string,classdata: unknown,logger: Parser.ParserErrorHandler){const{
key_info,
unimplemented_dependencies
}=introspect(classdata);constJITNode=createRuntimeClass(classname,key_info,logger);Parser.addRuntimeParser(classname,JITNode);for(const[name,data]ofunimplemented_dependencies)generateRuntimeClass(name,data,logger);returnJITNode;}/** * Generate a typescript class based on the key info * @param classname - The name of the class * @param key_info - The key info, as returned by {@link introspect} * @returns Typescript class file */exportfunctiongenerateTypescriptClass(classname: string,key_info: KeyInfo){constprops: string[]=[];constconstructor_lines=['super();'];for(const[key,value]ofkey_info){letsnake_key=camelToSnake(key);if(value.type==='misc'&&value.misc_type==='NavigationEndpoint')snake_key='endpoint';props.push(`${snake_key}${value.optional ? '?' : ''}: ${toTypeDeclaration(value)};`);constructor_lines.push(`this.${snake_key} = ${toParser(key,value)};`);}return`class ${classname} extends YTNode {\n static type = '${classname}';\n\n ${props.join('\n ')}\n\n constructor(data: RawNode) {\n ${constructor_lines.join('\n ')}\n }\n}\n`;}functiontoTypeDeclarationObject(indentation: number,keys: KeyInfo){return`{\n${keys.map(([key,value])=>`${' '.repeat((indentation+2)*2)}${camelToSnake(key)}${value.optional ? '?' : ''}: ${toTypeDeclaration(value,indentation+1)}`).join(',\n')}\n${' '.repeat((indentation+1)*2)}}`;}/** * For a given inference type, get the typescript type declaration * @param inference_type - The inference type to get the declaration for * @param indentation - The indentation level (used for objects) * @returns Typescript type declaration */exportfunctiontoTypeDeclaration(inference_type: InferenceType,indentation=0): string{switch(inference_type.type){case'renderer':
{return`${inference_type.renderers.map((type)=>`YTNodes.${type}`).join(' | ')} | null`;}case'array':
{switch(inference_type.array_type){case'renderer':
return`ObservedArray<${inference_type.renderers.map((type)=>`YTNodes.${type}`).join(' | ')}> | null`;case'primitive':
{constitems_list=inference_type.items.typeof;if(inference_type.items.optional&&!items_list.includes('undefined'))items_list.push('undefined');constitems=items_list.length===1 ?
`${items_list[0]}` : `(${items_list.join(' | ')})`;return`${items}[]`;}case'object':
return`${toTypeDeclarationObject(indentation,inference_type.items.keys)}[]`;default:
thrownewError('Unreachable code reached! Switch missing case!');}}case'object':
{returntoTypeDeclarationObject(indentation,inference_type.keys);}case'misc':
switch(inference_type.misc_type){case'Thumbnail':
return'Thumbnail[]';default:
returninference_type.misc_type;}case'primative':
returninference_type.typeof.join(' | ');}}functiontoParserObject(indentation: number,keys: KeyInfo,key_path: string[],key: string){constnew_keypath=[ ...key_path,key];return`{\n${keys.map(([key,value])=>`${' '.repeat((indentation+2)*2)}${camelToSnake(key)}: ${toParser(key,value,new_keypath,indentation+1)}`).join(',\n')}\n${' '.repeat((indentation+1)*2)}}`;}/** * Generate statements to parse a given inference type * @param key - The key to parse * @param inference_type - The inference type to parse * @param key_path - The path to the key (excluding the key itself) * @param indentation - The indentation level (used for objects) * @returns Statement to parse the given key */exportfunctiontoParser(key: string,inference_type: InferenceType,key_path: string[]=['data'],indentation=1){letparser='undefined';switch(inference_type.type){case'renderer':
{parser=`Parser.parseItem(${key_path.join('.')}.${key}, ${toParserValidTypes(inference_type.renderers)})`;}break;case'array':
{switch(inference_type.array_type){case'renderer':
parser=`Parser.parse(${key_path.join('.')}.${key}, true, ${toParserValidTypes(inference_type.renderers)})`;break;case'object':
parser=`${key_path.join('.')}.${key}.map((item: any) => (${toParserObject(indentation,inference_type.items.keys,[],'item')}))`;break;case'primitive':
parser=`${key_path.join('.')}.${key}`;break;default:
thrownewError('Unreachable code reached! Switch missing case!');}}break;case'object':
{parser=toParserObject(indentation,inference_type.keys,key_path,key);}break;case'misc':
switch(inference_type.misc_type){case'Thumbnail':
parser=`Thumbnail.fromResponse(${key_path.join('.')}.${key})`;break;case'Author':
{constauthor_parser=`new Author(${key_path.join('.')}.${inference_type.params[0]}, ${inference_type.params[1] ? `${key_path.join('.')}.${inference_type.params[1]}` : 'undefined'})`;if(inference_type.optional)return`Reflect.has(${key_path.join('.')}, '${inference_type.params[0]}') ? ${author_parser} : undefined`;returnauthor_parser;}default:
parser=`new ${inference_type.misc_type}(${key_path.join('.')}.${key})`;break;}if(parser==='undefined')thrownewError('Unreachable code reached! Switch missing case!');break;case'primative':
parser=`${key_path.join('.')}.${key}`;break;}if(inference_type.optional)return`Reflect.has(${key_path.join('.')}, '${key}') ? ${parser} : undefined`;returnparser;}functiontoParserValidTypes(types: string[]){if(types.length===1){return`YTNodes.${types[0]}`;}return`[ ${types.map((type)=>`YTNodes.${type}`).join(', ')} ]`;}functionaccessDataFromKeyPath(root: any,key_path: string[]){letdata=root;for(constkeyofkey_path)data=data[key];returndata;}functionhasDataFromKeyPath(root: any,key_path: string[]){letdata=root;for(constkeyofkey_path)if(!Reflect.has(data,key))returnfalse;elsedata=data[key];returntrue;}functionparseObject(key: string,data: unknown,key_path: string[],keys: KeyInfo,should_optional: boolean){constobj: any={};constnew_key_path=[ ...key_path,key];for(const[key,value]ofkeys){obj[key]=should_optional ? parse(key,value,data,new_key_path) : undefined;}returnobj;}/** * Parse a value from a given key path using the given inference type * @param key - The key to parse * @param inference_type - The inference type to parse * @param data - The data to parse from * @param key_path - The path to the key (excluding the key itself) * @returns The parsed value */exportfunctionparse(key: string,inference_type: InferenceType,data: unknown,key_path: string[]=['data']){constshould_optional=!inference_type.optional||hasDataFromKeyPath({ data },[ ...key_path,key]);switch(inference_type.type){case'renderer':
{returnshould_optional ? Parser.parseItem(accessDataFromKeyPath({ data },[ ...key_path,key]),inference_type.renderers.map((type)=>Parser.getParserByName(type))) : undefined;}case'array':
{switch(inference_type.array_type){case'renderer':
returnshould_optional ? Parser.parse(accessDataFromKeyPath({ data },[ ...key_path,key]),true,inference_type.renderers.map((type)=>Parser.getParserByName(type))) : undefined;break;case'object':
returnshould_optional ? accessDataFromKeyPath({ data },[ ...key_path,key]).map((_: any,idx: number)=>{returnparseObject(`${idx}`,data,[ ...key_path,key],inference_type.items.keys,should_optional);}) : undefined;case'primitive':
returnshould_optional ? accessDataFromKeyPath({ data },[ ...key_path,key]) : undefined;}thrownewError('Unreachable code reached! Switch missing case!');}case'object':
{returnparseObject(key,data,key_path,inference_type.keys,should_optional);}case'misc':
switch(inference_type.misc_type){case'NavigationEndpoint':
returnshould_optional ? newNavigationEndpoint(accessDataFromKeyPath({ data },[ ...key_path,key])) : undefined;case'Text':
returnshould_optional ? newText(accessDataFromKeyPath({ data },[ ...key_path,key])) : undefined;case'Thumbnail':
returnshould_optional ? Thumbnail.fromResponse(accessDataFromKeyPath({ data },[ ...key_path,key])) : undefined;case'Author':
{constauthor_should_optional=!inference_type.optional||hasDataFromKeyPath({ data },[ ...key_path,inference_type.params[0]]);returnauthor_should_optional ? newAuthor(accessDataFromKeyPath({ data },[ ...key_path,inference_type.params[0]]),inference_type.params[1] ?
accessDataFromKeyPath({ data },[ ...key_path,inference_type.params[1]]) : undefined) : undefined;}}thrownewError('Unreachable code reached! Switch missing case!');case'primative':
returnaccessDataFromKeyPath({ data },[ ...key_path,key]);}}/** * Merges two sets of key info, resolving any conflicts * @param key_info - The current key info * @param new_key_info - The new key info * @returns The merged key info */exportfunctionmergeKeyInfo(key_info: KeyInfo,new_key_info: KeyInfo){constchanged_keys=newMap<string,InferenceType>();constcurrent_keys=newSet(key_info.map(([key])=>key));constnew_keys=newSet(new_key_info.map(([key])=>key));constadded_keys=new_key_info.filter(([key])=>!current_keys.has(key));constremoved_keys=key_info.filter(([key])=>!new_keys.has(key));constcommon_keys=key_info.filter(([key])=>new_keys.has(key));constnew_key_map=newMap(new_key_info);for(const[key,type]ofcommon_keys){constnew_type=new_key_map.get(key);if(!new_type)continue;if(type.type!==new_type.type){// We've got a type mismatch, this is unknown, we do not resolve unionschanged_keys.set(key,{type: 'primative',typeof: ['unknown'],optional: true});continue;}// We've got the same type, so we can now resolve the changesswitch(type.type){case'object':
{if(new_type.type!=='object')continue;const{ resolved_key_info }=mergeKeyInfo(type.keys,new_type.keys);constresolved_key: InferenceType={type: 'object',keys: resolved_key_info,optional: type.optional||new_type.optional};constdid_change=JSON.stringify(resolved_key)!==JSON.stringify(type);if(did_change)changed_keys.set(key,resolved_key);}break;case'renderer':
{if(new_type.type!=='renderer')continue;constunion_map={
...type.renderers,
...new_type.renderers};consteither_optional=type.optional||new_type.optional;constresolved_key: InferenceType={type: 'renderer',renderers: union_map,optional: either_optional};constdid_change=JSON.stringify({
...resolved_key,renderers: Object.keys(resolved_key.renderers)})!==JSON.stringify({
...type,renderers: Object.keys(type.renderers)});if(did_change)changed_keys.set(key,resolved_key);}break;case'array':
{if(new_type.type!=='array')continue;switch(type.array_type){case'renderer':
{if(new_type.array_type!=='renderer'){// Type mismatchchanged_keys.set(key,{type: 'array',array_type: 'primitive',items: {type: 'primative',typeof: ['unknown'],optional: true},optional: true});continue;}constunion_map={
...type.renderers,
...new_type.renderers};consteither_optional=type.optional||new_type.optional;constresolved_key: InferenceType={type: 'array',array_type: 'renderer',renderers: union_map,optional: either_optional};constdid_change=JSON.stringify({
...resolved_key,renderers: Object.keys(resolved_key.renderers)})!==JSON.stringify({
...type,renderers: Object.keys(type.renderers)});if(did_change)changed_keys.set(key,resolved_key);}break;case'object':
{if(new_type.array_type==='primitive'&&new_type.items.typeof.length==1&&new_type.items.typeof[0]==='never'){// It's an empty array. We assume the type is unchangedcontinue;}if(new_type.array_type!=='object'){// Type mismatchchanged_keys.set(key,{type: 'array',array_type: 'primitive',items: {type: 'primative',typeof: ['unknown'],optional: true},optional: true});continue;}const{ resolved_key_info }=mergeKeyInfo(type.items.keys,new_type.items.keys);constresolved_key: InferenceType={type: 'array',array_type: 'object',items: {type: 'object',keys: resolved_key_info,optional: type.items.optional||new_type.items.optional},optional: type.optional||new_type.optional};constdid_change=JSON.stringify(resolved_key)!==JSON.stringify(type);if(did_change)changed_keys.set(key,resolved_key);}break;case'primitive':
{if(type.items.typeof.includes('never')&&new_type.array_type==='object'){// Type is now known from previosly unknownchanged_keys.set(key,new_type);continue;}if(new_type.array_type!=='primitive'){// Type mismatchchanged_keys.set(key,{type: 'array',array_type: 'primitive',items: {type: 'primative',typeof: ['unknown'],optional: true},optional: true});continue;}constkey_types=newSet([ ...new_type.items.typeof, ...type.items.typeof]);if(key_types.size>1&&key_types.has('never'))key_types.delete('never');constresolved_key: InferenceType={type: 'array',array_type: 'primitive',items: {type: 'primative',typeof: Array.from(key_types),optional: type.items.optional||new_type.items.optional},optional: type.optional||new_type.optional};constdid_change=JSON.stringify(resolved_key)!==JSON.stringify(type);if(did_change)changed_keys.set(key,resolved_key);}break;default:
thrownewError('Unreachable code reached! Switch missing case!');}}break;case'misc':
{if(new_type.type!=='misc')continue;if(type.misc_type!==new_type.misc_type){// We've got a type mismatch, this is unknown, we do not resolve unionschanged_keys.set(key,{type: 'primative',typeof: ['unknown'],optional: true});}switch(type.misc_type){case'Author':
{if(new_type.misc_type!=='Author')break;consthad_optional_param=type.params[1]||new_type.params[1];consteither_optional=type.optional||new_type.optional;constresolved_key: MiscInferenceType={type: 'misc',misc_type: 'Author',optional: either_optional,params: [new_type.params[0],had_optional_param]};constdid_change=JSON.stringify(resolved_key)!==JSON.stringify(type);if(did_change)changed_keys.set(key,resolved_key);}break;// Other cases can not change}}break;case'primative':
{if(new_type.type!=='primative')continue;constresolved_key: InferenceType={type: 'primative',typeof: Array.from(newSet([ ...new_type.typeof, ...type.typeof])),optional: type.optional||new_type.optional};constdid_change=JSON.stringify(resolved_key)!==JSON.stringify(type);if(did_change)changed_keys.set(key,resolved_key);}break;}}for(const[key,type]ofadded_keys){changed_keys.set(key,{
...type,optional: true});}for(const[key,type]ofremoved_keys){changed_keys.set(key,{
...type,optional: true});}constunchanged_keys=key_info.filter(([key])=>!changed_keys.has(key));constresolved_key_info_map=newMap([ ...unchanged_keys, ...changed_keys]);constresolved_key_info=[ ...resolved_key_info_map.entries()];return{
resolved_key_info,changed_keys: [ ...changed_keys.entries()]};}
importLogfrom'../utils/Log.js';import{deepCompare,ParsingError}from'../utils/Utils.js';constisObserved=Symbol('ObservedArray.isObserved');exportclassYTNode{staticreadonlytype: string='YTNode';readonlytype: string;constructor(){this.type=(this.constructorasYTNodeConstructor).type;}/** * Check if the node is of the given type. * @param type - The type to check * @returns whether the node is of the given type */
#is<TextendsYTNode>(type: YTNodeConstructor<T>): this is T{returnthis.type===type.type;}/** * Check if the node is of the given type. * @param types - The type to check * @returns whether the node is of the given type */is<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(...types: K): this is InstanceType<K[number]>{returntypes.some((type)=>this.#is(type));}/** * Cast to one of the given types. */as<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(...types: K): InstanceType<K[number]>{if(!this.is(...types)){thrownewParsingError(`Cannot cast ${this.type} to one of ${types.map((t)=>t.type).join(', ')}`);}returnthis;}/** * Check for a key without asserting the type. * @param key - The key to check * @returns Whether the node has the key */hasKey<Textendsstring,R=any>(key: T): this is this &{[kinT]: R}{returnReflect.has(this,key);}/** * Assert that the node has the given key and return it. * @param key - The key to check * @returns The value of the key wrapped in a Maybe * @throws If the node does not have the key */key<Textendsstring,R=any>(key: T){if(!this.hasKey<T,R>(key)){thrownewParsingError(`Missing key ${key}`);}returnnewMaybe(this[key]);}}exportclassMaybe{
#TAG ='Maybe';
#value;constructor(value: any){this.#value =value;}
#checkPrimative(type: 'string'|'number'|'bigint'|'boolean'|'symbol'|'undefined'|'object'|'function'){if(typeofthis.#value !==type){returnfalse;}returntrue;}
#assertPrimative(type: 'string'|'number'|'bigint'|'boolean'|'symbol'|'undefined'|'object'|'function'){if(!this.#checkPrimative(type)){thrownewTypeError(`Expected ${type}, got ${this.typeof}`);}returnthis.#value;}gettypeof(){returntypeofthis.#value;}string(): string{returnthis.#assertPrimative('string');}isString(){returnthis.#checkPrimative('string');}number(): number{returnthis.#assertPrimative('number');}isNumber(){returnthis.#checkPrimative('number');}bigint(): bigint{returnthis.#assertPrimative('bigint');}isBigint(){returnthis.#checkPrimative('bigint');}boolean(): boolean{returnthis.#assertPrimative('boolean');}isBoolean(){returnthis.#checkPrimative('boolean');}symbol(): symbol{returnthis.#assertPrimative('symbol');}isSymbol(){returnthis.#checkPrimative('symbol');}undefined(): undefined{returnthis.#assertPrimative('undefined');}isUndefined(){returnthis.#checkPrimative('undefined');}null(): null{if(this.#value !==null)thrownewTypeError(`Expected null, got ${typeofthis.#value}`);returnthis.#value;}isNull(){returnthis.#value ===null;}object(): object{returnthis.#assertPrimative('object');}isObject(){returnthis.#checkPrimative('object');}/* eslint-ignore */function(): Function{returnthis.#assertPrimative('function');}isFunction(){returnthis.#checkPrimative('function');}/** * Get the value as an array. * @returns the value as any[] * @throws If the value is not an array */array(): any[]{if(!Array.isArray(this.#value)){thrownewTypeError(`Expected array, got ${typeofthis.#value}`);}returnthis.#value;}/** * More typesafe variant of {@link Maybe#array}. * @returns a proxied array which returns all the values as {@link Maybe} * @throws If the value is not an array */arrayOfMaybe(): Maybe[]{constarrayProps: any[]=[];returnnewProxy(this.array(),{get(target,prop){if(Reflect.has(arrayProps,prop)){returnReflect.get(target,prop);}returnnewMaybe(Reflect.get(target,prop));}});}/** * Check whether the value is an array. * @returns whether the value is an array */isArray(){returnArray.isArray(this.#value);}/** * Get the value as a YTNode * @returns the value as a YTNode * @throws If the value is not a YTNode */node(){if(!(this.#value instanceofYTNode)){thrownewTypeError(`Expected YTNode, got ${this.#value.constructor.name}`);}returnthis.#value;}/** * Check if the value is a YTNode * @returns Whether the value is a YTNode */isNode(){returnthis.#value instanceofYTNode;}/** * Get the value as a YTNode of the given type. * @param type - The type to cast to * @returns The node casted to the given type * @throws If the node is not of the given type */nodeOfType<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(...types: K){returnthis.node().as(...types);}/** * Check if the value is a YTNode of the given type. * @param type - the type to check * @returns Whether the value is a YTNode of the given type */isNodeOfType<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(...types: K){returnthis.isNode()&&this.node().is(...types);}/** * Get the value as an ObservedArray. * @returns the value of the Maybe as a ObservedArray */observed(): ObservedArray<YTNode>{if(!this.isObserved()){thrownewTypeError(`Expected ObservedArray, got ${typeofthis.#value}`);}returnthis.#value;}/** * Check if the value is an ObservedArray. */isObserved(){returnthis.#value?.[isObserved];}/** * Get the value of the Maybe as a SuperParsedResult. * @returns the value as a SuperParsedResult * @throws If the value is not a SuperParsedResult */parsed(): SuperParsedResult{if(!(this.#value instanceofSuperParsedResult)){thrownewTypeError(`Expected SuperParsedResult, got ${typeofthis.#value}`);}returnthis.#value;}/** * Is the result a SuperParsedResult? */isParsed(){returnthis.#value instanceofSuperParsedResult;}/** * @deprecated * This call is not meant to be used outside of debugging. Please use the specific type getter instead. */any(): any{Log.warn(this.#TAG,'This call is not meant to be used outside of debugging. Please use the specific type getter instead.');returnthis.#value;}/** * Get the node as an instance of the given class. * @param type - The type to check * @returns the value as the given type * @throws If the node is not of the given type */instanceof<Textendsobject>(type: Constructor<T>): T{if(!this.isInstanceof(type)){thrownewTypeError(`Expected instance of ${type.name}, got ${this.#value.constructor.name}`);}returnthis.#value;}/** * Check if the node is an instance of the given class. * @param type - The type to check * @returns Whether the node is an instance of the given type */isInstanceof<Textendsobject>(type: Constructor<T>): this is this &T{returnthis.#value instanceoftype;}}exportinterfaceConstructor<T>{new(...args: any[]): T;}exportinterfaceYTNodeConstructor<TextendsYTNode=YTNode>{new(data: any): T;readonlytype: string;}/** * Represents a parsed response in an unknown state. Either a YTNode or a YTNode[] or null. */exportclassSuperParsedResult<TextendsYTNode=YTNode>{
#result;constructor(result: T|ObservedArray<T>|null){this.#result =result;}getis_null(){returnthis.#result ===null;}getis_array(){return!this.is_null&&Array.isArray(this.#result);}getis_node(){return!this.is_array;}array(){if(!this.is_array){thrownewTypeError('Expected an array, got a node');}returnthis.#result asObservedArray<T>;}item(){if(!this.is_node){thrownewTypeError('Expected a node, got an array');}returnthis.#result asT;}}exporttypeObservedArray<TextendsYTNode=YTNode>=Array<T>&{/** * Returns the first object to match the rule. */get: (rule: object,del_item?: boolean)=>T|undefined;/** * Returns all objects that match the rule. */getAll: (rule: object,del_items?: boolean)=>T[];/** * Returns the first object to match the condition. */matchCondition: (condition: (node: T)=>boolean)=>T|undefined;/** * Removes the item at the given index. */remove: (index: number)=>T[];/** * Get all items of a specific type */filterType<RextendsYTNode,KextendsYTNodeConstructor<R>[]>(...types: K): ObservedArray<InstanceType<K[number]>>;/** * Get the first of a specific type */firstOfType<RextendsYTNode,KextendsYTNodeConstructor<R>[]>(...types: K): InstanceType<K[number]>|undefined;/** * Get the first item */first: ()=>T;/** * This is similar to filter but throws if there's a type mismatch. */as<RextendsYTNode,KextendsYTNodeConstructor<R>[]>(...types: K): ObservedArray<InstanceType<K[number]>>;};/** * Creates a trap to intercept property access * and add utilities to an object. */exportfunctionobserve<TextendsYTNode>(obj: Array<T>): ObservedArray<T>{returnnewProxy(obj,{get(target,prop){if(prop=='get'){return(rule: object,del_item?: boolean)=>(target.find((obj,index)=>{constmatch=deepCompare(rule,obj);if(match&&del_item){target.splice(index,1);}returnmatch;}));}if(prop==isObserved){returntrue;}if(prop=='getAll'){return(rule: object,del_items: boolean)=>(target.filter((obj,index)=>{constmatch=deepCompare(rule,obj);if(match&&del_items){target.splice(index,1);}returnmatch;}));}if(prop=='matchCondition'){return(condition: (node: T)=>boolean)=>(target.find((obj)=>{returncondition(obj);}));}if(prop=='filterType'){return(...types: YTNodeConstructor<YTNode>[])=>{returnobserve(target.filter((node: YTNode)=>{if(node.is(...types))returntrue;returnfalse;}));};}if(prop=='firstOfType'){return(...types: YTNodeConstructor<YTNode>[])=>{returntarget.find((node: YTNode)=>{if(node.is(...types))returntrue;returnfalse;});};}if(prop=='first'){return()=>target[0];}if(prop=='as'){return(...types: YTNodeConstructor<YTNode>[])=>{returnobserve(target.map((node: YTNode)=>{if(node.is(...types))returnnode;thrownewParsingError(`Expected node of any type ${types.map((type)=>type.type).join(', ')}, got ${(nodeasYTNode).type}`);}));};}if(prop=='remove'){return(index: number): any=>target.splice(index,1);}returnReflect.get(target,prop);}})asObservedArray<T>;}exportclassMemoextendsMap<string,YTNode[]>{getType<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(types: K): ObservedArray<InstanceType<K[number]>>;getType<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(...types: K): ObservedArray<InstanceType<K[number]>>getType(...types: YTNodeConstructor<YTNode>[]|YTNodeConstructor<YTNode>[][]){types=types.flat();returnobserve(types.flatMap((type)=>(this.get(type.type)||[])asYTNode[]));}}
// This file was auto generated, do not edit.// See ./scripts/build-parser-map.jsexport{defaultasAuthor}from'./classes/misc/Author.js';export{defaultasChildElement}from'./classes/misc/ChildElement.js';export{defaultasEmojiRun}from'./classes/misc/EmojiRun.js';export{defaultasFormat}from'./classes/misc/Format.js';export{defaultasText}from'./classes/misc/Text.js';export{defaultasTextRun}from'./classes/misc/TextRun.js';export{defaultasThumbnail}from'./classes/misc/Thumbnail.js';export{defaultasVideoDetails}from'./classes/misc/VideoDetails.js';
LuanRT/YouTube.js/blob/main/src/parser/nodes.ts:
// This file was auto generated, do not edit.// See ./scripts/build-parser-map.jsexport{defaultasAboutChannel}from'./classes/AboutChannel.js';export{defaultasAboutChannelView}from'./classes/AboutChannelView.js';export{defaultasAccountChannel}from'./classes/AccountChannel.js';export{defaultasAccountItemSection}from'./classes/AccountItemSection.js';export{defaultasAccountItemSectionHeader}from'./classes/AccountItemSectionHeader.js';export{defaultasAccountSectionList}from'./classes/AccountSectionList.js';export{defaultasAppendContinuationItemsAction}from'./classes/actions/AppendContinuationItemsAction.js';export{defaultasOpenPopupAction}from'./classes/actions/OpenPopupAction.js';export{defaultasUpdateEngagementPanelAction}from'./classes/actions/UpdateEngagementPanelAction.js';export{defaultasAlert}from'./classes/Alert.js';export{defaultasAlertWithButton}from'./classes/AlertWithButton.js';export{defaultasAnalyticsMainAppKeyMetrics}from'./classes/analytics/AnalyticsMainAppKeyMetrics.js';export{defaultasAnalyticsRoot}from'./classes/analytics/AnalyticsRoot.js';export{defaultasAnalyticsShortsCarouselCard}from'./classes/analytics/AnalyticsShortsCarouselCard.js';export{defaultasAnalyticsVideo}from'./classes/analytics/AnalyticsVideo.js';export{defaultasAnalyticsVodCarouselCard}from'./classes/analytics/AnalyticsVodCarouselCard.js';export{defaultasCtaGoToCreatorStudio}from'./classes/analytics/CtaGoToCreatorStudio.js';export{defaultasDataModelSection}from'./classes/analytics/DataModelSection.js';export{defaultasStatRow}from'./classes/analytics/StatRow.js';export{defaultasAttributionView}from'./classes/AttributionView.js';export{defaultasAudioOnlyPlayability}from'./classes/AudioOnlyPlayability.js';export{defaultasAutomixPreviewVideo}from'./classes/AutomixPreviewVideo.js';export{defaultasAvatarView}from'./classes/AvatarView.js';export{defaultasBackstageImage}from'./classes/BackstageImage.js';export{defaultasBackstagePost}from'./classes/BackstagePost.js';export{defaultasBackstagePostThread}from'./classes/BackstagePostThread.js';export{defaultasBrowseFeedActions}from'./classes/BrowseFeedActions.js';export{defaultasBrowserMediaSession}from'./classes/BrowserMediaSession.js';export{defaultasButton}from'./classes/Button.js';export{defaultasButtonView}from'./classes/ButtonView.js';export{defaultasC4TabbedHeader}from'./classes/C4TabbedHeader.js';export{defaultasCallToActionButton}from'./classes/CallToActionButton.js';export{defaultasCard}from'./classes/Card.js';export{defaultasCardCollection}from'./classes/CardCollection.js';export{defaultasCarouselHeader}from'./classes/CarouselHeader.js';export{defaultasCarouselItem}from'./classes/CarouselItem.js';export{defaultasCarouselLockup}from'./classes/CarouselLockup.js';export{defaultasChannel}from'./classes/Channel.js';export{defaultasChannelAboutFullMetadata}from'./classes/ChannelAboutFullMetadata.js';export{defaultasChannelAgeGate}from'./classes/ChannelAgeGate.js';export{defaultasChannelExternalLinkView}from'./classes/ChannelExternalLinkView.js';export{defaultasChannelFeaturedContent}from'./classes/ChannelFeaturedContent.js';export{defaultasChannelHeaderLinks}from'./classes/ChannelHeaderLinks.js';export{defaultasChannelHeaderLinksView}from'./classes/ChannelHeaderLinksView.js';export{defaultasChannelMetadata}from'./classes/ChannelMetadata.js';export{defaultasChannelMobileHeader}from'./classes/ChannelMobileHeader.js';export{defaultasChannelOptions}from'./classes/ChannelOptions.js';export{defaultasChannelOwnerEmptyState}from'./classes/ChannelOwnerEmptyState.js';export{defaultasChannelSubMenu}from'./classes/ChannelSubMenu.js';export{defaultasChannelTagline}from'./classes/ChannelTagline.js';export{defaultasChannelThumbnailWithLink}from'./classes/ChannelThumbnailWithLink.js';export{defaultasChannelVideoPlayer}from'./classes/ChannelVideoPlayer.js';export{defaultasChapter}from'./classes/Chapter.js';export{defaultasChildVideo}from'./classes/ChildVideo.js';export{defaultasChipBarView}from'./classes/ChipBarView.js';export{defaultasChipCloud}from'./classes/ChipCloud.js';export{defaultasChipCloudChip}from'./classes/ChipCloudChip.js';export{defaultasChipView}from'./classes/ChipView.js';export{defaultasClipAdState}from'./classes/ClipAdState.js';export{defaultasClipCreation}from'./classes/ClipCreation.js';export{defaultasClipCreationScrubber}from'./classes/ClipCreationScrubber.js';export{defaultasClipCreationTextInput}from'./classes/ClipCreationTextInput.js';export{defaultasClipSection}from'./classes/ClipSection.js';export{defaultasCollaboratorInfoCardContent}from'./classes/CollaboratorInfoCardContent.js';export{defaultasCollageHeroImage}from'./classes/CollageHeroImage.js';export{defaultasCollectionThumbnailView}from'./classes/CollectionThumbnailView.js';export{defaultasAuthorCommentBadge}from'./classes/comments/AuthorCommentBadge.js';export{defaultasComment}from'./classes/comments/Comment.js';export{defaultasCommentActionButtons}from'./classes/comments/CommentActionButtons.js';export{defaultasCommentDialog}from'./classes/comments/CommentDialog.js';export{defaultasCommentReplies}from'./classes/comments/CommentReplies.js';export{defaultasCommentReplyDialog}from'./classes/comments/CommentReplyDialog.js';export{defaultasCommentsEntryPointHeader}from'./classes/comments/CommentsEntryPointHeader.js';export{defaultasCommentsEntryPointTeaser}from'./classes/comments/CommentsEntryPointTeaser.js';export{defaultasCommentsHeader}from'./classes/comments/CommentsHeader.js';export{defaultasCommentSimplebox}from'./classes/comments/CommentSimplebox.js';export{defaultasCommentsSimplebox}from'./classes/comments/CommentsSimplebox.js';export{defaultasCommentThread}from'./classes/comments/CommentThread.js';export{defaultasCommentView}from'./classes/comments/CommentView.js';export{defaultasCreatorHeart}from'./classes/comments/CreatorHeart.js';export{defaultasEmojiPicker}from'./classes/comments/EmojiPicker.js';export{defaultasPdgCommentChip}from'./classes/comments/PdgCommentChip.js';export{defaultasSponsorCommentBadge}from'./classes/comments/SponsorCommentBadge.js';export{defaultasCompactChannel}from'./classes/CompactChannel.js';export{defaultasCompactLink}from'./classes/CompactLink.js';export{defaultasCompactMix}from'./classes/CompactMix.js';export{defaultasCompactMovie}from'./classes/CompactMovie.js';export{defaultasCompactPlaylist}from'./classes/CompactPlaylist.js';export{defaultasCompactStation}from'./classes/CompactStation.js';export{defaultasCompactVideo}from'./classes/CompactVideo.js';export{defaultasConfirmDialog}from'./classes/ConfirmDialog.js';export{defaultasContentMetadataView}from'./classes/ContentMetadataView.js';export{defaultasContentPreviewImageView}from'./classes/ContentPreviewImageView.js';export{defaultasContinuationItem}from'./classes/ContinuationItem.js';export{defaultasConversationBar}from'./classes/ConversationBar.js';export{defaultasCopyLink}from'./classes/CopyLink.js';export{defaultasCreatePlaylistDialog}from'./classes/CreatePlaylistDialog.js';export{defaultasDecoratedAvatarView}from'./classes/DecoratedAvatarView.js';export{defaultasDecoratedPlayerBar}from'./classes/DecoratedPlayerBar.js';export{defaultasDefaultPromoPanel}from'./classes/DefaultPromoPanel.js';export{defaultasDescriptionPreviewView}from'./classes/DescriptionPreviewView.js';export{defaultasDidYouMean}from'./classes/DidYouMean.js';export{defaultasDislikeButtonView}from'./classes/DislikeButtonView.js';export{defaultasDownloadButton}from'./classes/DownloadButton.js';export{defaultasDropdown}from'./classes/Dropdown.js';export{defaultasDropdownItem}from'./classes/DropdownItem.js';export{defaultasDynamicTextView}from'./classes/DynamicTextView.js';export{defaultasElement}from'./classes/Element.js';export{defaultasEmergencyOnebox}from'./classes/EmergencyOnebox.js';export{defaultasEmojiPickerCategory}from'./classes/EmojiPickerCategory.js';export{defaultasEmojiPickerCategoryButton}from'./classes/EmojiPickerCategoryButton.js';export{defaultasEmojiPickerUpsellCategory}from'./classes/EmojiPickerUpsellCategory.js';export{defaultasEndscreen}from'./classes/Endscreen.js';export{defaultasEndscreenElement}from'./classes/EndscreenElement.js';export{defaultasEndScreenPlaylist}from'./classes/EndScreenPlaylist.js';export{defaultasEndScreenVideo}from'./classes/EndScreenVideo.js';export{defaultasEngagementPanelSectionList}from'./classes/EngagementPanelSectionList.js';export{defaultasEngagementPanelTitleHeader}from'./classes/EngagementPanelTitleHeader.js';export{defaultasEomSettingsDisclaimer}from'./classes/EomSettingsDisclaimer.js';export{defaultasExpandableMetadata}from'./classes/ExpandableMetadata.js';export{defaultasExpandableTab}from'./classes/ExpandableTab.js';export{defaultasExpandableVideoDescriptionBody}from'./classes/ExpandableVideoDescriptionBody.js';export{defaultasExpandedShelfContents}from'./classes/ExpandedShelfContents.js';export{defaultasFactoid}from'./classes/Factoid.js';export{defaultasFancyDismissibleDialog}from'./classes/FancyDismissibleDialog.js';export{defaultasFeedFilterChipBar}from'./classes/FeedFilterChipBar.js';export{defaultasFeedNudge}from'./classes/FeedNudge.js';export{defaultasFeedTabbedHeader}from'./classes/FeedTabbedHeader.js';export{defaultasFlexibleActionsView}from'./classes/FlexibleActionsView.js';export{defaultasGameCard}from'./classes/GameCard.js';export{defaultasGameDetails}from'./classes/GameDetails.js';export{defaultasGrid}from'./classes/Grid.js';export{defaultasGridChannel}from'./classes/GridChannel.js';export{defaultasGridHeader}from'./classes/GridHeader.js';export{defaultasGridMix}from'./classes/GridMix.js';export{defaultasGridMovie}from'./classes/GridMovie.js';export{defaultasGridPlaylist}from'./classes/GridPlaylist.js';export{defaultasGridShow}from'./classes/GridShow.js';export{defaultasGridVideo}from'./classes/GridVideo.js';export{defaultasGuideCollapsibleEntry}from'./classes/GuideCollapsibleEntry.js';export{defaultasGuideCollapsibleSectionEntry}from'./classes/GuideCollapsibleSectionEntry.js';export{defaultasGuideDownloadsEntry}from'./classes/GuideDownloadsEntry.js';export{defaultasGuideEntry}from'./classes/GuideEntry.js';export{defaultasGuideSection}from'./classes/GuideSection.js';export{defaultasGuideSubscriptionsSection}from'./classes/GuideSubscriptionsSection.js';export{defaultasHashtagHeader}from'./classes/HashtagHeader.js';export{defaultasHashtagTile}from'./classes/HashtagTile.js';export{defaultasHeatmap}from'./classes/Heatmap.js';export{defaultasHeatMarker}from'./classes/HeatMarker.js';export{defaultasHeroPlaylistThumbnail}from'./classes/HeroPlaylistThumbnail.js';export{defaultasHighlightsCarousel}from'./classes/HighlightsCarousel.js';export{defaultasHistorySuggestion}from'./classes/HistorySuggestion.js';export{defaultasHorizontalCardList}from'./classes/HorizontalCardList.js';export{defaultasHorizontalList}from'./classes/HorizontalList.js';export{defaultasHorizontalMovieList}from'./classes/HorizontalMovieList.js';export{defaultasIconLink}from'./classes/IconLink.js';export{defaultasImageBannerView}from'./classes/ImageBannerView.js';export{defaultasIncludingResultsFor}from'./classes/IncludingResultsFor.js';export{defaultasInfoPanelContainer}from'./classes/InfoPanelContainer.js';export{defaultasInfoPanelContent}from'./classes/InfoPanelContent.js';export{defaultasInfoRow}from'./classes/InfoRow.js';export{defaultasInteractiveTabbedHeader}from'./classes/InteractiveTabbedHeader.js';export{defaultasItemSection}from'./classes/ItemSection.js';export{defaultasItemSectionHeader}from'./classes/ItemSectionHeader.js';export{defaultasItemSectionTab}from'./classes/ItemSectionTab.js';export{defaultasItemSectionTabbedHeader}from'./classes/ItemSectionTabbedHeader.js';export{defaultasLikeButton}from'./classes/LikeButton.js';export{defaultasLikeButtonView}from'./classes/LikeButtonView.js';export{defaultasLiveChat}from'./classes/LiveChat.js';export{defaultasAddBannerToLiveChatCommand}from'./classes/livechat/AddBannerToLiveChatCommand.js';export{defaultasAddChatItemAction}from'./classes/livechat/AddChatItemAction.js';export{defaultasAddLiveChatTickerItemAction}from'./classes/livechat/AddLiveChatTickerItemAction.js';export{defaultasDimChatItemAction}from'./classes/livechat/DimChatItemAction.js';export{defaultasLiveChatAutoModMessage}from'./classes/livechat/items/LiveChatAutoModMessage.js';export{defaultasLiveChatBanner}from'./classes/livechat/items/LiveChatBanner.js';export{defaultasLiveChatBannerHeader}from'./classes/livechat/items/LiveChatBannerHeader.js';export{defaultasLiveChatBannerPoll}from'./classes/livechat/items/LiveChatBannerPoll.js';export{defaultasLiveChatMembershipItem}from'./classes/livechat/items/LiveChatMembershipItem.js';export{defaultasLiveChatPaidMessage}from'./classes/livechat/items/LiveChatPaidMessage.js';export{defaultasLiveChatPaidSticker}from'./classes/livechat/items/LiveChatPaidSticker.js';export{defaultasLiveChatPlaceholderItem}from'./classes/livechat/items/LiveChatPlaceholderItem.js';export{defaultasLiveChatProductItem}from'./classes/livechat/items/LiveChatProductItem.js';export{defaultasLiveChatRestrictedParticipation}from'./classes/livechat/items/LiveChatRestrictedParticipation.js';export{defaultasLiveChatTextMessage}from'./classes/livechat/items/LiveChatTextMessage.js';export{defaultasLiveChatTickerPaidMessageItem}from'./classes/livechat/items/LiveChatTickerPaidMessageItem.js';export{defaultasLiveChatTickerPaidStickerItem}from'./classes/livechat/items/LiveChatTickerPaidStickerItem.js';export{defaultasLiveChatTickerSponsorItem}from'./classes/livechat/items/LiveChatTickerSponsorItem.js';export{defaultasLiveChatViewerEngagementMessage}from'./classes/livechat/items/LiveChatViewerEngagementMessage.js';export{defaultasPollHeader}from'./classes/livechat/items/PollHeader.js';export{defaultasLiveChatActionPanel}from'./classes/livechat/LiveChatActionPanel.js';export{defaultasMarkChatItemAsDeletedAction}from'./classes/livechat/MarkChatItemAsDeletedAction.js';export{defaultasMarkChatItemsByAuthorAsDeletedAction}from'./classes/livechat/MarkChatItemsByAuthorAsDeletedAction.js';export{defaultasRemoveBannerForLiveChatCommand}from'./classes/livechat/RemoveBannerForLiveChatCommand.js';export{defaultasRemoveChatItemAction}from'./classes/livechat/RemoveChatItemAction.js';export{defaultasRemoveChatItemByAuthorAction}from'./classes/livechat/RemoveChatItemByAuthorAction.js';export{defaultasReplaceChatItemAction}from'./classes/livechat/ReplaceChatItemAction.js';export{defaultasReplayChatItemAction}from'./classes/livechat/ReplayChatItemAction.js';export{defaultasShowLiveChatActionPanelAction}from'./classes/livechat/ShowLiveChatActionPanelAction.js';export{defaultasShowLiveChatDialogAction}from'./classes/livechat/ShowLiveChatDialogAction.js';export{defaultasShowLiveChatTooltipCommand}from'./classes/livechat/ShowLiveChatTooltipCommand.js';export{defaultasUpdateDateTextAction}from'./classes/livechat/UpdateDateTextAction.js';export{defaultasUpdateDescriptionAction}from'./classes/livechat/UpdateDescriptionAction.js';export{defaultasUpdateLiveChatPollAction}from'./classes/livechat/UpdateLiveChatPollAction.js';export{defaultasUpdateTitleAction}from'./classes/livechat/UpdateTitleAction.js';export{defaultasUpdateToggleButtonTextAction}from'./classes/livechat/UpdateToggleButtonTextAction.js';export{defaultasUpdateViewershipAction}from'./classes/livechat/UpdateViewershipAction.js';export{defaultasLiveChatAuthorBadge}from'./classes/LiveChatAuthorBadge.js';export{defaultasLiveChatDialog}from'./classes/LiveChatDialog.js';export{defaultasLiveChatHeader}from'./classes/LiveChatHeader.js';export{defaultasLiveChatItemList}from'./classes/LiveChatItemList.js';export{defaultasLiveChatMessageInput}from'./classes/LiveChatMessageInput.js';export{defaultasLiveChatParticipant}from'./classes/LiveChatParticipant.js';export{defaultasLiveChatParticipantsList}from'./classes/LiveChatParticipantsList.js';export{defaultasLockupMetadataView}from'./classes/LockupMetadataView.js';export{defaultasLockupView}from'./classes/LockupView.js';export{defaultasMacroMarkersInfoItem}from'./classes/MacroMarkersInfoItem.js';export{defaultasMacroMarkersList}from'./classes/MacroMarkersList.js';export{defaultasMacroMarkersListItem}from'./classes/MacroMarkersListItem.js';export{defaultasMenu}from'./classes/menus/Menu.js';export{defaultasMenuNavigationItem}from'./classes/menus/MenuNavigationItem.js';export{defaultasMenuPopup}from'./classes/menus/MenuPopup.js';export{defaultasMenuServiceItem}from'./classes/menus/MenuServiceItem.js';export{defaultasMenuServiceItemDownload}from'./classes/menus/MenuServiceItemDownload.js';export{defaultasMultiPageMenu}from'./classes/menus/MultiPageMenu.js';export{defaultasMultiPageMenuNotificationSection}from'./classes/menus/MultiPageMenuNotificationSection.js';export{defaultasMusicMenuItemDivider}from'./classes/menus/MusicMenuItemDivider.js';export{defaultasMusicMultiSelectMenu}from'./classes/menus/MusicMultiSelectMenu.js';export{defaultasMusicMultiSelectMenuItem}from'./classes/menus/MusicMultiSelectMenuItem.js';export{defaultasSimpleMenuHeader}from'./classes/menus/SimpleMenuHeader.js';export{defaultasMerchandiseItem}from'./classes/MerchandiseItem.js';export{defaultasMerchandiseShelf}from'./classes/MerchandiseShelf.js';export{defaultasMessage}from'./classes/Message.js';export{defaultasMetadataBadge}from'./classes/MetadataBadge.js';export{defaultasMetadataRow}from'./classes/MetadataRow.js';export{defaultasMetadataRowContainer}from'./classes/MetadataRowContainer.js';export{defaultasMetadataRowHeader}from'./classes/MetadataRowHeader.js';export{defaultasMetadataScreen}from'./classes/MetadataScreen.js';export{defaultasMicroformatData}from'./classes/MicroformatData.js';export{defaultasMix}from'./classes/Mix.js';export{defaultasModalWithTitleAndButton}from'./classes/ModalWithTitleAndButton.js';export{defaultasMovie}from'./classes/Movie.js';export{defaultasMovingThumbnail}from'./classes/MovingThumbnail.js';export{defaultasMultiMarkersPlayerBar}from'./classes/MultiMarkersPlayerBar.js';export{defaultasMusicCardShelf}from'./classes/MusicCardShelf.js';export{defaultasMusicCardShelfHeaderBasic}from'./classes/MusicCardShelfHeaderBasic.js';export{defaultasMusicCarouselShelf}from'./classes/MusicCarouselShelf.js';export{defaultasMusicCarouselShelfBasicHeader}from'./classes/MusicCarouselShelfBasicHeader.js';export{defaultasMusicDescriptionShelf}from'./classes/MusicDescriptionShelf.js';export{defaultasMusicDetailHeader}from'./classes/MusicDetailHeader.js';export{defaultasMusicDownloadStateBadge}from'./classes/MusicDownloadStateBadge.js';export{defaultasMusicEditablePlaylistDetailHeader}from'./classes/MusicEditablePlaylistDetailHeader.js';export{defaultasMusicElementHeader}from'./classes/MusicElementHeader.js';export{defaultasMusicHeader}from'./classes/MusicHeader.js';export{defaultasMusicImmersiveHeader}from'./classes/MusicImmersiveHeader.js';export{defaultasMusicInlineBadge}from'./classes/MusicInlineBadge.js';export{defaultasMusicItemThumbnailOverlay}from'./classes/MusicItemThumbnailOverlay.js';export{defaultasMusicLargeCardItemCarousel}from'./classes/MusicLargeCardItemCarousel.js';export{defaultasMusicMultiRowListItem}from'./classes/MusicMultiRowListItem.js';export{defaultasMusicNavigationButton}from'./classes/MusicNavigationButton.js';export{defaultasMusicPlayButton}from'./classes/MusicPlayButton.js';export{defaultasMusicPlaylistEditHeader}from'./classes/MusicPlaylistEditHeader.js';export{defaultasMusicPlaylistShelf}from'./classes/MusicPlaylistShelf.js';export{defaultasMusicQueue}from'./classes/MusicQueue.js';export{defaultasMusicResponsiveHeader}from'./classes/MusicResponsiveHeader.js';export{defaultasMusicResponsiveListItem}from'./classes/MusicResponsiveListItem.js';export{defaultasMusicResponsiveListItemFixedColumn}from'./classes/MusicResponsiveListItemFixedColumn.js';export{defaultasMusicResponsiveListItemFlexColumn}from'./classes/MusicResponsiveListItemFlexColumn.js';export{defaultasMusicShelf}from'./classes/MusicShelf.js';export{defaultasMusicSideAlignedItem}from'./classes/MusicSideAlignedItem.js';export{defaultasMusicSortFilterButton}from'./classes/MusicSortFilterButton.js';export{defaultasMusicTastebuilderShelf}from'./classes/MusicTastebuilderShelf.js';export{defaultasMusicTastebuilderShelfThumbnail}from'./classes/MusicTastebuilderShelfThumbnail.js';export{defaultasMusicThumbnail}from'./classes/MusicThumbnail.js';export{defaultasMusicTwoRowItem}from'./classes/MusicTwoRowItem.js';export{defaultasMusicVisualHeader}from'./classes/MusicVisualHeader.js';export{defaultasNavigationEndpoint}from'./classes/NavigationEndpoint.js';export{defaultasNotification}from'./classes/Notification.js';export{defaultasPageHeader}from'./classes/PageHeader.js';export{defaultasPageHeaderView}from'./classes/PageHeaderView.js';export{defaultasPageIntroduction}from'./classes/PageIntroduction.js';export{defaultasPivotButton}from'./classes/PivotButton.js';export{defaultasPlayerAnnotationsExpanded}from'./classes/PlayerAnnotationsExpanded.js';export{defaultasPlayerCaptionsTracklist}from'./classes/PlayerCaptionsTracklist.js';export{defaultasPlayerControlsOverlay}from'./classes/PlayerControlsOverlay.js';export{defaultasPlayerErrorMessage}from'./classes/PlayerErrorMessage.js';export{defaultasPlayerLegacyDesktopYpcOffer}from'./classes/PlayerLegacyDesktopYpcOffer.js';export{defaultasPlayerLegacyDesktopYpcTrailer}from'./classes/PlayerLegacyDesktopYpcTrailer.js';export{defaultasPlayerLiveStoryboardSpec}from'./classes/PlayerLiveStoryboardSpec.js';export{defaultasPlayerMicroformat}from'./classes/PlayerMicroformat.js';export{defaultasPlayerOverflow}from'./classes/PlayerOverflow.js';export{defaultasPlayerOverlay}from'./classes/PlayerOverlay.js';export{defaultasPlayerOverlayAutoplay}from'./classes/PlayerOverlayAutoplay.js';export{defaultasPlayerStoryboardSpec}from'./classes/PlayerStoryboardSpec.js';export{defaultasPlaylist}from'./classes/Playlist.js';export{defaultasPlaylistCustomThumbnail}from'./classes/PlaylistCustomThumbnail.js';export{defaultasPlaylistHeader}from'./classes/PlaylistHeader.js';export{defaultasPlaylistInfoCardContent}from'./classes/PlaylistInfoCardContent.js';export{defaultasPlaylistMetadata}from'./classes/PlaylistMetadata.js';export{defaultasPlaylistPanel}from'./classes/PlaylistPanel.js';export{defaultasPlaylistPanelVideo}from'./classes/PlaylistPanelVideo.js';export{defaultasPlaylistPanelVideoWrapper}from'./classes/PlaylistPanelVideoWrapper.js';export{defaultasPlaylistSidebar}from'./classes/PlaylistSidebar.js';export{defaultasPlaylistSidebarPrimaryInfo}from'./classes/PlaylistSidebarPrimaryInfo.js';export{defaultasPlaylistSidebarSecondaryInfo}from'./classes/PlaylistSidebarSecondaryInfo.js';export{defaultasPlaylistVideo}from'./classes/PlaylistVideo.js';export{defaultasPlaylistVideoList}from'./classes/PlaylistVideoList.js';export{defaultasPlaylistVideoThumbnail}from'./classes/PlaylistVideoThumbnail.js';export{defaultasPoll}from'./classes/Poll.js';export{defaultasPost}from'./classes/Post.js';export{defaultasPostMultiImage}from'./classes/PostMultiImage.js';export{defaultasProductList}from'./classes/ProductList.js';export{defaultasProductListHeader}from'./classes/ProductListHeader.js';export{defaultasProductListItem}from'./classes/ProductListItem.js';export{defaultasProfileColumn}from'./classes/ProfileColumn.js';export{defaultasProfileColumnStats}from'./classes/ProfileColumnStats.js';export{defaultasProfileColumnStatsEntry}from'./classes/ProfileColumnStatsEntry.js';export{defaultasProfileColumnUserInfo}from'./classes/ProfileColumnUserInfo.js';export{defaultasQuiz}from'./classes/Quiz.js';export{defaultasRecognitionShelf}from'./classes/RecognitionShelf.js';export{defaultasReelItem}from'./classes/ReelItem.js';export{defaultasReelPlayerHeader}from'./classes/ReelPlayerHeader.js';export{defaultasReelPlayerOverlay}from'./classes/ReelPlayerOverlay.js';export{defaultasReelShelf}from'./classes/ReelShelf.js';export{defaultasRelatedChipCloud}from'./classes/RelatedChipCloud.js';export{defaultasRichGrid}from'./classes/RichGrid.js';export{defaultasRichItem}from'./classes/RichItem.js';export{defaultasRichListHeader}from'./classes/RichListHeader.js';export{defaultasRichMetadata}from'./classes/RichMetadata.js';export{defaultasRichMetadataRow}from'./classes/RichMetadataRow.js';export{defaultasRichSection}from'./classes/RichSection.js';export{defaultasRichShelf}from'./classes/RichShelf.js';export{defaultasSearchBox}from'./classes/SearchBox.js';export{defaultasSearchFilter}from'./classes/SearchFilter.js';export{defaultasSearchFilterGroup}from'./classes/SearchFilterGroup.js';export{defaultasSearchFilterOptionsDialog}from'./classes/SearchFilterOptionsDialog.js';export{defaultasSearchHeader}from'./classes/SearchHeader.js';export{defaultasSearchRefinementCard}from'./classes/SearchRefinementCard.js';export{defaultasSearchSubMenu}from'./classes/SearchSubMenu.js';export{defaultasSearchSuggestion}from'./classes/SearchSuggestion.js';export{defaultasSearchSuggestionsSection}from'./classes/SearchSuggestionsSection.js';export{defaultasSecondarySearchContainer}from'./classes/SecondarySearchContainer.js';export{defaultasSectionList}from'./classes/SectionList.js';export{defaultasSegmentedLikeDislikeButton}from'./classes/SegmentedLikeDislikeButton.js';export{defaultasSegmentedLikeDislikeButtonView}from'./classes/SegmentedLikeDislikeButtonView.js';export{defaultasSettingBoolean}from'./classes/SettingBoolean.js';export{defaultasSettingsCheckbox}from'./classes/SettingsCheckbox.js';export{defaultasSettingsOptions}from'./classes/SettingsOptions.js';export{defaultasSettingsSidebar}from'./classes/SettingsSidebar.js';export{defaultasSettingsSwitch}from'./classes/SettingsSwitch.js';export{defaultasSharedPost}from'./classes/SharedPost.js';export{defaultasShelf}from'./classes/Shelf.js';export{defaultasShowCustomThumbnail}from'./classes/ShowCustomThumbnail.js';export{defaultasShowingResultsFor}from'./classes/ShowingResultsFor.js';export{defaultasSimpleCardContent}from'./classes/SimpleCardContent.js';export{defaultasSimpleCardTeaser}from'./classes/SimpleCardTeaser.js';export{defaultasSimpleTextSection}from'./classes/SimpleTextSection.js';export{defaultasSingleActionEmergencySupport}from'./classes/SingleActionEmergencySupport.js';export{defaultasSingleColumnBrowseResults}from'./classes/SingleColumnBrowseResults.js';export{defaultasSingleColumnMusicWatchNextResults}from'./classes/SingleColumnMusicWatchNextResults.js';export{defaultasSingleHeroImage}from'./classes/SingleHeroImage.js';export{defaultasSlimOwner}from'./classes/SlimOwner.js';export{defaultasSlimVideoMetadata}from'./classes/SlimVideoMetadata.js';export{defaultasSortFilterHeader}from'./classes/SortFilterHeader.js';export{defaultasSortFilterSubMenu}from'./classes/SortFilterSubMenu.js';export{defaultasStructuredDescriptionContent}from'./classes/StructuredDescriptionContent.js';export{defaultasStructuredDescriptionPlaylistLockup}from'./classes/StructuredDescriptionPlaylistLockup.js';export{defaultasSubFeedOption}from'./classes/SubFeedOption.js';export{defaultasSubFeedSelector}from'./classes/SubFeedSelector.js';export{defaultasSubscribeButton}from'./classes/SubscribeButton.js';export{defaultasSubscriptionNotificationToggleButton}from'./classes/SubscriptionNotificationToggleButton.js';export{defaultasTab}from'./classes/Tab.js';export{defaultasTabbed}from'./classes/Tabbed.js';export{defaultasTabbedSearchResults}from'./classes/TabbedSearchResults.js';export{defaultasTextHeader}from'./classes/TextHeader.js';export{defaultasThumbnailBadgeView}from'./classes/ThumbnailBadgeView.js';export{defaultasThumbnailHoverOverlayView}from'./classes/ThumbnailHoverOverlayView.js';export{defaultasThumbnailLandscapePortrait}from'./classes/ThumbnailLandscapePortrait.js';export{defaultasThumbnailOverlayBadgeView}from'./classes/ThumbnailOverlayBadgeView.js';export{defaultasThumbnailOverlayBottomPanel}from'./classes/ThumbnailOverlayBottomPanel.js';export{defaultasThumbnailOverlayEndorsement}from'./classes/ThumbnailOverlayEndorsement.js';export{defaultasThumbnailOverlayHoverText}from'./classes/ThumbnailOverlayHoverText.js';export{defaultasThumbnailOverlayInlineUnplayable}from'./classes/ThumbnailOverlayInlineUnplayable.js';export{defaultasThumbnailOverlayLoadingPreview}from'./classes/ThumbnailOverlayLoadingPreview.js';export{defaultasThumbnailOverlayNowPlaying}from'./classes/ThumbnailOverlayNowPlaying.js';export{defaultasThumbnailOverlayPinking}from'./classes/ThumbnailOverlayPinking.js';export{defaultasThumbnailOverlayPlaybackStatus}from'./classes/ThumbnailOverlayPlaybackStatus.js';export{defaultasThumbnailOverlayResumePlayback}from'./classes/ThumbnailOverlayResumePlayback.js';export{defaultasThumbnailOverlaySidePanel}from'./classes/ThumbnailOverlaySidePanel.js';export{defaultasThumbnailOverlayTimeStatus}from'./classes/ThumbnailOverlayTimeStatus.js';export{defaultasThumbnailOverlayToggleButton}from'./classes/ThumbnailOverlayToggleButton.js';export{defaultasThumbnailView}from'./classes/ThumbnailView.js';export{defaultasTimedMarkerDecoration}from'./classes/TimedMarkerDecoration.js';export{defaultasTitleAndButtonListHeader}from'./classes/TitleAndButtonListHeader.js';export{defaultasToggleButton}from'./classes/ToggleButton.js';export{defaultasToggleButtonView}from'./classes/ToggleButtonView.js';export{defaultasToggleMenuServiceItem}from'./classes/ToggleMenuServiceItem.js';export{defaultasTooltip}from'./classes/Tooltip.js';export{defaultasTopicChannelDetails}from'./classes/TopicChannelDetails.js';export{defaultasTranscript}from'./classes/Transcript.js';export{defaultasTranscriptFooter}from'./classes/TranscriptFooter.js';export{defaultasTranscriptSearchBox}from'./classes/TranscriptSearchBox.js';export{defaultasTranscriptSearchPanel}from'./classes/TranscriptSearchPanel.js';export{defaultasTranscriptSectionHeader}from'./classes/TranscriptSectionHeader.js';export{defaultasTranscriptSegment}from'./classes/TranscriptSegment.js';export{defaultasTranscriptSegmentList}from'./classes/TranscriptSegmentList.js';export{defaultasTwoColumnBrowseResults}from'./classes/TwoColumnBrowseResults.js';export{defaultasTwoColumnSearchResults}from'./classes/TwoColumnSearchResults.js';export{defaultasTwoColumnWatchNextResults}from'./classes/TwoColumnWatchNextResults.js';export{defaultasUniversalWatchCard}from'./classes/UniversalWatchCard.js';export{defaultasUploadTimeFactoid}from'./classes/UploadTimeFactoid.js';export{defaultasUpsellDialog}from'./classes/UpsellDialog.js';export{defaultasVerticalList}from'./classes/VerticalList.js';export{defaultasVerticalWatchCardList}from'./classes/VerticalWatchCardList.js';export{defaultasVideo}from'./classes/Video.js';export{defaultasVideoAttributesSectionView}from'./classes/VideoAttributesSectionView.js';export{defaultasVideoAttributeView}from'./classes/VideoAttributeView.js';export{defaultasVideoCard}from'./classes/VideoCard.js';export{defaultasVideoDescriptionCourseSection}from'./classes/VideoDescriptionCourseSection.js';export{defaultasVideoDescriptionHeader}from'./classes/VideoDescriptionHeader.js';export{defaultasVideoDescriptionInfocardsSection}from'./classes/VideoDescriptionInfocardsSection.js';export{defaultasVideoDescriptionMusicSection}from'./classes/VideoDescriptionMusicSection.js';export{defaultasVideoDescriptionTranscriptSection}from'./classes/VideoDescriptionTranscriptSection.js';export{defaultasVideoInfoCardContent}from'./classes/VideoInfoCardContent.js';export{defaultasVideoOwner}from'./classes/VideoOwner.js';export{defaultasVideoPrimaryInfo}from'./classes/VideoPrimaryInfo.js';export{defaultasVideoSecondaryInfo}from'./classes/VideoSecondaryInfo.js';export{defaultasViewCountFactoid}from'./classes/ViewCountFactoid.js';export{defaultasWatchCardCompactVideo}from'./classes/WatchCardCompactVideo.js';export{defaultasWatchCardHeroVideo}from'./classes/WatchCardHeroVideo.js';export{defaultasWatchCardRichHeader}from'./classes/WatchCardRichHeader.js';export{defaultasWatchCardSectionSequence}from'./classes/WatchCardSectionSequence.js';export{defaultasWatchNextEndScreen}from'./classes/WatchNextEndScreen.js';export{defaultasWatchNextTabbedResults}from'./classes/WatchNextTabbedResults.js';export{defaultasYpcTrailer}from'./classes/YpcTrailer.js';export{defaultasAnchoredSection}from'./classes/ytkids/AnchoredSection.js';export{defaultasKidsBlocklistPicker}from'./classes/ytkids/KidsBlocklistPicker.js';export{defaultasKidsBlocklistPickerItem}from'./classes/ytkids/KidsBlocklistPickerItem.js';export{defaultasKidsCategoriesHeader}from'./classes/ytkids/KidsCategoriesHeader.js';export{defaultasKidsCategoryTab}from'./classes/ytkids/KidsCategoryTab.js';export{defaultasKidsHomeScreen}from'./classes/ytkids/KidsHomeScreen.js';
LuanRT/YouTube.js/blob/main/src/parser/parser.ts:
import*asYTNodesfrom'./nodes.js';import{InnertubeError,ParsingError,Platform}from'../utils/Utils.js';import{Memo,observe,SuperParsedResult}from'./helpers.js';import{camelToSnake,generateRuntimeClass,generateTypescriptClass}from'./generator.js';import{Log}from'../utils/index.js';import{Continuation,ItemSectionContinuation,SectionListContinuation,LiveChatContinuation,MusicPlaylistShelfContinuation,MusicShelfContinuation,GridContinuation,PlaylistPanelContinuation,NavigateAction,ShowMiniplayerCommand,ReloadContinuationItemsCommand,ContinuationCommand}from'./continuations.js';importAudioOnlyPlayabilityfrom'./classes/AudioOnlyPlayability.js';importCardCollectionfrom'./classes/CardCollection.js';importEndscreenfrom'./classes/Endscreen.js';importPlayerAnnotationsExpandedfrom'./classes/PlayerAnnotationsExpanded.js';importPlayerCaptionsTracklistfrom'./classes/PlayerCaptionsTracklist.js';importPlayerLiveStoryboardSpecfrom'./classes/PlayerLiveStoryboardSpec.js';importPlayerStoryboardSpecfrom'./classes/PlayerStoryboardSpec.js';importAlertfrom'./classes/Alert.js';importAlertWithButtonfrom'./classes/AlertWithButton.js';importEngagementPanelSectionListfrom'./classes/EngagementPanelSectionList.js';importMusicMultiSelectMenuItemfrom'./classes/menus/MusicMultiSelectMenuItem.js';importFormatfrom'./classes/misc/Format.js';importVideoDetailsfrom'./classes/misc/VideoDetails.js';importNavigationEndpointfrom'./classes/NavigationEndpoint.js';importCommentViewfrom'./classes/comments/CommentView.js';importMusicThumbnailfrom'./classes/MusicThumbnail.js';importtype{KeyInfo}from'./generator.js';importtype{ObservedArray,YTNodeConstructor,YTNode}from'./helpers.js';importtype{IParsedResponse,IRawResponse,RawData,RawNode}from'./types/index.js';constTAG='Parser';exporttypeParserError={classname: string,}&({error_type: 'typecheck',classdata: RawNode,expected: string|string[]}|{error_type: 'parse',classdata: RawNode,error: unknown}|{error_type: 'mutation_data_missing',classname: string}|{error_type: 'mutation_data_invalid',total: number,failed: number,titles: string[]}|{error_type: 'class_not_found',key_info: KeyInfo,}|{error_type: 'class_changed',key_info: KeyInfo,changed_keys: KeyInfo});exporttypeParserErrorHandler=(error: ParserError)=>void;constIGNORED_LIST=newSet(['AdSlot','DisplayAd','SearchPyv','MealbarPromo','PrimetimePromo','BackgroundPromo','PromotedSparklesWeb','RunAttestationCommand','CompactPromotedVideo','BrandVideoShelf','BrandVideoSingleton','StatementBanner','GuideSigninPromo','AdsEngagementPanelContent','MiniGameCardView']);constRUNTIME_NODES=newMap<string,YTNodeConstructor>(Object.entries(YTNodes));constDYNAMIC_NODES=newMap<string,YTNodeConstructor>();letMEMO: Memo|null=null;letERROR_HANDLER: ParserErrorHandler=({ classname, ...context}: ParserError)=>{switch(context.error_type){case'parse':
if(context.errorinstanceofError){Log.warn(TAG,newInnertubeError(`Something went wrong at ${classname}!\n`+`This is a bug, please report it at ${Platform.shim.info.bugs_url}`,{stack: context.error.stack,classdata: JSON.stringify(context.classdata,null,2)}));}break;case'typecheck':
Log.warn(TAG,newParsingError(`Type mismatch, got ${classname} expected ${Array.isArray(context.expected) ? context.expected.join(' | ') : context.expected}.`,context.classdata));break;case'mutation_data_missing':
Log.warn(TAG,newInnertubeError(`Mutation data required for processing ${classname}, but none found.\n`+`This is a bug, please report it at ${Platform.shim.info.bugs_url}`));break;case'mutation_data_invalid':
Log.warn(TAG,newInnertubeError(`Mutation data missing or invalid for ${context.failed} out of ${context.total} MusicMultiSelectMenuItems. `+`The titles of the failed items are: ${context.titles.join(', ')}.\n`+`This is a bug, please report it at ${Platform.shim.info.bugs_url}`));break;case'class_not_found':
Log.warn(TAG,newInnertubeError(`${classname} not found!\n`+`This is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!\n`+`Introspected and JIT generated this class in the meantime:\n${generateTypescriptClass(classname,context.key_info)}`));break;case'class_changed':
Log.warn(TAG,`${classname} changed!\n`+`The following keys where altered: ${context.changed_keys.map(([key])=>camelToSnake(key)).join(', ')}\n`+`The class has changed to:\n${generateTypescriptClass(classname,context.key_info)}`);break;default:
Log.warn(TAG,'Unreachable code reached at ParserErrorHandler');break;}};exportfunctionsetParserErrorHandler(handler: ParserErrorHandler){ERROR_HANDLER=handler;}function_clearMemo(){MEMO=null;}function_createMemo(){MEMO=newMemo();}function_addToMemo(classname: string,result: YTNode){if(!MEMO)return;constlist=MEMO.get(classname);if(!list)returnMEMO.set(classname,[result]);list.push(result);}function_getMemo(){if(!MEMO)thrownewError('Parser#getMemo() called before Parser#createMemo()');returnMEMO;}exportfunctionshouldIgnore(classname: string){returnIGNORED_LIST.has(classname);}exportfunctionsanitizeClassName(input: string){return(input.charAt(0).toUpperCase()+input.slice(1)).replace(/Renderer|Model/g,'').replace(/Radio/g,'Mix').trim();}exportfunctiongetParserByName(classname: string){constParserConstructor=RUNTIME_NODES.get(classname);if(!ParserConstructor){consterror=newError(`Module not found: ${classname}`);(errorasany).code='MODULE_NOT_FOUND';throwerror;}returnParserConstructor;}exportfunctionhasParser(classname: string){returnRUNTIME_NODES.has(classname);}exportfunctionaddRuntimeParser(classname: string,ParserConstructor: YTNodeConstructor){RUNTIME_NODES.set(classname,ParserConstructor);DYNAMIC_NODES.set(classname,ParserConstructor);}exportfunctiongetDynamicParsers(){returnObject.fromEntries(DYNAMIC_NODES);}/** * Parses given InnerTube response. * @param data - Raw data. */exportfunctionparseResponse<TextendsIParsedResponse=IParsedResponse>(data: IRawResponse): T{constparsed_data={}asT;_createMemo();constcontents=parse(data.contents);constcontents_memo=_getMemo();if(contents){parsed_data.contents=contents;parsed_data.contents_memo=contents_memo;}_clearMemo();_createMemo();conston_response_received_actions=data.onResponseReceivedActions ? parseRR(data.onResponseReceivedActions) : null;conston_response_received_actions_memo=_getMemo();if(on_response_received_actions){parsed_data.on_response_received_actions=on_response_received_actions;parsed_data.on_response_received_actions_memo=on_response_received_actions_memo;}_clearMemo();_createMemo();conston_response_received_endpoints=data.onResponseReceivedEndpoints ? parseRR(data.onResponseReceivedEndpoints) : null;conston_response_received_endpoints_memo=_getMemo();if(on_response_received_endpoints){parsed_data.on_response_received_endpoints=on_response_received_endpoints;parsed_data.on_response_received_endpoints_memo=on_response_received_endpoints_memo;}_clearMemo();_createMemo();conston_response_received_commands=data.onResponseReceivedCommands ? parseRR(data.onResponseReceivedCommands) : null;conston_response_received_commands_memo=_getMemo();if(on_response_received_commands){parsed_data.on_response_received_commands=on_response_received_commands;parsed_data.on_response_received_commands_memo=on_response_received_commands_memo;}_clearMemo();_createMemo();constcontinuation_contents=data.continuationContents ? parseLC(data.continuationContents) : null;constcontinuation_contents_memo=_getMemo();if(continuation_contents){parsed_data.continuation_contents=continuation_contents;parsed_data.continuation_contents_memo=continuation_contents_memo;}_clearMemo();_createMemo();constactions=data.actions ? parseActions(data.actions) : null;constactions_memo=_getMemo();if(actions){parsed_data.actions=actions;parsed_data.actions_memo=actions_memo;}_clearMemo();_createMemo();constlive_chat_item_context_menu_supported_renderers=data.liveChatItemContextMenuSupportedRenderers ? parseItem(data.liveChatItemContextMenuSupportedRenderers) : null;constlive_chat_item_context_menu_supported_renderers_memo=_getMemo();if(live_chat_item_context_menu_supported_renderers){parsed_data.live_chat_item_context_menu_supported_renderers=live_chat_item_context_menu_supported_renderers;parsed_data.live_chat_item_context_menu_supported_renderers_memo=live_chat_item_context_menu_supported_renderers_memo;}_clearMemo();_createMemo();constheader=data.header ? parse(data.header) : null;constheader_memo=_getMemo();if(header){parsed_data.header=header;parsed_data.header_memo=header_memo;}_clearMemo();_createMemo();constsidebar=data.sidebar ? parseItem(data.sidebar) : null;constsidebar_memo=_getMemo();if(sidebar){parsed_data.sidebar=sidebar;parsed_data.sidebar_memo=sidebar_memo;}_clearMemo();_createMemo();constitems=parse(data.items);if(items){parsed_data.items=items;parsed_data.items_memo=_getMemo();}_clearMemo();applyMutations(contents_memo,data.frameworkUpdates?.entityBatchUpdate?.mutations);if(on_response_received_endpoints_memo){applyCommentsMutations(on_response_received_endpoints_memo,data.frameworkUpdates?.entityBatchUpdate?.mutations);}constcontinuation=data.continuation ? parseC(data.continuation) : null;if(continuation){parsed_data.continuation=continuation;}constcontinuation_endpoint=data.continuationEndpoint ? parseLC(data.continuationEndpoint) : null;if(continuation_endpoint){parsed_data.continuation_endpoint=continuation_endpoint;}constmetadata=parse(data.metadata);if(metadata){parsed_data.metadata=metadata;}constmicroformat=parseItem(data.microformat);if(microformat){parsed_data.microformat=microformat;}constoverlay=parseItem(data.overlay);if(overlay){parsed_data.overlay=overlay;}constalerts=parseArray(data.alerts,[Alert,AlertWithButton]);if(alerts.length){parsed_data.alerts=alerts;}constrefinements=data.refinements;if(refinements){parsed_data.refinements=refinements;}constestimated_results=data.estimatedResults ? parseInt(data.estimatedResults) : null;if(estimated_results){parsed_data.estimated_results=estimated_results;}constplayer_overlays=parse(data.playerOverlays);if(player_overlays){parsed_data.player_overlays=player_overlays;}constbackground=parseItem(data.background,MusicThumbnail);if(background){parsed_data.background=background;}constplayback_tracking=data.playbackTracking ? {videostats_watchtime_url: data.playbackTracking.videostatsWatchtimeUrl.baseUrl,videostats_playback_url: data.playbackTracking.videostatsPlaybackUrl.baseUrl} : null;if(playback_tracking){parsed_data.playback_tracking=playback_tracking;}constplayability_status=data.playabilityStatus ? {status: data.playabilityStatus.status,reason: data.playabilityStatus.reason||'',embeddable: !!data.playabilityStatus.playableInEmbed||false,audio_only_playablility: parseItem(data.playabilityStatus.audioOnlyPlayability,AudioOnlyPlayability),error_screen: parseItem(data.playabilityStatus.errorScreen)} : null;if(playability_status){parsed_data.playability_status=playability_status;}if(data.streamingData){// Currently each response with streaming data only has two n param values// One for the adaptive formats and another for the combined formats// As they are the same for a response, we only need to decipher them once// For all futher deciphering calls on formats from that response, we can use the cached output, given the same input n paramconstthis_response_nsig_cache=newMap<string,string>();conststreaming_data={expires: newDate(Date.now()+parseInt(data.streamingData.expiresInSeconds)*1000),formats: parseFormats(data.streamingData.formats,this_response_nsig_cache),adaptive_formats: parseFormats(data.streamingData.adaptiveFormats,this_response_nsig_cache),dash_manifest_url: data.streamingData.dashManifestUrl||null,hls_manifest_url: data.streamingData.hlsManifestUrl||null};parsed_data.streaming_data=streaming_data;}if(data.playerConfig){constplayer_config={audio_config: {loudness_db: data.playerConfig.audioConfig?.loudnessDb,perceptual_loudness_db: data.playerConfig.audioConfig?.perceptualLoudnessDb,enable_per_format_loudness: data.playerConfig.audioConfig?.enablePerFormatLoudness},stream_selection_config: {max_bitrate: data.playerConfig.streamSelectionConfig?.maxBitrate||'0'},media_common_config: {dynamic_readahead_config: {max_read_ahead_media_time_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.maxReadAheadMediaTimeMs||0,min_read_ahead_media_time_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.minReadAheadMediaTimeMs||0,read_ahead_growth_rate_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.readAheadGrowthRateMs||0}}};parsed_data.player_config=player_config;}constcurrent_video_endpoint=data.currentVideoEndpoint ? newNavigationEndpoint(data.currentVideoEndpoint) : null;if(current_video_endpoint){parsed_data.current_video_endpoint=current_video_endpoint;}constendpoint=data.endpoint ? newNavigationEndpoint(data.endpoint) : null;if(endpoint){parsed_data.endpoint=endpoint;}constcaptions=parseItem(data.captions,PlayerCaptionsTracklist);if(captions){parsed_data.captions=captions;}constvideo_details=data.videoDetails ? newVideoDetails(data.videoDetails) : null;if(video_details){parsed_data.video_details=video_details;}constannotations=parseArray(data.annotations,PlayerAnnotationsExpanded);if(annotations.length){parsed_data.annotations=annotations;}conststoryboards=parseItem(data.storyboards,[PlayerStoryboardSpec,PlayerLiveStoryboardSpec]);if(storyboards){parsed_data.storyboards=storyboards;}constendscreen=parseItem(data.endscreen,Endscreen);if(endscreen){parsed_data.endscreen=endscreen;}constcards=parseItem(data.cards,CardCollection);if(cards){parsed_data.cards=cards;}constengagement_panels=parseArray(data.engagementPanels,EngagementPanelSectionList);if(engagement_panels.length){parsed_data.engagement_panels=engagement_panels;}if(data.playerResponse){constplayer_response=parseResponse(data.playerResponse);parsed_data.player_response=player_response;}if(data.watchNextResponse){constwatch_next_response=parseResponse(data.watchNextResponse);parsed_data.watch_next_response=watch_next_response;}if(data.cpnInfo){constcpn_info={cpn: data.cpnInfo.cpn,cpn_source: data.cpnInfo.cpnSource};parsed_data.cpn_info=cpn_info;}if(data.entries){parsed_data.entries=data.entries.map((entry)=>newNavigationEndpoint(entry));}returnparsed_data;}/** * Parses a single item. * @param data - The data to parse. * @param validTypes - YTNode types that are allowed to be parsed. */exportfunctionparseItem<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(data: RawNode|undefined,validTypes: K): InstanceType<K[number]>|null;exportfunctionparseItem<TextendsYTNode>(data: RawNode|undefined,validTypes: YTNodeConstructor<T>): T|null;exportfunctionparseItem(data?: RawNode): YTNode;exportfunctionparseItem(data?: RawNode,validTypes?: YTNodeConstructor|YTNodeConstructor[]){if(!data)returnnull;constkeys=Object.keys(data);if(!keys.length)returnnull;constclassname=sanitizeClassName(keys[0]);if(!shouldIgnore(classname)){try{consthas_target_class=hasParser(classname);constTargetClass=has_target_class ?
getParserByName(classname) :
generateRuntimeClass(classname,data[keys[0]],ERROR_HANDLER);if(validTypes){if(Array.isArray(validTypes)){if(!validTypes.some((type)=>type.type===TargetClass.type)){ERROR_HANDLER({classdata: data[keys[0]],
classname,error_type: 'typecheck',expected: validTypes.map((type)=>type.type)});returnnull;}}elseif(TargetClass.type!==validTypes.type){ERROR_HANDLER({classdata: data[keys[0]],
classname,error_type: 'typecheck',expected: validTypes.type});returnnull;}}constresult=newTargetClass(data[keys[0]]);_addToMemo(classname,result);returnresult;}catch(err){ERROR_HANDLER({
classname,classdata: data[keys[0]],error: err,error_type: 'parse'});returnnull;}}returnnull;}/** * Parses an array of items. * @param data - The data to parse. * @param validTypes - YTNode types that are allowed to be parsed. */exportfunctionparseArray<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(data: RawNode[]|undefined,validTypes: K): ObservedArray<InstanceType<K[number]>>;exportfunctionparseArray<TextendsYTNode=YTNode>(data: RawNode[]|undefined,validType: YTNodeConstructor<T>): ObservedArray<T>;exportfunctionparseArray(data: RawNode[]|undefined): ObservedArray<YTNode>;exportfunctionparseArray(data?: RawNode[],validTypes?: YTNodeConstructor|YTNodeConstructor[]){if(Array.isArray(data)){constresults: YTNode[]=[];for(constitemofdata){constresult=parseItem(item,validTypesasYTNodeConstructor);if(result){results.push(result);}}returnobserve(results);}elseif(!data){returnobserve([]asYTNode[]);}thrownewParsingError('Expected array but got a single item');}/** * Parses an item or an array of items. * @param data - The data to parse. * @param requireArray - Whether the data should be parsed as an array. * @param validTypes - YTNode types that are allowed to be parsed. */exportfunctionparse<TextendsYTNode,KextendsYTNodeConstructor<T>[]>(data: RawData,requireArray: true,validTypes?: K): ObservedArray<InstanceType<K[number]>>|null;exportfunctionparse<TextendsYTNode,KextendsYTNodeConstructor<T>>(data: RawData,requireArray: true,validTypes?: K): ObservedArray<InstanceType<K>>|null;exportfunctionparse<TextendsYTNode=YTNode>(data?: RawData,requireArray?: false|undefined,validTypes?: YTNodeConstructor<T>|YTNodeConstructor<T>[]): SuperParsedResult<T>;exportfunctionparse<TextendsYTNode=YTNode>(data?: RawData,requireArray?: boolean,validTypes?: YTNodeConstructor<T>|YTNodeConstructor<T>[]){if(!data)returnnull;if(Array.isArray(data)){constresults: T[]=[];for(constitemofdata){constresult=parseItem(item,validTypesasYTNodeConstructor<T>);if(result){results.push(result);}}constres=observe(results);returnrequireArray ? res : newSuperParsedResult(res);}elseif(requireArray){thrownewParsingError('Expected array but got a single item');}returnnewSuperParsedResult(parseItem(data,validTypesasYTNodeConstructor<T>));}exportfunctionparseC(data: RawNode){if(data.timedContinuationData)returnnewContinuation({continuation: data.timedContinuationData,type: 'timed'});returnnull;}exportfunctionparseLC(data: RawNode){if(data.itemSectionContinuation)returnnewItemSectionContinuation(data.itemSectionContinuation);if(data.sectionListContinuation)returnnewSectionListContinuation(data.sectionListContinuation);if(data.liveChatContinuation)returnnewLiveChatContinuation(data.liveChatContinuation);if(data.musicPlaylistShelfContinuation)returnnewMusicPlaylistShelfContinuation(data.musicPlaylistShelfContinuation);if(data.musicShelfContinuation)returnnewMusicShelfContinuation(data.musicShelfContinuation);if(data.gridContinuation)returnnewGridContinuation(data.gridContinuation);if(data.playlistPanelContinuation)returnnewPlaylistPanelContinuation(data.playlistPanelContinuation);if(data.continuationCommand)returnnewContinuationCommand(data.continuationCommand);returnnull;}exportfunctionparseRR(actions: RawNode[]){returnobserve(actions.map((action: any)=>{if(action.navigateAction)returnnewNavigateAction(action.navigateAction);if(action.showMiniplayerCommand)returnnewShowMiniplayerCommand(action.showMiniplayerCommand);if(action.reloadContinuationItemsCommand)returnnewReloadContinuationItemsCommand(action.reloadContinuationItemsCommand);if(action.appendContinuationItemsAction)returnnewYTNodes.AppendContinuationItemsAction(action.appendContinuationItemsAction);}).filter((item)=>item)as(ReloadContinuationItemsCommand|YTNodes.AppendContinuationItemsAction)[]);}exportfunctionparseActions(data: RawData){if(Array.isArray(data)){returnparse(data.map((action)=>{deleteaction.clickTrackingParams;returnaction;}));}returnnewSuperParsedResult(parseItem(data));}exportfunctionparseFormats(formats: RawNode[],this_response_nsig_cache: Map<string,string>): Format[]{returnformats?.map((format)=>newFormat(format,this_response_nsig_cache))||[];}exportfunctionapplyMutations(memo: Memo,mutations: RawNode[]){// Apply mutations to MusicMultiSelectMenuItemsconstmusic_multi_select_menu_items=memo.getType(MusicMultiSelectMenuItem);if(music_multi_select_menu_items.length>0&&!mutations){ERROR_HANDLER({error_type: 'mutation_data_missing',classname: 'MusicMultiSelectMenuItem'});}else{constmissing_or_invalid_mutations=[];for(constmenu_itemofmusic_multi_select_menu_items){constmutation=mutations.find((mutation)=>mutation.payload?.musicFormBooleanChoice?.id===menu_item.form_item_entity_key);constchoice=mutation?.payload.musicFormBooleanChoice;if(choice?.selected!==undefined&&choice?.opaqueToken){menu_item.selected=choice.selected;}else{missing_or_invalid_mutations.push(`'${menu_item.title}'`);}}if(missing_or_invalid_mutations.length>0){ERROR_HANDLER({error_type: 'mutation_data_invalid',classname: 'MusicMultiSelectMenuItem',total: music_multi_select_menu_items.length,failed: missing_or_invalid_mutations.length,titles: missing_or_invalid_mutations});}}}exportfunctionapplyCommentsMutations(memo: Memo,mutations: RawNode[]){constcomment_view_items=memo.getType(CommentView);if(comment_view_items.length>0){if(!mutations){ERROR_HANDLER({error_type: 'mutation_data_missing',classname: 'CommentView'});}for(constcomment_viewofcomment_view_items){constcomment_mutation=mutations.find((mutation)=>mutation.payload?.commentEntityPayload?.key===comment_view.keys.comment)?.payload?.commentEntityPayload;consttoolbar_state_mutation=mutations.find((mutation)=>mutation.payload?.engagementToolbarStateEntityPayload?.key===comment_view.keys.toolbar_state)?.payload?.engagementToolbarStateEntityPayload;constengagement_toolbar=mutations.find((mutation)=>mutation.entityKey===comment_view.keys.toolbar_surface)?.payload?.engagementToolbarSurfaceEntityPayload;comment_view.applyMutations(comment_mutation,toolbar_state_mutation,engagement_toolbar);}}}
import{Parser}from'../index.js';import{InnertubeError}from'../../utils/Utils.js';importAccountSectionListfrom'../classes/AccountSectionList.js';importtype{ApiResponse}from'../../core/index.js';importtype{IParsedResponse}from'../types/index.js';importtypeAccountItemSectionfrom'../classes/AccountItemSection.js';importtypeAccountChannelfrom'../classes/AccountChannel.js';exportdefaultclassAccountInfo{
#page: IParsedResponse;contents: AccountItemSection|null;footers: AccountChannel|null;constructor(response: ApiResponse){this.#page =Parser.parseResponse(response.data);if(!this.#page.contents)thrownewInnertubeError('Page contents not found');constaccount_section_list=this.#page.contents.array().as(AccountSectionList).first();if(!account_section_list)thrownewInnertubeError('Account section list not found');this.contents=account_section_list.contents;this.footers=account_section_list.footers;}getpage(): IParsedResponse{returnthis.#page;}}
importFeedfrom'../../core/mixins/Feed.js';importFilterableFeedfrom'../../core/mixins/FilterableFeed.js';import{ChannelError,InnertubeError}from'../../utils/Utils.js';importTabbedFeedfrom'../../core/mixins/TabbedFeed.js';importC4TabbedHeaderfrom'../classes/C4TabbedHeader.js';importCarouselHeaderfrom'../classes/CarouselHeader.js';importChannelAboutFullMetadatafrom'../classes/ChannelAboutFullMetadata.js';importAboutChannelfrom'../classes/AboutChannel.js';importChannelMetadatafrom'../classes/ChannelMetadata.js';importInteractiveTabbedHeaderfrom'../classes/InteractiveTabbedHeader.js';importMicroformatDatafrom'../classes/MicroformatData.js';importSubscribeButtonfrom'../classes/SubscribeButton.js';importExpandableTabfrom'../classes/ExpandableTab.js';importSectionListfrom'../classes/SectionList.js';importTabfrom'../classes/Tab.js';importPageHeaderfrom'../classes/PageHeader.js';importTwoColumnBrowseResultsfrom'../classes/TwoColumnBrowseResults.js';importChipCloudChipfrom'../classes/ChipCloudChip.js';importFeedFilterChipBarfrom'../classes/FeedFilterChipBar.js';importChannelSubMenufrom'../classes/ChannelSubMenu.js';importSortFilterSubMenufrom'../classes/SortFilterSubMenu.js';importContinuationItemfrom'../classes/ContinuationItem.js';importNavigationEndpointfrom'../classes/NavigationEndpoint.js';importtype{AppendContinuationItemsAction,ReloadContinuationItemsCommand}from'../index.js';importtype{ApiResponse,Actions}from'../../core/index.js';importtype{IBrowseResponse}from'../types/index.js';exportdefaultclassChannelextendsTabbedFeed<IBrowseResponse>{header?: C4TabbedHeader|CarouselHeader|InteractiveTabbedHeader|PageHeader;metadata;subscribe_button?: SubscribeButton;current_tab?: Tab|ExpandableTab;constructor(actions: Actions,data: ApiResponse|IBrowseResponse,already_parsed=false){super(actions,data,already_parsed);this.header=this.page.header?.item()?.as(C4TabbedHeader,CarouselHeader,InteractiveTabbedHeader,PageHeader);constmetadata=this.page.metadata?.item().as(ChannelMetadata);constmicroformat=this.page.microformat?.as(MicroformatData);if(this.page.alerts){constalert=this.page.alerts.first();if(alert?.alert_type==='ERROR'){thrownewChannelError(alert.text.toString());}}if(!metadata&&!this.page.contents)thrownewInnertubeError('Invalid channel',this);this.metadata={ ...metadata, ...(microformat||{})};this.subscribe_button=this.page.header_memo?.getType(SubscribeButton).first();this.current_tab=this.page.contents?.item().as(TwoColumnBrowseResults).tabs.array().filterType(Tab,ExpandableTab).get({selected: true});}/** * Applies given filter to the list. Use {@link filters} to get available filters. * @param filter - The filter to apply */asyncapplyFilter(filter: string|ChipCloudChip): Promise<FilteredChannelList>{lettarget_filter: ChipCloudChip|undefined;constfilter_chipbar=this.memo.getType(FeedFilterChipBar).first();if(typeoffilter==='string'){target_filter=filter_chipbar?.contents.get({text: filter});if(!target_filter)thrownewInnertubeError(`Filter ${filter} not found`,{available_filters: this.filters});}elseif(filterinstanceofChipCloudChip){target_filter=filter;}if(!target_filter)thrownewInnertubeError('Invalid filter',filter);constpage=awaittarget_filter.endpoint?.call<IBrowseResponse>(this.actions,{parse: true});if(!page)thrownewInnertubeError('No page returned',{filter: target_filter});returnnewFilteredChannelList(this.actions,page,true);}/** * Applies given sort filter to the list. Use {@link sort_filters} to get available filters. * @param sort - The sort filter to apply */asyncapplySort(sort: string): Promise<Channel>{constsort_filter_sub_menu=this.memo.getType(SortFilterSubMenu).first();if(!sort_filter_sub_menu)thrownewInnertubeError('No sort filter sub menu found');consttarget_sort=sort_filter_sub_menu?.sub_menu_items?.find((item)=>item.title===sort);if(!target_sort)thrownewInnertubeError(`Sort filter ${sort} not found`,{available_sort_filters: this.sort_filters});if(target_sort.selected)returnthis;constpage=awaittarget_sort.endpoint?.call<IBrowseResponse>(this.actions,{parse: true});returnnewChannel(this.actions,page,true);}/** * Applies given content type filter to the list. Use {@link content_type_filters} to get available filters. * @param content_type_filter - The content type filter to apply */asyncapplyContentTypeFilter(content_type_filter: string): Promise<Channel>{constsub_menu=this.current_tab?.content?.as(SectionList).sub_menu?.as(ChannelSubMenu);if(!sub_menu)thrownewInnertubeError('Sub menu not found');constitem=sub_menu.content_type_sub_menu_items.find((item)=>item.title===content_type_filter);if(!item)thrownewInnertubeError(`Sub menu item ${content_type_filter} not found`,{available_filters: this.content_type_filters});if(item.selected)returnthis;constpage=awaititem.endpoint?.call<IBrowseResponse>(this.actions,{parse: true});returnnewChannel(this.actions,page,true);}getfilters(): string[]{returnthis.memo.getType(FeedFilterChipBar)?.[0]?.contents.filterType(ChipCloudChip).map((chip)=>chip.text)||[];}getsort_filters(): string[]{constsort_filter_sub_menu=this.memo.getType(SortFilterSubMenu).first();returnsort_filter_sub_menu?.sub_menu_items?.map((item)=>item.title)||[];}getcontent_type_filters(): string[]{constsub_menu=this.current_tab?.content?.as(SectionList).sub_menu?.as(ChannelSubMenu);returnsub_menu?.content_type_sub_menu_items.map((item)=>item.title)||[];}asyncgetHome(): Promise<Channel>{consttab=awaitthis.getTabByURL('featured');returnnewChannel(this.actions,tab.page,true);}asyncgetVideos(): Promise<Channel>{consttab=awaitthis.getTabByURL('videos');returnnewChannel(this.actions,tab.page,true);}asyncgetShorts(): Promise<Channel>{consttab=awaitthis.getTabByURL('shorts');returnnewChannel(this.actions,tab.page,true);}asyncgetLiveStreams(): Promise<Channel>{consttab=awaitthis.getTabByURL('streams');returnnewChannel(this.actions,tab.page,true);}asyncgetReleases(): Promise<Channel>{consttab=awaitthis.getTabByURL('releases');returnnewChannel(this.actions,tab.page,true);}asyncgetPodcasts(): Promise<Channel>{consttab=awaitthis.getTabByURL('podcasts');returnnewChannel(this.actions,tab.page,true);}asyncgetPlaylists(): Promise<Channel>{consttab=awaitthis.getTabByURL('playlists');returnnewChannel(this.actions,tab.page,true);}asyncgetCommunity(): Promise<Channel>{consttab=awaitthis.getTabByURL('community');returnnewChannel(this.actions,tab.page,true);}/** * Retrieves the about page. * Note that this does not return a new {@link Channel} object. */asyncgetAbout(): Promise<ChannelAboutFullMetadata|AboutChannel>{if(this.hasTabWithURL('about')){consttab=awaitthis.getTabByURL('about');returntab.memo.getType(ChannelAboutFullMetadata)[0];}consttagline=this.header?.is(C4TabbedHeader)&&this.header.tagline;if(tagline||this.header?.is(PageHeader)&&this.header.content?.description){if(tagline&&tagline.more_endpointinstanceofNavigationEndpoint){constresponse=awaittagline.more_endpoint.call(this.actions);consttab=newTabbedFeed<IBrowseResponse>(this.actions,response,false);returntab.memo.getType(ChannelAboutFullMetadata)[0];}constendpoint=this.page.header_memo?.getType(ContinuationItem)[0]?.endpoint;if(!endpoint){thrownewInnertubeError('Failed to extract continuation to get channel about');}constresponse=awaitendpoint.call<IBrowseResponse>(this.actions,{parse: true});if(!response.on_response_received_endpoints_memo){thrownewInnertubeError('Unexpected response while fetching channel about',{ response });}returnresponse.on_response_received_endpoints_memo.getType(AboutChannel)[0];}thrownewInnertubeError('About not found');}/** * Searches within the channel. */asyncsearch(query: string): Promise<Channel>{consttab=this.memo.getType(ExpandableTab)?.[0];if(!tab)thrownewInnertubeError('Search tab not found',this);constpage=awaittab.endpoint?.call<IBrowseResponse>(this.actions,{ query,parse: true});returnnewChannel(this.actions,page,true);}gethas_home(): boolean{returnthis.hasTabWithURL('featured');}gethas_videos(): boolean{returnthis.hasTabWithURL('videos');}gethas_shorts(): boolean{returnthis.hasTabWithURL('shorts');}gethas_live_streams(): boolean{returnthis.hasTabWithURL('streams');}gethas_releases(): boolean{returnthis.hasTabWithURL('releases');}gethas_podcasts(): boolean{returnthis.hasTabWithURL('podcasts');}gethas_playlists(): boolean{returnthis.hasTabWithURL('playlists');}gethas_community(): boolean{returnthis.hasTabWithURL('community');}gethas_about(): boolean{// Game topic channels still have an about tab, user channels have switched to the popupreturnthis.hasTabWithURL('about')||!!(this.header?.is(C4TabbedHeader)&&this.header.tagline?.more_endpoint)||!!(this.header?.is(PageHeader)&&this.header.content?.description?.more_endpoint);}gethas_search(): boolean{returnthis.memo.getType(ExpandableTab)?.length>0;}/** * Retrives list continuation. */asyncgetContinuation(): Promise<ChannelListContinuation>{constpage=awaitsuper.getContinuationData();if(!page)thrownewInnertubeError('Could not get continuation data');returnnewChannelListContinuation(this.actions,page,true);}}exportclassChannelListContinuationextendsFeed<IBrowseResponse>{contents?: ReloadContinuationItemsCommand|AppendContinuationItemsAction;constructor(actions: Actions,data: ApiResponse|IBrowseResponse,already_parsed=false){super(actions,data,already_parsed);this.contents=this.page.on_response_received_actions?.first()||this.page.on_response_received_endpoints?.first();}/** * Retrieves list continuation. */asyncgetContinuation(): Promise<ChannelListContinuation>{constpage=awaitsuper.getContinuationData();if(!page)thrownewInnertubeError('Could not get continuation data');returnnewChannelListContinuation(this.actions,page,true);}}exportclassFilteredChannelListextendsFilterableFeed<IBrowseResponse>{applied_filter?: ChipCloudChip;contents?: ReloadContinuationItemsCommand|AppendContinuationItemsAction;constructor(actions: Actions,data: ApiResponse|IBrowseResponse,already_parsed=false){super(actions,data,already_parsed);this.applied_filter=this.memo.getType(ChipCloudChip).get({is_selected: true});// Removes the filter chipbar from the actions listif(this.page.on_response_received_actions&&this.page.on_response_received_actions.length>1){this.page.on_response_received_actions.shift();}this.contents=this.page.on_response_received_actions?.first();}/** * Applies given filter to the list. * @param filter - The filter to apply */asyncapplyFilter(filter: string|ChipCloudChip): Promise<FilteredChannelList>{constfeed=awaitsuper.getFilteredFeed(filter);returnnewFilteredChannelList(this.actions,feed.page,true);}/** * Retrieves list continuation. */asyncgetContinuation(): Promise<FilteredChannelList>{constpage=awaitsuper.getContinuationData();if(!page?.on_response_received_actions_memo)thrownewInnertubeError('Unexpected continuation data',page);// Keep the filterspage.on_response_received_actions_memo.set('FeedFilterChipBar',this.memo.getType(FeedFilterChipBar));page.on_response_received_actions_memo.set('ChipCloudChip',this.memo.getType(ChipCloudChip));returnnewFilteredChannelList(this.actions,page,true);}}
import{Parser}from'../index.js';import{InnertubeError}from'../../utils/Utils.js';import{observe}from'../helpers.js';importCommentsHeaderfrom'../classes/comments/CommentsHeader.js';importCommentSimpleboxfrom'../classes/comments/CommentSimplebox.js';importCommentThreadfrom'../classes/comments/CommentThread.js';importContinuationItemfrom'../classes/ContinuationItem.js';importtype{Actions,ApiResponse}from'../../core/index.js';importtype{ObservedArray}from'../helpers.js';importtype{INextResponse}from'../types/index.js';exportdefaultclassComments{
#page: INextResponse;
#actions: Actions;
#continuation?: ContinuationItem;header?: CommentsHeader;contents: ObservedArray<CommentThread>;constructor(actions: Actions,data: any,already_parsed=false){this.#page =already_parsed ? data : Parser.parseResponse<INextResponse>(data);this.#actions =actions;constcontents=this.#page.on_response_received_endpoints;if(!contents)thrownewInnertubeError('Comments page did not have any content.');constheader_node=contents.at(0);constbody_node=contents.at(1);this.header=header_node?.contents?.firstOfType(CommentsHeader);constthreads=body_node?.contents?.filterType(CommentThread)||[];this.contents=observe(threads.map((thread)=>{thread.comment?.setActions(this.#actions);thread.setActions(this.#actions);returnthread;}));this.#continuation =body_node?.contents?.firstOfType(ContinuationItem);}/** * Applies given sort option to the comments. * @param sort - Sort type. */asyncapplySort(sort: 'TOP_COMMENTS'|'NEWEST_FIRST'): Promise<Comments>{if(!this.header)thrownewInnertubeError('Page header is missing. Cannot apply sort option.');letbutton;if(sort==='TOP_COMMENTS'){button=this.header.sort_menu?.sub_menu_items?.at(0);}elseif(sort==='NEWEST_FIRST'){button=this.header.sort_menu?.sub_menu_items?.at(1);}if(!button)thrownewInnertubeError('Could not find target button.');if(button.selected)returnthis;constresponse=awaitbutton.endpoint.call(this.#actions,{parse: true});returnnewComments(this.#actions,response,true);}/** * Creates a top-level comment. * @param text - Comment text. */asynccreateComment(text: string): Promise<ApiResponse>{if(!this.header)thrownewInnertubeError('Page header is missing. Cannot create comment.');constbutton=this.header.create_renderer?.as(CommentSimplebox).submit_button;if(!button)thrownewInnertubeError('Could not find target button. You are probably not logged in.');if(!button.endpoint)thrownewInnertubeError('Button does not have an endpoint.');constresponse=awaitbutton.endpoint.call(this.#actions,{commentText: text});returnresponse;}/** * Retrieves next batch of comments. */asyncgetContinuation(): Promise<Comments>{if(!this.#continuation)thrownewInnertubeError('Continuation not found');constdata=awaitthis.#continuation.endpoint.call(this.#actions,{parse: true});// Copy the previous page so we can keep the header.constpage=Object.assign({},this.#page);if(!page.on_response_received_endpoints||!data.on_response_received_endpoints)thrownewInnertubeError('Invalid reponse format, missing on_response_received_endpoints.');// Remove previous items and append the continuation.page.on_response_received_endpoints.pop();page.on_response_received_endpoints.push(data.on_response_received_endpoints[0]);returnnewComments(this.#actions,page,true);}gethas_continuation(): boolean{return!!this.#continuation;}getpage(): INextResponse{returnthis.#page;}}
import{InnertubeError}from'../../utils/Utils.js';importFilterableFeedfrom'../../core/mixins/FilterableFeed.js';importHashtagHeaderfrom'../classes/HashtagHeader.js';importRichGridfrom'../classes/RichGrid.js';importPageHeaderfrom'../classes/PageHeader.js';importTabfrom'../classes/Tab.js';importtype{Actions,ApiResponse}from'../../core/index.js';importtype{IBrowseResponse}from'../index.js';importtypeChipCloudChipfrom'../classes/ChipCloudChip.js';exportdefaultclassHashtagFeedextendsFilterableFeed<IBrowseResponse>{header?: HashtagHeader|PageHeader;contents: RichGrid;constructor(actions: Actions,response: IBrowseResponse|ApiResponse){super(actions,response);if(!this.page.contents_memo)thrownewInnertubeError('Unexpected response',this.page);consttab=this.page.contents_memo.getType(Tab).first();if(!tab.content)thrownewInnertubeError('Content tab has no content',tab);if(this.page.header){this.header=this.page.header.item().as(HashtagHeader,PageHeader);}this.contents=tab.content.as(RichGrid);}/** * Applies given filter and returns a new {@link HashtagFeed} object. Use {@link HashtagFeed.filters} to get available filters. * @param filter - Filter to apply. */asyncapplyFilter(filter: string|ChipCloudChip): Promise<HashtagFeed>{constresponse=awaitsuper.getFilteredFeed(filter);returnnewHashtagFeed(this.actions,response.page);}}
importFilterableFeedfrom'../../core/mixins/FilterableFeed.js';importFeedTabbedHeaderfrom'../classes/FeedTabbedHeader.js';importRichGridfrom'../classes/RichGrid.js';importtype{IBrowseResponse}from'../types/index.js';importtype{AppendContinuationItemsAction,ReloadContinuationItemsCommand}from'../index.js';importtype{ApiResponse,Actions}from'../../core/index.js';importtypeChipCloudChipfrom'../classes/ChipCloudChip.js';exportdefaultclassHomeFeedextendsFilterableFeed<IBrowseResponse>{contents?: RichGrid|AppendContinuationItemsAction|ReloadContinuationItemsCommand;header?: FeedTabbedHeader;constructor(actions: Actions,data: ApiResponse|IBrowseResponse,already_parsed=false){super(actions,data,already_parsed);this.header=this.memo.getType(FeedTabbedHeader).first();this.contents=this.memo.getType(RichGrid).first()||this.page.on_response_received_actions?.first();}/** * Applies given filter to the feed. Use {@link filters} to get available filters. * @param filter - Filter to apply. */asyncapplyFilter(filter: string|ChipCloudChip): Promise<HomeFeed>{constfeed=awaitsuper.getFilteredFeed(filter);returnnewHomeFeed(this.actions,feed.page,true);}/** * Retrieves next batch of contents. */asyncgetContinuation(): Promise<HomeFeed>{constfeed=awaitsuper.getContinuation();// Keep the page headerfeed.page.header=this.page.header;if(this.header)feed.page.header_memo?.set(this.header.type,[this.header]);returnnewHomeFeed(this.actions,feed.page,true);}}
importMenufrom'../classes/menus/Menu.js';importButtonfrom'../classes/Button.js';importMenuServiceItemfrom'../classes/menus/MenuServiceItem.js';importtype{Actions}from'../../core/index.js';import{InnertubeError}from'../../utils/Utils.js';importtype{ObservedArray,YTNode}from'../helpers.js';importtype{IParsedResponse}from'../types/index.js';importtypeNavigationEndpointfrom'../classes/NavigationEndpoint.js';exportdefaultclassItemMenu{
#page: IParsedResponse;
#actions: Actions;
#items: ObservedArray<YTNode>;constructor(data: IParsedResponse,actions: Actions){this.#page =data;this.#actions =actions;constmenu=data?.live_chat_item_context_menu_supported_renderers;if(!menu||!menu.is(Menu))thrownewInnertubeError('Response did not have a "live_chat_item_context_menu_supported_renderers" property. The call may have failed.');this.#items =menu.as(Menu).items;}asyncselectItem(icon_type: string): Promise<IParsedResponse>asyncselectItem(button: Button): Promise<IParsedResponse>asyncselectItem(item: string|Button): Promise<IParsedResponse>{letendpoint: NavigationEndpoint|undefined;if(iteminstanceofButton){if(!item.endpoint)thrownewInnertubeError('Item does not have an endpoint.');endpoint=item.endpoint;}else{constbutton=this.#items.find((button)=>{if(!button.is(MenuServiceItem)){returnfalse;}constmenuServiceItem=button.as(MenuServiceItem);returnmenuServiceItem.icon_type===item;});if(!button||!button.is(MenuServiceItem))thrownewInnertubeError(`Button "${item}" not found.`);endpoint=button.endpoint;}if(!endpoint)thrownewInnertubeError('Target button does not have an endpoint.');constresponse=awaitendpoint.call(this.#actions,{parse: true});returnresponse;}items(): ObservedArray<YTNode>{returnthis.#items;}page(): IParsedResponse{returnthis.#page;}}
import*asProtofrom'../../proto/index.js';import{EventEmitter}from'../../utils/index.js';import{InnertubeError,Platform}from'../../utils/Utils.js';import{Parser,LiveChatContinuation}from'../index.js';importSmoothedQueuefrom'./SmoothedQueue.js';importAddChatItemActionfrom'../classes/livechat/AddChatItemAction.js';importUpdateDateTextActionfrom'../classes/livechat/UpdateDateTextAction.js';importUpdateDescriptionActionfrom'../classes/livechat/UpdateDescriptionAction.js';importUpdateTitleActionfrom'../classes/livechat/UpdateTitleAction.js';importUpdateToggleButtonTextActionfrom'../classes/livechat/UpdateToggleButtonTextAction.js';importUpdateViewershipActionfrom'../classes/livechat/UpdateViewershipAction.js';importNavigationEndpointfrom'../classes/NavigationEndpoint.js';importItemMenufrom'./ItemMenu.js';importtype{ObservedArray,YTNode}from'../helpers.js';importtypeVideoInfofrom'./VideoInfo.js';importtypeAddBannerToLiveChatCommandfrom'../classes/livechat/AddBannerToLiveChatCommand.js';importtypeRemoveBannerForLiveChatCommandfrom'../classes/livechat/RemoveBannerForLiveChatCommand.js';importtypeShowLiveChatTooltipCommandfrom'../classes/livechat/ShowLiveChatTooltipCommand.js';importtypeLiveChatAutoModMessagefrom'../classes/livechat/items/LiveChatAutoModMessage.js';importtypeLiveChatMembershipItemfrom'../classes/livechat/items/LiveChatMembershipItem.js';importtypeLiveChatPaidMessagefrom'../classes/livechat/items/LiveChatPaidMessage.js';importtypeLiveChatPaidStickerfrom'../classes/livechat/items/LiveChatPaidSticker.js';importtypeLiveChatTextMessagefrom'../classes/livechat/items/LiveChatTextMessage.js';importtypeLiveChatViewerEngagementMessagefrom'../classes/livechat/items/LiveChatViewerEngagementMessage.js';importtypeAddLiveChatTickerItemActionfrom'../classes/livechat/AddLiveChatTickerItemAction.js';importtypeMarkChatItemAsDeletedActionfrom'../classes/livechat/MarkChatItemAsDeletedAction.js';importtypeMarkChatItemsByAuthorAsDeletedActionfrom'../classes/livechat/MarkChatItemsByAuthorAsDeletedAction.js';importtypeReplaceChatItemActionfrom'../classes/livechat/ReplaceChatItemAction.js';importtypeReplayChatItemActionfrom'../classes/livechat/ReplayChatItemAction.js';importtypeShowLiveChatActionPanelActionfrom'../classes/livechat/ShowLiveChatActionPanelAction.js';importtypeButtonfrom'../classes/Button.js';importtype{Actions}from'../../core/index.js';importtype{IParsedResponse,IUpdatedMetadataResponse}from'../types/index.js';exporttypeChatAction=AddChatItemAction|AddBannerToLiveChatCommand|AddLiveChatTickerItemAction|MarkChatItemAsDeletedAction|MarkChatItemsByAuthorAsDeletedAction|RemoveBannerForLiveChatCommand|ReplaceChatItemAction|ReplayChatItemAction|ShowLiveChatActionPanelAction|ShowLiveChatTooltipCommand;exporttypeChatItemWithMenu=LiveChatAutoModMessage|LiveChatMembershipItem|LiveChatPaidMessage|LiveChatPaidSticker|LiveChatTextMessage|LiveChatViewerEngagementMessage;exportinterfaceLiveMetadata{title?: UpdateTitleAction;description?: UpdateDescriptionAction;views?: UpdateViewershipAction;likes?: UpdateToggleButtonTextAction;date?: UpdateDateTextAction;}exportdefaultclassLiveChatextendsEventEmitter{smoothed_queue: SmoothedQueue;
#actions: Actions;
#video_id: string;
#channel_id: string;
#continuation?: string;
#mcontinuation?: string;
#retry_count =0;initial_info?: LiveChatContinuation;metadata?: LiveMetadata;running=false;is_replay=false;constructor(video_info: VideoInfo){super();this.#video_id =video_info.basic_info.idasstring;this.#channel_id =video_info.basic_info.channel_idasstring;this.#actions =video_info.actions;this.#continuation =video_info.livechat?.continuation;this.is_replay=video_info.livechat?.is_replay||false;this.smoothed_queue=newSmoothedQueue();this.smoothed_queue.callback=async(actions: YTNode[])=>{if(!actions.length){// Wait 2 seconds before requesting an incremental continuation if the action group is empty.awaitthis.#wait(2000);}elseif(actions.length<10){// If there are less than 10 actions, wait until all of them are emitted.awaitthis.#emitSmoothedActions(actions);}elseif(this.is_replay){/** * NOTE: Live chat replays require data from the video player for actions to be emitted timely * and as we don't have that, this ends up being quite innacurate. */this.#emitSmoothedActions(actions);awaitthis.#wait(2000);}else{// There are more than 10 actions, emit them asynchonously so we can request the next incremental continuation.this.#emitSmoothedActions(actions);}if(this.running){this.#pollLivechat();}};}on(type: 'start',listener: (initial_data: LiveChatContinuation)=>void): void;on(type: 'chat-update',listener: (action: ChatAction)=>void): void;on(type: 'metadata-update',listener: (metadata: LiveMetadata)=>void): void;on(type: 'error',listener: (err: Error)=>void): void;on(type: 'end',listener: ()=>void): void;on(type: string,listener: (...args: any[])=>void): void{super.on(type,listener);}once(type: 'start',listener: (initial_data: LiveChatContinuation)=>void): void;once(type: 'chat-update',listener: (action: ChatAction)=>void): void;once(type: 'metadata-update',listener: (metadata: LiveMetadata)=>void): void;once(type: 'error',listener: (err: Error)=>void): void;once(type: 'end',listener: ()=>void): void;once(type: string,listener: (...args: any[])=>void): void{super.once(type,listener);}start(){if(!this.running){this.running=true;this.#pollLivechat();this.#pollMetadata();}}stop(){this.smoothed_queue.clear();this.running=false;}
#pollLivechat(){(async()=>{try{constresponse=awaitthis.#actions.execute(this.is_replay ? 'live_chat/get_live_chat_replay' : 'live_chat/get_live_chat',{continuation: this.#continuation,parse: true});constcontents=response.continuation_contents;if(!contents){this.emit('error',newInnertubeError('Unexpected live chat incremental continuation response',response));this.emit('end');this.stop();}if(!(contentsinstanceofLiveChatContinuation)){this.stop();this.emit('end');return;}this.#continuation =contents.continuation.token;// Header only exists in the first requestif(contents.header){this.initial_info=contents;this.emit('start',contents);if(this.running)this.#pollLivechat();}else{this.smoothed_queue.enqueueActionGroup(contents.actions);}this.#retry_count =0;}catch(err){this.emit('error',err);if(this.#retry_count++<10){awaitthis.#wait(2000);this.#pollLivechat();}else{this.emit('error',newInnertubeError('Reached retry limit for incremental continuation requests',err));this.emit('end');this.stop();}}})();}/** * Ensures actions are emitted at the right speed. * This and {@link SmoothedQueue} were based off of YouTube's own implementation. */async #emitSmoothedActions(action_queue: YTNode[]){constbase=1E4;letdelay=action_queue.length<base/80 ? 1 : Math.ceil(action_queue.length/(base/80));constemit_delay_ms=delay==1 ? (delay=base/action_queue.length,delay*=Math.random()+0.5,delay=Math.min(1E3,delay),delay=Math.max(80,delay)) : delay=80;for(constactionofaction_queue){awaitthis.#wait(emit_delay_ms);this.emit('chat-update',action);}}
#pollMetadata(){(async()=>{try{constpayload: {videoId?: string;continuation?: string;}={videoId: this.#video_id };if(this.#mcontinuation){payload.continuation=this.#mcontinuation;}constresponse=awaitthis.#actions.execute('/updated_metadata',payload);constdata=Parser.parseResponse<IUpdatedMetadataResponse>(response.data);this.#mcontinuation =data.continuation?.token;this.metadata={title: data.actions?.array().firstOfType(UpdateTitleAction)||this.metadata?.title,description: data.actions?.array().firstOfType(UpdateDescriptionAction)||this.metadata?.description,views: data.actions?.array().firstOfType(UpdateViewershipAction)||this.metadata?.views,likes: data.actions?.array().firstOfType(UpdateToggleButtonTextAction)||this.metadata?.likes,date: data.actions?.array().firstOfType(UpdateDateTextAction)||this.metadata?.date};this.emit('metadata-update',this.metadata);awaitthis.#wait(5000);if(this.running)this.#pollMetadata();}catch(err){awaitthis.#wait(2000);if(this.running)this.#pollMetadata();}})();}/** * Sends a message. * @param text - Text to send. */asyncsendMessage(text: string): Promise<ObservedArray<AddChatItemAction>>{constresponse=awaitthis.#actions.execute('/live_chat/send_message',{params: Proto.encodeMessageParams(this.#channel_id,this.#video_id),richMessage: {textSegments: [{ text }]},clientMessageId: Platform.shim.uuidv4(),client: 'ANDROID',parse: true});if(!response.actions)thrownewInnertubeError('Unexpected response from send_message',response);returnresponse.actions.array().as(AddChatItemAction);}/** * Applies given filter to the live chat. * @param filter - Filter to apply. */applyFilter(filter: 'TOP_CHAT'|'LIVE_CHAT'): void{if(!this.initial_info)thrownewInnertubeError('Cannot apply filter before initial info is retrieved.');constmenu_items=this.initial_info?.header?.view_selector?.sub_menu_items;if(filter==='TOP_CHAT'){if(menu_items?.at(0)?.selected)return;this.#continuation =menu_items?.at(0)?.continuation;}else{if(menu_items?.at(1)?.selected)return;this.#continuation =menu_items?.at(1)?.continuation;}}/** * Retrieves given chat item's menu. */asyncgetItemMenu(item: ChatItemWithMenu): Promise<ItemMenu>{if(!item.hasKey('menu_endpoint')||!item.key('menu_endpoint').isInstanceof(NavigationEndpoint))thrownewInnertubeError('This item does not have a menu.',item);constresponse=awaititem.key('menu_endpoint').instanceof(NavigationEndpoint).call(this.#actions,{parse: true});if(!response)thrownewInnertubeError('Could not retrieve item menu.',item);returnnewItemMenu(response,this.#actions);}/** * Equivalent to "clicking" a button. */asyncselectButton(button: Button): Promise<IParsedResponse>{constresponse=awaitbutton.endpoint.call(this.#actions,{parse: true});returnresponse;}async #wait(ms: number){returnnewPromise<void>((resolve)=>setTimeout(()=>resolve(),ms));}}
import{InnertubeError}from'../../utils/Utils.js';importFeedfrom'../../core/mixins/Feed.js';importMessagefrom'../classes/Message.js';importPlaylistCustomThumbnailfrom'../classes/PlaylistCustomThumbnail.js';importPlaylistHeaderfrom'../classes/PlaylistHeader.js';importPlaylistMetadatafrom'../classes/PlaylistMetadata.js';importPlaylistSidebarPrimaryInfofrom'../classes/PlaylistSidebarPrimaryInfo.js';importPlaylistSidebarSecondaryInfofrom'../classes/PlaylistSidebarSecondaryInfo.js';importPlaylistVideoThumbnailfrom'../classes/PlaylistVideoThumbnail.js';importReelItemfrom'../classes/ReelItem.js';importVideoOwnerfrom'../classes/VideoOwner.js';importAlertfrom'../classes/Alert.js';importContinuationItemfrom'../classes/ContinuationItem.js';importPlaylistVideofrom'../classes/PlaylistVideo.js';importSectionListfrom'../classes/SectionList.js';import{observe,typeObservedArray}from'../helpers.js';importtype{ApiResponse,Actions}from'../../core/index.js';importtype{IBrowseResponse}from'../types/ParsedResponse.js';importtypeThumbnailfrom'../classes/misc/Thumbnail.js';importtypeNavigationEndpointfrom'../classes/NavigationEndpoint.js';exportdefaultclassPlaylistextendsFeed<IBrowseResponse>{info;menu;endpoint?: NavigationEndpoint;messages: ObservedArray<Message>;constructor(actions: Actions,data: ApiResponse|IBrowseResponse,already_parsed=false){super(actions,data,already_parsed);constheader=this.memo.getType(PlaylistHeader).first();constprimary_info=this.memo.getType(PlaylistSidebarPrimaryInfo).first();constsecondary_info=this.memo.getType(PlaylistSidebarSecondaryInfo).first();constalert=this.page.alerts?.firstOfType(Alert);if(alert&&alert.alert_type==='ERROR')thrownewInnertubeError(alert.text.toString(),alert);if(!primary_info&&!secondary_info&&Object.keys(this.page).length===0)thrownewInnertubeError('Got empty continuation response. This is likely the end of the playlist.');this.info={
...this.page.metadata?.item().as(PlaylistMetadata),
...{subtitle: header ? header.subtitle : null,author: secondary_info?.owner?.as(VideoOwner).author??header?.author,thumbnails: primary_info?.thumbnail_renderer?.as(PlaylistVideoThumbnail,PlaylistCustomThumbnail).thumbnailasThumbnail[],total_items: this.#getStat(0,primary_info),views: this.#getStat(1,primary_info),last_updated: this.#getStat(2,primary_info),can_share: header?.can_share,can_delete: header?.can_delete,is_editable: header?.is_editable,privacy: header?.privacy}};this.menu=primary_info?.menu;this.endpoint=primary_info?.endpoint;this.messages=this.memo.getType(Message);}
#getStat(index: number,primary_info?: PlaylistSidebarPrimaryInfo): string{if(!primary_info||!primary_info.stats)return'N/A';returnprimary_info.stats[index]?.toString()||'N/A';}getitems(): ObservedArray<PlaylistVideo|ReelItem>{returnobserve(this.videos.as(PlaylistVideo,ReelItem).filter((video)=>(videoasPlaylistVideo).style!=='PLAYLIST_VIDEO_RENDERER_STYLE_RECOMMENDED_VIDEO'));}gethas_continuation(){constsection_list=this.memo.getType(SectionList).first();if(!section_list)returnsuper.has_continuation;return!!this.memo.getType(ContinuationItem).find((node)=>!section_list.contents.includes(node));}asyncgetContinuationData(): Promise<IBrowseResponse|undefined>{constsection_list=this.memo.getType(SectionList).first();/** * No section list means there can't be additional continuation nodes here, * so no need to check. */if(!section_list)returnawaitsuper.getContinuationData();constplaylist_contents_continuation=this.memo.getType(ContinuationItem).find((node)=>!section_list.contents.includes(node));if(!playlist_contents_continuation)thrownewInnertubeError('There are no continuations.');constresponse=awaitplaylist_contents_continuation.endpoint.call<IBrowseResponse>(this.actions,{parse: true});returnresponse;}asyncgetContinuation(): Promise<Playlist>{constpage=awaitthis.getContinuationData();if(!page)thrownewInnertubeError('Could not get continuation data');returnnewPlaylist(this.actions,page,true);}}
importFeedfrom'../../core/mixins/Feed.js';import{InnertubeError}from'../../utils/Utils.js';importHorizontalCardListfrom'../classes/HorizontalCardList.js';importItemSectionfrom'../classes/ItemSection.js';importSearchHeaderfrom'../classes/SearchHeader.js';importSearchRefinementCardfrom'../classes/SearchRefinementCard.js';importSearchSubMenufrom'../classes/SearchSubMenu.js';importSectionListfrom'../classes/SectionList.js';importUniversalWatchCardfrom'../classes/UniversalWatchCard.js';import{observe}from'../helpers.js';importtype{ApiResponse,Actions}from'../../core/index.js';importtype{ObservedArray,YTNode}from'../helpers.js';importtype{ISearchResponse}from'../types/index.js';exportdefaultclassSearchextendsFeed<ISearchResponse>{header?: SearchHeader;results: ObservedArray<YTNode>;refinements: string[];estimated_results: number;sub_menu?: SearchSubMenu;watch_card?: UniversalWatchCard;refinement_cards?: HorizontalCardList|null;constructor(actions: Actions,data: ApiResponse|ISearchResponse,already_parsed=false){super(actions,data,already_parsed);constcontents=this.page.contents_memo?.getType(SectionList).first().contents||this.page.on_response_received_commands?.first().contents;if(!contents)thrownewInnertubeError('No contents found in search response');if(this.page.header)this.header=this.page.header.item().as(SearchHeader);this.results=observe(contents.filterType(ItemSection).flatMap((section)=>section.contents));this.refinements=this.page.refinements||[];this.estimated_results=this.page.estimated_results||0;if(this.page.contents_memo){this.sub_menu=this.page.contents_memo.getType(SearchSubMenu).first();this.watch_card=this.page.contents_memo.getType(UniversalWatchCard).first();}this.refinement_cards=this.results?.firstOfType(HorizontalCardList);}/** * Applies given refinement card and returns a new {@link Search} object. Use {@link refinement_card_queries} to get a list of available refinement cards. */asyncselectRefinementCard(card: SearchRefinementCard|string): Promise<Search>{lettarget_card: SearchRefinementCard|undefined;if(typeofcard==='string'){if(!this.refinement_cards)thrownewInnertubeError('No refinement cards found.');target_card=this.refinement_cards?.cards.get({query: card})?.as(SearchRefinementCard);if(!target_card)thrownewInnertubeError(`Refinement card "${card}" not found`,{available_cards: this.refinement_card_queries});}elseif(card.type==='SearchRefinementCard'){target_card=card;}else{thrownewInnertubeError('Invalid refinement card!');}constpage=awaittarget_card.endpoint.call<ISearchResponse>(this.actions,{parse: true});returnnewSearch(this.actions,page,true);}/** * Returns a list of refinement card queries. */getrefinement_card_queries(): string[]{returnthis.refinement_cards?.cards.as(SearchRefinementCard).map((card)=>card.query)||[];}/** * Retrieves next batch of results. */asyncgetContinuation(): Promise<Search>{constresponse=awaitthis.getContinuationData();if(!response)thrownewInnertubeError('Could not get continuation data');returnnewSearch(this.actions,response,true);}}
import{Parser}from'../index.js';import{InnertubeError}from'../../utils/Utils.js';importCompactLinkfrom'../classes/CompactLink.js';importItemSectionfrom'../classes/ItemSection.js';importPageIntroductionfrom'../classes/PageIntroduction.js';importSectionListfrom'../classes/SectionList.js';importSettingsOptionsfrom'../classes/SettingsOptions.js';importSettingsSidebarfrom'../classes/SettingsSidebar.js';importSettingsSwitchfrom'../classes/SettingsSwitch.js';importCommentsHeaderfrom'../classes/comments/CommentsHeader.js';importItemSectionHeaderfrom'../classes/ItemSectionHeader.js';importItemSectionTabbedHeaderfrom'../classes/ItemSectionTabbedHeader.js';importTabfrom'../classes/Tab.js';importTwoColumnBrowseResultsfrom'../classes/TwoColumnBrowseResults.js';importtype{ApiResponse,Actions}from'../../core/index.js';importtype{IBrowseResponse}from'../types/index.js';exportdefaultclassSettings{
#page: IBrowseResponse;
#actions: Actions;sidebar?: SettingsSidebar;introduction?: PageIntroduction;sections;constructor(actions: Actions,response: ApiResponse){this.#actions =actions;this.#page =Parser.parseResponse<IBrowseResponse>(response.data);this.sidebar=this.#page.sidebar?.as(SettingsSidebar);if(!this.#page.contents)thrownewInnertubeError('Page contents not found');consttab=this.#page.contents.item().as(TwoColumnBrowseResults).tabs.array().as(Tab).get({selected: true});if(!tab)thrownewInnertubeError('Target tab not found');constcontents=tab.content?.as(SectionList).contents.as(ItemSection);this.introduction=contents?.shift()?.contents?.firstOfType(PageIntroduction);this.sections=contents?.map((el: ItemSection)=>({title: el.header?.is(CommentsHeader,ItemSectionHeader,ItemSectionTabbedHeader) ? el.header.title.toString() : null,contents: el.contents}));}/** * Selects an item from the sidebar menu. Use {@link sidebar_items} to see available items. */asyncselectSidebarItem(target_item: string|CompactLink): Promise<Settings>{if(!this.sidebar)thrownewInnertubeError('Sidebar not available');letitem: CompactLink|undefined;if(typeoftarget_item==='string'){item=this.sidebar.items.get({title: target_item});if(!item)thrownewInnertubeError(`Item "${target_item}" not found`,{available_items: this.sidebar_items});}elseif(target_item?.is(CompactLink)){item=target_item;}else{thrownewInnertubeError('Invalid item',{ target_item });}constresponse=awaititem.endpoint.call(this.#actions,{parse: false});returnnewSettings(this.#actions,response);}/** * Finds a setting by name and returns it. Use {@link setting_options} to see available options. */getSettingOption(name: string): SettingsSwitch{if(!this.sections)thrownewInnertubeError('Sections not available');for(constsectionofthis.sections){if(!section.contents)continue;for(constelofsection.contents){constoptions=el.as(SettingsOptions).options;if(options){for(constoptionofoptions){if(option.is(SettingsSwitch)&&option.title?.toString()===name)returnoption;}}}}thrownewInnertubeError(`Option "${name}" not found`,{available_options: this.setting_options});}/** * Returns settings available in the page. */getsetting_options(): string[]{if(!this.sections)thrownewInnertubeError('Sections not available');letoptions: any[]=[];for(constsectionofthis.sections){if(!section.contents)continue;for(constelofsection.contents){if(el.as(SettingsOptions).options)options=options.concat(el.as(SettingsOptions).options);}}returnoptions.map((opt)=>opt.title?.toString()).filter((el)=>el);}/** * Returns options available in the sidebar. */getsidebar_items(): string[]{if(!this.sidebar)thrownewInnertubeError('Sidebar not available');returnthis.sidebar.items.map((item)=>item.title.toString());}getpage(): IBrowseResponse{returnthis.#page;}}