diff --git a/deno/package.json b/deno/package.json index f4baf9ed..c4d7284b 100644 --- a/deno/package.json +++ b/deno/package.json @@ -1,6 +1,6 @@ { "name": "youtubei.js", - "version": "5.1.0", + "version": "5.2.0", "description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).", "type": "module", "types": "./dist/src/platform/lib.d.ts", diff --git a/deno/src/core/OAuth.ts b/deno/src/core/OAuth.ts index ea90d90a..db3b9146 100644 --- a/deno/src/core/OAuth.ts +++ b/deno/src/core/OAuth.ts @@ -96,7 +96,7 @@ export default class OAuth { client_id: this.#identity.client_id, scope: Constants.OAUTH.SCOPE, device_id: Platform.shim.uuidv4(), - model_name: Constants.OAUTH.MODEL_NAME + device_model: Constants.OAUTH.MODEL_NAME }; const response = await this.#session.http.fetch_function(new URL('/o/oauth2/device/code', Constants.URLS.YT_BASE), { diff --git a/deno/src/core/endpoints/PlayerEndpoint.ts b/deno/src/core/endpoints/PlayerEndpoint.ts index 74ec799b..5a28edcb 100644 --- a/deno/src/core/endpoints/PlayerEndpoint.ts +++ b/deno/src/core/endpoints/PlayerEndpoint.ts @@ -38,7 +38,7 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest { client: opts.client, playlistId: opts.playlist_id, // Workaround streaming URLs returning 403 when using Android clients and throttling in web clients. - params: '8AEB' + params: '2AMBCgIQBg' } }; } \ No newline at end of file diff --git a/deno/src/parser/classes/CarouselLockup.ts b/deno/src/parser/classes/CarouselLockup.ts new file mode 100644 index 00000000..81cbf5f9 --- /dev/null +++ b/deno/src/parser/classes/CarouselLockup.ts @@ -0,0 +1,20 @@ +import { type ObservedArray, YTNode } from '../helpers.ts'; +import InfoRow from './InfoRow.ts'; +import Parser, { type RawNode } from '../index.ts'; +import CompactVideo from './CompactVideo.ts'; + +export default class CarouselLockup extends YTNode { + static type = 'CarouselLockup'; + + info_rows: ObservedArray; + video_lockup?: CompactVideo; + + constructor(data: RawNode) { + super(); + this.info_rows = Parser.parseArray(data.infoRows, InfoRow); + const video_lockup = Parser.parseItem(data.videoLockup, CompactVideo); + if (video_lockup != null) { + this.video_lockup = video_lockup; + } + } +} diff --git a/deno/src/parser/classes/EngagementPanelSectionList.ts b/deno/src/parser/classes/EngagementPanelSectionList.ts new file mode 100644 index 00000000..f35932a0 --- /dev/null +++ b/deno/src/parser/classes/EngagementPanelSectionList.ts @@ -0,0 +1,20 @@ +import { YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import ContinuationItem from './ContinuationItem.ts'; +import SectionList from './SectionList.ts'; +import StructuredDescriptionContent from './StructuredDescriptionContent.ts'; + +export default class EngagementPanelSectionList extends YTNode { + static type = 'EngagementPanelSectionList'; + + target_id: String; + content?: SectionList|ContinuationItem|StructuredDescriptionContent; + constructor(data: RawNode) { + super(); + this.target_id = data.targetId; + const content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent ]); + if (content !== null) { + this.content = content; + } + } +} diff --git a/deno/src/parser/classes/ExpandableVideoDescriptionBody.ts b/deno/src/parser/classes/ExpandableVideoDescriptionBody.ts new file mode 100644 index 00000000..8151d63c --- /dev/null +++ b/deno/src/parser/classes/ExpandableVideoDescriptionBody.ts @@ -0,0 +1,22 @@ +import { YTNode } from '../helpers.ts'; +import { type RawNode } from '../index.ts'; +import { Text } from '../misc.ts'; + +export default class ExpandableVideoDescriptionBody extends YTNode { + static type = 'ExpandableVideoDescriptionBody'; + + show_more_text: Text; + show_less_text: Text; + attributed_description_body_text: { + content: String + }; + + constructor(data: RawNode) { + super(); + this.show_more_text = new Text(data.showMoreText); + this.show_less_text = new Text(data.showLessText); + this.attributed_description_body_text = { + content: data.attributedDescriptionBodyText.content + }; + } +} diff --git a/deno/src/parser/classes/Factoid.ts b/deno/src/parser/classes/Factoid.ts new file mode 100644 index 00000000..f8b5c2a5 --- /dev/null +++ b/deno/src/parser/classes/Factoid.ts @@ -0,0 +1,17 @@ +import { YTNode } from '../helpers.ts'; +import { type RawNode } from '../index.ts'; +import { Text } from '../misc.ts'; + +export default class Factoid extends YTNode { + static type = 'Factoid'; + label: Text; + value: Text; + accessibility_text: String; + + constructor(data: RawNode) { + super(); + this.label = new Text(data.label); + this.value = new Text(data.value); + this.accessibility_text = data.accessibilityText; + } +} diff --git a/deno/src/parser/classes/InfoRow.ts b/deno/src/parser/classes/InfoRow.ts new file mode 100644 index 00000000..cca29132 --- /dev/null +++ b/deno/src/parser/classes/InfoRow.ts @@ -0,0 +1,38 @@ +import { YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import { Text } from '../misc.ts'; +import NavigationEndpoint from './NavigationEndpoint.ts'; + +export default class InfoRow extends YTNode { + static type = 'InfoRow'; + metadata_text?: Text; + metadata_endpoint?: NavigationEndpoint; + info_row_expand_status_key: String; + title: Text; + + constructor(data: RawNode) { + super(); + if ('defaultMetadata' in data && 'runs' in data.defaultMetadata) { + const runs = data.defaultMetadata.runs; + if (runs.length > 0) { + const run = runs[0]; + this.metadata_text = run?.text; + if ('navigationEndpoint' in run) { + this.metadata_endpoint = Parser.parseItem({ navigationEndpoint: run.navigationEndpoint }, NavigationEndpoint) || undefined; + } + } + } + if ('expandedMetadata' in data && 'runs' in data.expandedMetadata) { + this.metadata_text = new Text(data.expandedMetadata); + } + if (this.metadata_text === undefined) { + this.metadata_text = data.expandedMetadata?.simpleText + ? new Text(data.expandedMetadata) + : data.defaultMetadata?.simpleText + ? new Text(data.defaultMetadata) + : undefined; + } + this.info_row_expand_status_key = data.infoRowExpandStatusKey; + this.title = new Text(data.title); + } +} diff --git a/deno/src/parser/classes/Playlist.ts b/deno/src/parser/classes/Playlist.ts index 2563e180..17095819 100644 --- a/deno/src/parser/classes/Playlist.ts +++ b/deno/src/parser/classes/Playlist.ts @@ -1,6 +1,7 @@ import { YTNode, type ObservedArray } from '../helpers.ts'; import Parser, { type RawNode } from '../index.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; +import PlaylistVideoThumbnail from './PlaylistVideoThumbnail.ts'; import Author from './misc/Author.ts'; import Text from './misc/Text.ts'; import Thumbnail from './misc/Thumbnail.ts'; @@ -12,6 +13,7 @@ export default class Playlist extends YTNode { title: Text; author: Text | Author; thumbnails: Thumbnail[]; + thumbnail_renderer?: PlaylistVideoThumbnail; video_count: Text; video_count_short: Text; first_videos: ObservedArray; @@ -41,6 +43,10 @@ export default class Playlist extends YTNode { this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays); + if (Reflect.has(data, 'thumbnailRenderer')) { + this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, PlaylistVideoThumbnail) || undefined; + } + if (Reflect.has(data, 'viewPlaylistText')) { this.view_playlist = new Text(data.viewPlaylistText); } diff --git a/deno/src/parser/classes/StructuredDescriptionContent.ts b/deno/src/parser/classes/StructuredDescriptionContent.ts new file mode 100644 index 00000000..751c07ec --- /dev/null +++ b/deno/src/parser/classes/StructuredDescriptionContent.ts @@ -0,0 +1,16 @@ +import { type ObservedArray, YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import ExpandableVideoDescriptionBody from './ExpandableVideoDescriptionBody.ts'; +import VideoDescriptionHeader from './VideoDescriptionHeader.ts'; +import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.ts'; + +export default class StructuredDescriptionContent extends YTNode { + static type = 'StructuredDescriptionContent'; + + items: ObservedArray; + + constructor(data: RawNode) { + super(); + this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection ]); + } +} diff --git a/deno/src/parser/classes/VideoDescriptionHeader.ts b/deno/src/parser/classes/VideoDescriptionHeader.ts new file mode 100644 index 00000000..c9ffdb0a --- /dev/null +++ b/deno/src/parser/classes/VideoDescriptionHeader.ts @@ -0,0 +1,29 @@ +import { type ObservedArray, YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import { Text } from '../misc.ts'; +import type { Thumbnail } from '../misc.ts'; +import Factoid from './Factoid.ts'; +import NavigationEndpoint from './NavigationEndpoint.ts'; + +export default class VideoDescriptionHeader extends YTNode { + static type = 'VideoDescriptionHeader'; + + channel: Text; + channel_navigation_endpoint?: NavigationEndpoint; + channel_thumbnails: String[]; + factoids: ObservedArray; + publish_date: Text; + title: Text; + views: Text; + + constructor(data: RawNode) { + super(); + this.title = new Text(data.title); + this.channel = new Text(data.channel); + this.channel_navigation_endpoint = Parser.parseItem(data.channelNavigationEndpoint, NavigationEndpoint) || undefined; + this.channel_thumbnails = data.channelThumbnail.thumbnails.map((thumbnail: Thumbnail) => thumbnail.url); + this.publish_date = new Text(data.publishDate); + this.views = new Text(data.views); + this.factoids = Parser.parseArray(data.factoid, Factoid); + } +} diff --git a/deno/src/parser/classes/VideoDescriptionMusicSection.ts b/deno/src/parser/classes/VideoDescriptionMusicSection.ts new file mode 100644 index 00000000..8ee8ed1d --- /dev/null +++ b/deno/src/parser/classes/VideoDescriptionMusicSection.ts @@ -0,0 +1,16 @@ +import { type ObservedArray, YTNode } from '../helpers.ts'; +import CarouselLockup from './CarouselLockup.ts'; +import Parser, { type RawNode } from '../index.ts'; +import { Text } from '../misc.ts'; + +export default class VideoDescriptionMusicSection extends YTNode { + static type = 'VideoDescriptionMusicSection'; + + carousel_lockups: ObservedArray; + section_title: Text; + constructor(data: RawNode) { + super(); + this.carousel_lockups = Parser.parseArray(data.carouselLockups, CarouselLockup); + this.section_title = new Text(data.sectionTitle); + } +} diff --git a/deno/src/parser/classes/misc/VideoDetails.ts b/deno/src/parser/classes/misc/VideoDetails.ts index 2cd93987..c2f0a4b0 100644 --- a/deno/src/parser/classes/misc/VideoDetails.ts +++ b/deno/src/parser/classes/misc/VideoDetails.ts @@ -18,6 +18,7 @@ export default class VideoDetails { is_live_content: boolean; is_upcoming: boolean; is_crawlable: boolean; + is_post_live_dvr: boolean; constructor(data: RawNode) { this.id = data.videoId; @@ -35,6 +36,7 @@ export default class VideoDetails { this.is_live = !!data.isLive; this.is_live_content = !!data.isLiveContent; this.is_upcoming = !!data.isUpcoming; + this.is_post_live_dvr = !!data.isPostLiveDvr; this.is_crawlable = !!data.isCrawlable; } } \ No newline at end of file diff --git a/deno/src/parser/nodes.ts b/deno/src/parser/nodes.ts index aedd547f..20ce6758 100644 --- a/deno/src/parser/nodes.ts +++ b/deno/src/parser/nodes.ts @@ -30,6 +30,7 @@ export { default as Card } from './classes/Card.ts'; export { default as CardCollection } from './classes/CardCollection.ts'; export { default as CarouselHeader } from './classes/CarouselHeader.ts'; export { default as CarouselItem } from './classes/CarouselItem.ts'; +export { default as CarouselLockup } from './classes/CarouselLockup.ts'; export { default as Channel } from './classes/Channel.ts'; export { default as ChannelAboutFullMetadata } from './classes/ChannelAboutFullMetadata.ts'; export { default as ChannelAgeGate } from './classes/ChannelAgeGate.ts'; @@ -88,9 +89,12 @@ export { default as Endscreen } from './classes/Endscreen.ts'; export { default as EndscreenElement } from './classes/EndscreenElement.ts'; export { default as EndScreenPlaylist } from './classes/EndScreenPlaylist.ts'; export { default as EndScreenVideo } from './classes/EndScreenVideo.ts'; +export { default as EngagementPanelSectionList } from './classes/EngagementPanelSectionList.ts'; export { default as ExpandableMetadata } from './classes/ExpandableMetadata.ts'; export { default as ExpandableTab } from './classes/ExpandableTab.ts'; +export { default as ExpandableVideoDescriptionBody } from './classes/ExpandableVideoDescriptionBody.ts'; export { default as ExpandedShelfContents } from './classes/ExpandedShelfContents.ts'; +export { default as Factoid } from './classes/Factoid.ts'; export { default as FeedFilterChipBar } from './classes/FeedFilterChipBar.ts'; export { default as FeedTabbedHeader } from './classes/FeedTabbedHeader.ts'; export { default as GameCard } from './classes/GameCard.ts'; @@ -121,6 +125,7 @@ export { default as HorizontalMovieList } from './classes/HorizontalMovieList.ts export { default as IconLink } from './classes/IconLink.ts'; export { default as InfoPanelContainer } from './classes/InfoPanelContainer.ts'; export { default as InfoPanelContent } from './classes/InfoPanelContent.ts'; +export { default as InfoRow } from './classes/InfoRow.ts'; export { default as InteractiveTabbedHeader } from './classes/InteractiveTabbedHeader.ts'; export { default as ItemSection } from './classes/ItemSection.ts'; export { default as ItemSectionHeader } from './classes/ItemSectionHeader.ts'; @@ -299,6 +304,7 @@ export { default as SingleHeroImage } from './classes/SingleHeroImage.ts'; export { default as SlimOwner } from './classes/SlimOwner.ts'; export { default as SlimVideoMetadata } from './classes/SlimVideoMetadata.ts'; export { default as SortFilterSubMenu } from './classes/SortFilterSubMenu.ts'; +export { default as StructuredDescriptionContent } from './classes/StructuredDescriptionContent.ts'; export { default as SubFeedOption } from './classes/SubFeedOption.ts'; export { default as SubFeedSelector } from './classes/SubFeedSelector.ts'; export { default as SubscribeButton } from './classes/SubscribeButton.ts'; @@ -335,6 +341,8 @@ export { default as VerticalList } from './classes/VerticalList.ts'; export { default as VerticalWatchCardList } from './classes/VerticalWatchCardList.ts'; export { default as Video } from './classes/Video.ts'; export { default as VideoCard } from './classes/VideoCard.ts'; +export { default as VideoDescriptionHeader } from './classes/VideoDescriptionHeader.ts'; +export { default as VideoDescriptionMusicSection } from './classes/VideoDescriptionMusicSection.ts'; export { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.ts'; export { default as VideoOwner } from './classes/VideoOwner.ts'; export { default as VideoPrimaryInfo } from './classes/VideoPrimaryInfo.ts'; diff --git a/deno/src/parser/parser.ts b/deno/src/parser/parser.ts index a2ec8c38..70497a1f 100644 --- a/deno/src/parser/parser.ts +++ b/deno/src/parser/parser.ts @@ -264,6 +264,13 @@ export default class Parser { parsed_data.cards = cards; } + const engagement_panels = data.engagementPanels?.map((e) => { + const item = this.parseItem(e, YTNodes.EngagementPanelSectionList) as YTNodes.EngagementPanelSectionList; + return item; + }); + if (engagement_panels) { + parsed_data.engagement_panels = engagement_panels; + } this.#createMemo(); const items = this.parse(data.items); if (items) { @@ -502,14 +509,15 @@ export default class Parser { 'BrandVideoShelf', 'BrandVideoSingleton', 'StatementBanner', - 'GuideSigninPromo' + 'GuideSigninPromo', + 'AdsEngagementPanelContent' ]); static shouldIgnore(classname: string) { return this.ignore_list.has(classname); } - static #rt_nodes = new Map(Array.from(Object.entries(YTNodes))); + static #rt_nodes = new Map(Object.entries(YTNodes)); static #dynamic_nodes = new Map(); static getParserByName(classname: string) { @@ -739,4 +747,4 @@ export class LiveChatContinuation extends YTNode { this.viewer_name = data.viewerName; } -} \ No newline at end of file +} diff --git a/deno/src/parser/types/ParsedResponse.ts b/deno/src/parser/types/ParsedResponse.ts index bf8a17e9..fd85090d 100644 --- a/deno/src/parser/types/ParsedResponse.ts +++ b/deno/src/parser/types/ParsedResponse.ts @@ -17,7 +17,7 @@ import type VideoDetails from '../classes/misc/VideoDetails.ts'; import type Alert from '../classes/Alert.ts'; import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.ts'; - +import type EngagementPanelSectionList from '../classes/EngagementPanelSectionList.ts'; export interface IParsedResponse { actions?: SuperParsedResult; actions_memo?: Memo; @@ -73,6 +73,7 @@ export interface IParsedResponse { storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec; endscreen?: Endscreen; cards?: CardCollection; + engagement_panels?: EngagementPanelSectionList[]; items?: SuperParsedResult; } @@ -111,6 +112,7 @@ export interface INextResponse { on_response_received_endpoints?: ObservedArray; on_response_received_endpoints_memo?: Memo; player_overlays?: SuperParsedResult; + engagement_panels?: EngagementPanelSectionList[]; } export interface IBrowseResponse { @@ -163,4 +165,4 @@ export interface IUpdatedMetadataResponse { export interface IGuideResponse { items: SuperParsedResult; items_memo: Memo; -} \ No newline at end of file +} diff --git a/deno/src/parser/types/RawResponse.ts b/deno/src/parser/types/RawResponse.ts index 92fe7ead..95182013 100644 --- a/deno/src/parser/types/RawResponse.ts +++ b/deno/src/parser/types/RawResponse.ts @@ -53,4 +53,5 @@ export interface IRawResponse { cards?: RawNode; items?: RawNode[]; frameworkUpdates?: any; -} \ No newline at end of file + engagementPanels: RawNode[]; +} diff --git a/deno/src/parser/youtube/Search.ts b/deno/src/parser/youtube/Search.ts index 8d566157..7a3c43d4 100644 --- a/deno/src/parser/youtube/Search.ts +++ b/deno/src/parser/youtube/Search.ts @@ -30,7 +30,7 @@ class Search extends Feed { if (!contents) throw new InnertubeError('No contents found in search response'); - this.results = contents.filterType(ItemSection).find((section) => section.contents && section.contents.length > 0)?.contents; + this.results = contents.find((content) => content.is(ItemSection) && content.contents && content.contents.length > 0)?.as(ItemSection).contents; this.refinements = this.page.refinements || []; this.estimated_results = this.page.estimated_results; diff --git a/deno/src/parser/youtube/VideoInfo.ts b/deno/src/parser/youtube/VideoInfo.ts index dca96477..0014b4b1 100644 --- a/deno/src/parser/youtube/VideoInfo.ts +++ b/deno/src/parser/youtube/VideoInfo.ts @@ -33,6 +33,8 @@ import type { ObservedArray, YTNode } from '../helpers.ts'; import { InnertubeError } from '../../utils/Utils.ts'; import { MediaInfo } from '../../core/mixins/index.ts'; +import StructuredDescriptionContent from '../classes/StructuredDescriptionContent.ts'; +import { VideoDescriptionMusicSection } from '../nodes.ts'; class VideoInfo extends MediaInfo { #watch_next_continuation?: ContinuationItem; @@ -336,41 +338,46 @@ class VideoInfo extends MediaInfo { /** * Get songs used in the video. */ - // TODO: this seems to be broken with the new UI, further investigation needed get music_tracks() { - /* - Const metadata = this.secondary_info?.metadata; - if (!metadata) - return []; - const songs = []; - let current_song: Record = {}; - let is_music_section = false; - for (let i = 0; i < metadata.rows.length; i++) { - const row = metadata.rows[i]; - if (row.is(MetadataRowHeader)) { - if (row.content?.toString().toLowerCase().startsWith('music')) { - is_music_section = true; - i++; // Skip the learn more link + const description_content = this.page[1]?.engagement_panels?.filter((panel) => panel.content?.is(StructuredDescriptionContent)); + if (description_content !== undefined && description_content.length > 0) { + const music_section = (description_content[0].content as StructuredDescriptionContent)?.items.filter((item: YTNode) => item?.is(VideoDescriptionMusicSection)) as VideoDescriptionMusicSection[]; + if (music_section !== undefined && music_section.length > 0) { + return music_section[0].carousel_lockups?.map((lookup) => { + let song, artist, album, license, videoId, channelId; + // If the song isn't in the video_lockup, it should be in the info_rows + song = lookup.video_lockup?.title; + // If the video id isn't in the video_lockup, it should be in the info_rows + videoId = lookup.video_lockup?.endpoint.payload.videoId; + for (let i = 0; i < lookup.info_rows.length; i++) { + const info_row = lookup.info_rows[i]; + if (info_row.info_row_expand_status_key === undefined) { + if (song === undefined) { + song = info_row.metadata_text; + if (videoId === undefined) { + videoId = info_row.metadata_endpoint?.payload; } - continue; + } else { + album = info_row.metadata_text; + } + } else { + if (info_row.info_row_expand_status_key?.indexOf('structured-description-music-section-artists-row-state-id') !== -1) { + artist = info_row.metadata_text; + if (channelId === undefined) { + channelId = info_row.metadata_endpoint?.payload?.browseId; + } + } + if (info_row.info_row_expand_status_key?.indexOf('structured-description-music-section-licenses-row-state-id') !== -1) { + license = info_row.metadata_text; + } } - if (!is_music_section) - continue; - if (row.is(MetadataRow)) - current_song[row.title?.toString().toLowerCase().replace(/ /g, '_')] = row.contents; - // TODO: this makes no sense, we continue above when - if (row.has_divider_line) { - songs.push(current_song); - current_song = {}; - } - - } - if (is_music_section) - songs.push(current_song); - return songs; - */ + } + return { song, artist, album, license, videoId, channelId }; + }); + } + } return []; } } -export default VideoInfo; \ No newline at end of file +export default VideoInfo; diff --git a/deno/src/parser/ytmusic/HomeFeed.ts b/deno/src/parser/ytmusic/HomeFeed.ts index 51c34e84..750ef6d5 100644 --- a/deno/src/parser/ytmusic/HomeFeed.ts +++ b/deno/src/parser/ytmusic/HomeFeed.ts @@ -9,6 +9,8 @@ import type { ApiResponse } from '../../core/Actions.ts'; import type { ObservedArray } from '../helpers.ts'; import type { IBrowseResponse } from '../types/ParsedResponse.ts'; import { InnertubeError } from '../../utils/Utils.ts'; +import ChipCloud from '../classes/ChipCloud.ts'; +import ChipCloudChip from '../classes/ChipCloudChip.ts'; class HomeFeed { #page: IBrowseResponse; @@ -16,6 +18,7 @@ class HomeFeed { #continuation?: string; sections?: ObservedArray; + header?: ChipCloud; constructor(response: ApiResponse, actions: Actions) { this.#actions = actions; @@ -36,6 +39,7 @@ class HomeFeed { return; } + this.header = tab.content?.as(SectionList).header?.as(ChipCloud); this.#continuation = tab.content?.as(SectionList).continuation; this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf, MusicTastebuilderShelf); } @@ -55,6 +59,33 @@ class HomeFeed { return new HomeFeed(response, this.#actions); } + async applyFilter(target_filter: string | ChipCloudChip): Promise { + let cloud_chip: ChipCloudChip | undefined; + + if (typeof target_filter === 'string') { + cloud_chip = this.header?.chips?.as(ChipCloudChip).get({ text: target_filter }); + if (!cloud_chip) + throw new InnertubeError('Could not find filter with given name.', { available_filters: this.filters }); + } else if (target_filter?.is(ChipCloudChip)) { + cloud_chip = target_filter; + } + + if (!cloud_chip) + throw new InnertubeError('Invalid filter', { available_filters: this.filters }); + + if (cloud_chip?.is_selected) return this; + + if (!cloud_chip.endpoint) + throw new InnertubeError('Selected filter does not have an endpoint.'); + + const response = await cloud_chip.endpoint.call(this.#actions, { client: 'YTMUSIC' }); + return new HomeFeed(response, this.#actions); + } + + get filters(): string[] { + return this.header?.chips?.as(ChipCloudChip).map((chip) => chip.text) || []; + } + get has_continuation(): boolean { return !!this.#continuation; } diff --git a/deno/src/utils/Constants.ts b/deno/src/utils/Constants.ts index 21ec752b..36ee278b 100644 --- a/deno/src/utils/Constants.ts +++ b/deno/src/utils/Constants.ts @@ -29,13 +29,13 @@ export const OAUTH = Object.freeze({ }), REGEX: Object.freeze({ AUTH_SCRIPT: /