diff --git a/src/parser/classes/MusicResponsiveListItem.ts b/src/parser/classes/MusicResponsiveListItem.ts index 105aa525..bee0fc56 100644 --- a/src/parser/classes/MusicResponsiveListItem.ts +++ b/src/parser/classes/MusicResponsiveListItem.ts @@ -59,7 +59,7 @@ class MusicResponsiveListItem extends YTNode { channel_id?: string endpoint?: NavigationEndpoint }; - item_count?: number; + item_count?: string | undefined; year?: string; constructor(data: any) { @@ -190,14 +190,21 @@ class MusicResponsiveListItem extends YTNode { #parsePlaylist() { this.id = this.endpoint?.browse?.id; this.title = this.#flex_columns[0].key('title').instanceof(Text).toString(); - this.item_count = parseInt(this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => run.text.match(/\d+ (song|songs)/))?.text.match(/\d+/g)); + + const item_count_run = this.#flex_columns[1].key('title') + .instanceof(Text).runs?.find((run) => run.text.match(/\d+ (song|songs)/)); + + this.item_count = item_count_run ? item_count_run.text : undefined; const author = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.browse?.id.startsWith('UC')) as TextRun; - author && (this.author = { - name: author.text, - channel_id: author.endpoint?.browse?.id, - endpoint: author.endpoint - }); + + if (author) { + this.author = { + name: author.text, + channel_id: author.endpoint?.browse?.id, + endpoint: author.endpoint + }; + } } } diff --git a/src/parser/classes/Playlist.js b/src/parser/classes/Playlist.ts similarity index 74% rename from src/parser/classes/Playlist.js rename to src/parser/classes/Playlist.ts index 78ade20d..e65c2957 100644 --- a/src/parser/classes/Playlist.js +++ b/src/parser/classes/Playlist.ts @@ -8,7 +8,20 @@ import { YTNode } from '../helpers'; class Playlist extends YTNode { static type = 'Playlist'; - constructor(data) { + id: string; + title: Text; + author: Text | PlaylistAuthor; + thumbnails: Thumbnail[]; + video_count: Text; + video_count_short: Text; + first_videos; + share_url: string | null; + menu; + badges; + endpoint: NavigationEndpoint; + thumbnail_overlays; + + constructor(data: any) { super(); this.id = data.playlistId; this.title = new Text(data.title); @@ -17,7 +30,7 @@ class Playlist extends YTNode { new Text(data.shortBylineText) : new PlaylistAuthor(data.longBylineText, data.ownerBadges, null); - this.thumbnails = Thumbnail.fromResponse(data.thumbnail || { thumbnails: data.thumbnails.map((th) => th.thumbnails).flat(1) }); + this.thumbnails = Thumbnail.fromResponse(data.thumbnail || { thumbnails: data.thumbnails.map((th: any) => th.thumbnails).flat(1) }); this.video_count = new Text(data.thumbnailText); this.video_count_short = new Text(data.videoCountShortText); this.first_videos = Parser.parse(data.videos) || []; diff --git a/src/parser/classes/PlaylistHeader.js b/src/parser/classes/PlaylistHeader.ts similarity index 66% rename from src/parser/classes/PlaylistHeader.js rename to src/parser/classes/PlaylistHeader.ts index 9cb40092..1677e54d 100644 --- a/src/parser/classes/PlaylistHeader.js +++ b/src/parser/classes/PlaylistHeader.ts @@ -6,12 +6,28 @@ import { YTNode } from '../helpers'; class PlaylistHeader extends YTNode { static type = 'PlaylistHeader'; - constructor(data) { + id: string; + title: Text; + stats: Text[]; + brief_stats: Text[]; + author: PlaylistAuthor; + description: Text; + num_videos: Text; + view_count: Text; + can_share: boolean; + can_delete: boolean; + is_editable: boolean; + privacy: string; + save_button; + shuffle_play_button; + menu; + + constructor(data: any) { super(); this.id = data.playlistId; this.title = new Text(data.title); - this.stats = data.stats.map((stat) => new Text(stat)); - this.brief_stats = data.briefStats.map((stat) => new Text(stat)); + this.stats = data.stats.map((stat: any) => new Text(stat)); + this.brief_stats = data.briefStats.map((stat: any) => new Text(stat)); this.author = new PlaylistAuthor({ ...data.ownerText, navigationEndpoint: data.ownerEndpoint }, data.ownerBadges, null); this.description = new Text(data.descriptionText); this.num_videos = new Text(data.numVideosText); diff --git a/src/parser/classes/PlaylistInfoCardContent.js b/src/parser/classes/PlaylistInfoCardContent.ts similarity index 75% rename from src/parser/classes/PlaylistInfoCardContent.js rename to src/parser/classes/PlaylistInfoCardContent.ts index 61140265..6a683a01 100644 --- a/src/parser/classes/PlaylistInfoCardContent.js +++ b/src/parser/classes/PlaylistInfoCardContent.ts @@ -6,7 +6,13 @@ import { YTNode } from '../helpers'; class PlaylistInfoCardContent extends YTNode { static type = 'PlaylistInfoCardContent'; - constructor(data) { + title: Text; + thumbnails: Thumbnail[]; + video_count: Text; + channel_name: Text; + endpoint: NavigationEndpoint; + + constructor(data: any) { super(); this.title = new Text(data.playlistTitle); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); diff --git a/src/parser/classes/PlaylistMetadata.js b/src/parser/classes/PlaylistMetadata.ts similarity index 72% rename from src/parser/classes/PlaylistMetadata.js rename to src/parser/classes/PlaylistMetadata.ts index 8efe09a9..b27826af 100644 --- a/src/parser/classes/PlaylistMetadata.js +++ b/src/parser/classes/PlaylistMetadata.ts @@ -3,7 +3,10 @@ import { YTNode } from '../helpers'; class PlaylistMetadata extends YTNode { static type = 'PlaylistMetadata'; - constructor(data) { + title: string; + description: string; + + constructor(data: any) { super(); this.title = data.title; this.description = data.description || null; diff --git a/src/parser/classes/PlaylistPanel.js b/src/parser/classes/PlaylistPanel.ts similarity index 70% rename from src/parser/classes/PlaylistPanel.js rename to src/parser/classes/PlaylistPanel.ts index 82ea6f93..d61a1f4d 100644 --- a/src/parser/classes/PlaylistPanel.js +++ b/src/parser/classes/PlaylistPanel.ts @@ -5,7 +5,17 @@ import { YTNode } from '../helpers'; class PlaylistPanel extends YTNode { static type = 'PlaylistPanel'; - constructor(data) { + title: string; + title_text: Text; + contents; + playlist_id: string; + is_infinite: boolean; + continuation: string; + is_editable: boolean; + preview_description: string; + num_items_to_show: string; + + constructor(data: any) { super(); this.title = data.title; this.title_text = new Text(data.titleText); diff --git a/src/parser/classes/PlaylistPanelVideo.js b/src/parser/classes/PlaylistPanelVideo.js deleted file mode 100644 index cf7b4af3..00000000 --- a/src/parser/classes/PlaylistPanelVideo.js +++ /dev/null @@ -1,52 +0,0 @@ -import Parser from '../index'; -import Text from './misc/Text'; -import Thumbnail from './misc/Thumbnail'; -import NavigationEndpoint from './NavigationEndpoint'; -import { timeToSeconds } from '../../utils/Utils'; -import { YTNode } from '../helpers'; - -class PlaylistPanelVideo extends YTNode { - static type = 'PlaylistPanelVideo'; - - constructor(data) { - super(); - this.title = new Text(data.title); - this.thumbnail = Thumbnail.fromResponse(data.thumbnail); - this.endpoint = new NavigationEndpoint(data.navigationEndpoint); - this.selected = data.selected; - this.video_id = data.videoId; - - this.duration = { - text: new Text(data.lengthText).toString(), - seconds: timeToSeconds(new Text(data.lengthText).toString()) - }; - - const album = new Text(data.longBylineText).runs?.find((run) => run.endpoint?.browse?.id.startsWith('MPR')); - const artists = new Text(data.longBylineText).runs?.filter((run) => run.endpoint?.browse?.id.startsWith('UC')); - - this.author = new Text(data.shortBylineText).toString(); - - if (album) { - this.album = { - id: album.endpoint?.browse.id, - name: album.text, - year: new Text(data.longBylineText).runs.slice(-1)[0].text, - endpoint: album.endpoint - }; - } - - if (artists) { - this.artists = artists.map((artist) => ({ - name: artist.text, - channel_id: artist.endpoint?.browse.id, - endpoint: artist.endpoint - })); - } - - this.badges = Parser.parse(data.badges); - this.menu = Parser.parse(data.menu); - this.set_video_id = data.playlistSetVideoId; - } -} - -export default PlaylistPanelVideo; \ No newline at end of file diff --git a/src/parser/classes/PlaylistPanelVideo.ts b/src/parser/classes/PlaylistPanelVideo.ts new file mode 100644 index 00000000..90ce6a55 --- /dev/null +++ b/src/parser/classes/PlaylistPanelVideo.ts @@ -0,0 +1,85 @@ +import Parser from '../index'; +import Text from './misc/Text'; +import TextRun from './misc/TextRun'; +import Thumbnail from './misc/Thumbnail'; +import NavigationEndpoint from './NavigationEndpoint'; +import { timeToSeconds } from '../../utils/Utils'; + +import { YTNode } from '../helpers'; + +class PlaylistPanelVideo extends YTNode { + static type = 'PlaylistPanelVideo'; + + title: Text; + thumbnail: Thumbnail[]; + endpoint: NavigationEndpoint; + selected: boolean; + video_id: string; + + duration: { + text: string; + seconds: number + }; + + author: string; + + album?: { + id: string | undefined; + name: string; + year: string | undefined; + endpoint: NavigationEndpoint | undefined; + }; + + artists?: { + name: string; + channel_id: string | undefined; + endpoint: NavigationEndpoint | undefined; + }[]; + + badges; + menu; + set_video_id: string | undefined; + + constructor(data: any) { + super(); + + this.title = new Text(data.title); + this.thumbnail = Thumbnail.fromResponse(data.thumbnail); + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + this.selected = data.selected; + this.video_id = data.videoId; + + this.duration = { + text: new Text(data.lengthText).toString(), + seconds: timeToSeconds(new Text(data.lengthText).toString()) + }; + + const album = new Text(data.longBylineText).runs?.find((run: any) => run.endpoint?.browse?.id.startsWith('MPR')); + const artists = new Text(data.longBylineText).runs?.filter((run: any) => run.endpoint?.browse?.id.startsWith('UC')); + + this.author = new Text(data.shortBylineText).toString(); + + if (album) { + this.album = { + id: (album as TextRun).endpoint?.browse?.id, + name: (album as TextRun).text, + year: new Text(data.longBylineText).runs?.slice(-1)[0].text, + endpoint: (album as TextRun).endpoint + }; + } + + if (artists) { + this.artists = artists.map((artist) => ({ + name: (artist as TextRun).text, + channel_id: (artist as TextRun).endpoint?.browse?.id, + endpoint: (artist as TextRun).endpoint + })); + } + + this.badges = Parser.parse(data.badges); + this.menu = Parser.parse(data.menu); + this.set_video_id = data.playlistSetVideoId; + } +} + +export default PlaylistPanelVideo; \ No newline at end of file diff --git a/src/parser/classes/PlaylistSidebar.js b/src/parser/classes/PlaylistSidebar.ts similarity index 80% rename from src/parser/classes/PlaylistSidebar.js rename to src/parser/classes/PlaylistSidebar.ts index 49a335ce..10a58fa1 100644 --- a/src/parser/classes/PlaylistSidebar.js +++ b/src/parser/classes/PlaylistSidebar.ts @@ -4,7 +4,9 @@ import { YTNode } from '../helpers'; class PlaylistSidebar extends YTNode { static type = 'PlaylistSidebar'; - constructor(data) { + items; + + constructor(data: any) { super(); this.items = Parser.parse(data.items); } diff --git a/src/parser/classes/PlaylistSidebarPrimaryInfo.js b/src/parser/classes/PlaylistSidebarPrimaryInfo.ts similarity index 68% rename from src/parser/classes/PlaylistSidebarPrimaryInfo.js rename to src/parser/classes/PlaylistSidebarPrimaryInfo.ts index 86760bf6..2a8a2884 100644 --- a/src/parser/classes/PlaylistSidebarPrimaryInfo.js +++ b/src/parser/classes/PlaylistSidebarPrimaryInfo.ts @@ -1,14 +1,22 @@ import Parser from '../index'; -import NavigationEndpoint from './NavigationEndpoint'; import Text from './misc/Text'; +import NavigationEndpoint from './NavigationEndpoint'; + import { YTNode } from '../helpers'; class PlaylistSidebarPrimaryInfo extends YTNode { static type = 'PlaylistSidebarPrimaryInfo'; - constructor(data) { + stats: Text[]; + thumbnail_renderer; + title: Text; + menu; + endpoint: NavigationEndpoint; + description: Text; + + constructor(data: any) { super(); - this.stats = data.stats.map((stat) => new Text(stat)); + this.stats = data.stats.map((stat: any) => new Text(stat)); this.thumbnail_renderer = Parser.parse(data.thumbnailRenderer); this.title = new Text(data.title); this.menu = data.menu && Parser.parse(data.menu); diff --git a/src/parser/classes/PlaylistSidebarSecondaryInfo.js b/src/parser/classes/PlaylistSidebarSecondaryInfo.ts similarity index 76% rename from src/parser/classes/PlaylistSidebarSecondaryInfo.js rename to src/parser/classes/PlaylistSidebarSecondaryInfo.ts index d7d709c6..dd3f892b 100644 --- a/src/parser/classes/PlaylistSidebarSecondaryInfo.js +++ b/src/parser/classes/PlaylistSidebarSecondaryInfo.ts @@ -4,7 +4,10 @@ import { YTNode } from '../helpers'; class PlaylistSidebarSecondaryInfo extends YTNode { static type = 'PlaylistSidebarSecondaryInfo'; - constructor(data) { + owner; + button; + + constructor(data: any) { super(); this.owner = Parser.parse(data.videoOwner) || null; this.button = Parser.parse(data.button) || null; diff --git a/src/parser/classes/PlaylistVideo.js b/src/parser/classes/PlaylistVideo.ts similarity index 73% rename from src/parser/classes/PlaylistVideo.js rename to src/parser/classes/PlaylistVideo.ts index 8664ab5f..bf7636ed 100644 --- a/src/parser/classes/PlaylistVideo.js +++ b/src/parser/classes/PlaylistVideo.ts @@ -8,7 +8,23 @@ import { YTNode } from '../helpers'; class PlaylistVideo extends YTNode { static type = 'PlaylistVideo'; - constructor(data) { + id: string; + index: Text; + title: Text; + author: PlaylistAuthor; + thumbnails: Thumbnail[]; + thumbnail_overlays; + set_video_id: string | undefined; + endpoint: NavigationEndpoint; + is_playable: boolean; + menu; + + duration: { + text: string; + seconds: number; + }; + + constructor(data: any) { super(); this.id = data.videoId; this.index = new Text(data.index); diff --git a/src/parser/classes/PlaylistVideoList.js b/src/parser/classes/PlaylistVideoList.ts similarity index 71% rename from src/parser/classes/PlaylistVideoList.js rename to src/parser/classes/PlaylistVideoList.ts index 16450b7e..3bf31242 100644 --- a/src/parser/classes/PlaylistVideoList.js +++ b/src/parser/classes/PlaylistVideoList.ts @@ -4,7 +4,12 @@ import { YTNode } from '../helpers'; class PlaylistVideoList extends YTNode { static type = 'PlaylistVideoList'; - constructor(data) { + id: string; + is_editable: boolean; + can_reorder: boolean; + videos; + + constructor(data: any) { super(); this.id = data.playlistId; this.is_editable = data.isEditable; diff --git a/src/parser/classes/PlaylistVideoThumbnail.js b/src/parser/classes/PlaylistVideoThumbnail.ts similarity index 73% rename from src/parser/classes/PlaylistVideoThumbnail.js rename to src/parser/classes/PlaylistVideoThumbnail.ts index e360ca9f..42f08263 100644 --- a/src/parser/classes/PlaylistVideoThumbnail.js +++ b/src/parser/classes/PlaylistVideoThumbnail.ts @@ -4,7 +4,9 @@ import { YTNode } from '../helpers'; class PlaylistVideoThumbnail extends YTNode { static type = 'PlaylistVideoThumbnail'; - constructor(data) { + thumbnail: Thumbnail[]; + + constructor(data: any) { super(); this.thumbnail = Thumbnail.fromResponse(data.thumbnail); } diff --git a/src/parser/classes/EmojiRun.js b/src/parser/classes/misc/EmojiRun.ts similarity index 54% rename from src/parser/classes/EmojiRun.js rename to src/parser/classes/misc/EmojiRun.ts index 36ec3e01..782311c7 100644 --- a/src/parser/classes/EmojiRun.js +++ b/src/parser/classes/misc/EmojiRun.ts @@ -1,16 +1,19 @@ -import Thumbnail from './misc/Thumbnail'; -import { YTNode } from '../helpers'; +import Thumbnail from './Thumbnail'; -class EmojiRun extends YTNode { - static type = 'EmojiRun'; - - constructor(data) { - super(); +class EmojiRun { + text: string; + emoji: { + emoji_id: string; + shortcuts: string[]; + search_terms: string[]; + image: Thumbnail[]; + }; + constructor(data: any) { this.text = data.emoji?.emojiId || data.emoji?.shortcuts?.[0] || - null; + ''; this.emoji = { emoji_id: data.emoji.emojiId, diff --git a/src/parser/classes/misc/PlaylistAuthor.js b/src/parser/classes/misc/PlaylistAuthor.js index ada0a0c6..518f8523 100644 --- a/src/parser/classes/misc/PlaylistAuthor.js +++ b/src/parser/classes/misc/PlaylistAuthor.js @@ -1,8 +1,8 @@ import Author from './Author'; class PlaylistAuthor extends Author { - constructor(data) { - super(data); + constructor(item, badges, thumbs) { + super(item, badges, thumbs); delete this.badges; delete this.is_verified; delete this.is_verified_artist; diff --git a/src/parser/classes/misc/Text.ts b/src/parser/classes/misc/Text.ts index b734352c..205efd41 100644 --- a/src/parser/classes/misc/Text.ts +++ b/src/parser/classes/misc/Text.ts @@ -1,5 +1,5 @@ import TextRun from './TextRun'; -import EmojiRun from '../EmojiRun'; +import EmojiRun from './EmojiRun'; class Text { text: string; diff --git a/src/parser/classes/misc/TextRun.js b/src/parser/classes/misc/TextRun.ts similarity index 65% rename from src/parser/classes/misc/TextRun.js rename to src/parser/classes/misc/TextRun.ts index 0ab7ee7e..230019af 100644 --- a/src/parser/classes/misc/TextRun.js +++ b/src/parser/classes/misc/TextRun.ts @@ -1,7 +1,10 @@ import NavigationEndpoint from '../NavigationEndpoint'; class TextRun { - constructor(data) { + text: string; + endpoint: NavigationEndpoint | undefined; + + constructor(data: any) { this.text = data.text; this.endpoint = data.navigationEndpoint ? new NavigationEndpoint(data.navigationEndpoint) : undefined; } diff --git a/src/parser/map.ts b/src/parser/map.ts index 9d14611e..9e896d8e 100644 --- a/src/parser/map.ts +++ b/src/parser/map.ts @@ -50,7 +50,6 @@ import { default as DidYouMean } from './classes/DidYouMean'; import { default as DownloadButton } from './classes/DownloadButton'; import { default as Element } from './classes/Element'; import { default as EmergencyOnebox } from './classes/EmergencyOnebox'; -import { default as EmojiRun } from './classes/EmojiRun'; import { default as Endscreen } from './classes/Endscreen'; import { default as EndscreenElement } from './classes/EndscreenElement'; import { default as EndScreenPlaylist } from './classes/EndScreenPlaylist'; @@ -282,7 +281,6 @@ const map: Record = { DownloadButton, Element, EmergencyOnebox, - EmojiRun, Endscreen, EndscreenElement, EndScreenPlaylist, diff --git a/src/parser/youtube/Playlist.ts b/src/parser/youtube/Playlist.ts index a7d2010b..c925bb7d 100644 --- a/src/parser/youtube/Playlist.ts +++ b/src/parser/youtube/Playlist.ts @@ -1,47 +1,55 @@ import Actions from '../../core/Actions'; import Feed from '../../core/Feed'; -import { InnertubeError } from '../../utils/Utils'; + import Thumbnail from '../classes/misc/Thumbnail'; +import VideoOwner from '../classes/VideoOwner'; + +import PlaylistSidebar from '../classes/PlaylistSidebar'; +import PlaylistMetadata from '../classes/PlaylistMetadata'; import PlaylistSidebarPrimaryInfo from '../classes/PlaylistSidebarPrimaryInfo'; import PlaylistSidebarSecondaryInfo from '../classes/PlaylistSidebarSecondaryInfo'; -import VideoOwner from '../classes/VideoOwner'; +import PlaylistVideoThumbnail from '../classes/PlaylistVideoThumbnail'; +import PlaylistHeader from '../classes/PlaylistHeader'; class Playlist extends Feed { info; menu; endpoint; + constructor(actions: Actions, data: any, already_parsed = false) { super(actions, data, already_parsed); - const primary_info = this.page.sidebar?.key('contents').parsed().array().firstOfType(PlaylistSidebarPrimaryInfo); - const secondary_info = this.page.sidebar?.key('contents').parsed().array().firstOfType(PlaylistSidebarSecondaryInfo); + + const header = this.page.header.item().as(PlaylistHeader); + const primary_info = this.page.sidebar?.as(PlaylistSidebar).contents.array().firstOfType(PlaylistSidebarPrimaryInfo); + const secondary_info = this.page.sidebar?.as(PlaylistSidebar).contents.array().firstOfType(PlaylistSidebarSecondaryInfo); + this.info = { - ...this.page.metadata, + ...this.page.metadata.item().as(PlaylistMetadata), ...{ author: secondary_info?.owner.item().as(VideoOwner).author, - thumbnails: primary_info?.thumbnail_renderer.item().key('thumbnail').array().map((thumbnail) => { - if (!(thumbnail instanceof Thumbnail)) - throw new InnertubeError('Thumbnail is not of type Thumbnail.'); - return thumbnail; - }), + thumbnails: primary_info?.thumbnail_renderer.item().as(PlaylistVideoThumbnail).thumbnail as Thumbnail[], total_items: this.#getStat(0, primary_info), views: this.#getStat(1, primary_info), last_updated: this.#getStat(2, primary_info), - can_share: this.page.header.item().key('can_share').boolean(), - can_delete: this.page.header.item().key('can_delete').boolean(), - is_editable: this.page.header.item().key('is_editable').boolean(), - privacy: this.page.header.item().key('privacy').any() + 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; } - #getStat(index: number, primary_info?: PlaylistSidebarPrimaryInfo) { - if (!primary_info || !primary_info.stats) - return 'N/A'; + + #getStat(index: number, primary_info?: PlaylistSidebarPrimaryInfo): string { + if (!primary_info || !primary_info.stats) return 'N/A'; return primary_info.stats[index]?.toString() || 'N/A'; } + get items() { return this.videos; } } -export default Playlist; + +export default Playlist; \ No newline at end of file diff --git a/src/parser/ytmusic/Search.ts b/src/parser/ytmusic/Search.ts index 213b94c7..857b8635 100644 --- a/src/parser/ytmusic/Search.ts +++ b/src/parser/ytmusic/Search.ts @@ -142,7 +142,7 @@ class Search { } get playlists() { - return this.sections?.find((section) => section.title === 'Playlists'); + return this.sections?.find((section) => section.title === 'Community playlists'); } get page() {