diff --git a/lib/core/Music.js b/lib/core/Music.js index c6254bcc..6c561d94 100644 --- a/lib/core/Music.js +++ b/lib/core/Music.js @@ -6,6 +6,7 @@ const HomeFeed = require('../parser/ytmusic/HomeFeed'); const Explore = require('../parser/ytmusic/Explore'); const Library = require('../parser/ytmusic/Library'); const Artist = require('../parser/ytmusic/Artist'); +const Album = require('../parser/ytmusic/Album'); const { InnertubeError, observe } = require('../utils/Utils'); @@ -68,13 +69,25 @@ class Music { /** * Retrieves artist's info & content. * - * @param {string} id + * @param {string} artist_id + * @returns {Promise.} */ async getArtist(artist_id) { const response = await this.#actions.browse(artist_id, { client: 'YTMUSIC' }); return new Artist(response, this.#actions); } + /** + * Retrieves album. + * + * @param {string} album_id + * @returns {Promise.} + */ + async getAlbum(album_id) { + const response = await this.#actions.browse(album_id, { client: 'YTMUSIC' }); + return new Album(response, this.#actions); + } + /** * Retrieves song lyrics. * diff --git a/lib/parser/contents/classes/MusicDetailHeader.js b/lib/parser/contents/classes/MusicDetailHeader.js new file mode 100644 index 00000000..46f5967b --- /dev/null +++ b/lib/parser/contents/classes/MusicDetailHeader.js @@ -0,0 +1,33 @@ +'use strict'; + +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); +const Parser = require('..'); + +class MusicDetailHeader { + type = 'MusicDetailHeader'; + + constructor(data) { + this.title = new Text(data.title); + this.description = new Text(data.description); + this.subtitle = new Text(data.subtitle); + this.second_subtitle = new Text(data.secondSubtitle); + this.year = this.subtitle.runs.find((run) => /^[12][0-9]{3}$/.test(run.text)).text; + this.song_count = this.second_subtitle.runs[0].text; + this.total_duration = this.second_subtitle.runs[2].text; + this.thumbnails = Thumbnail.fromResponse(data.thumbnail.croppedSquareThumbnailRenderer.thumbnail); + this.badges = Parser.parse(data.subtitleBadges); + + const author = this.subtitle.runs.find((run) => run.endpoint.browse?.id.startsWith('UC')); + + author && (this.author = { + name: author.text, + channel_id: author.endpoint.browse.id, + endpoint: author.endpoint + }); + + this.menu = Parser.parse(data.menu); + } +} + +module.exports = MusicDetailHeader; \ No newline at end of file diff --git a/lib/parser/contents/classes/MusicImmersiveHeader.js b/lib/parser/contents/classes/MusicImmersiveHeader.js index 23109730..ae7f972e 100644 --- a/lib/parser/contents/classes/MusicImmersiveHeader.js +++ b/lib/parser/contents/classes/MusicImmersiveHeader.js @@ -16,7 +16,7 @@ class MusicImmersiveHeader { this.menu = Parser.parse(data.menu); this.play_button = Parser.parse(data.playButton); this.start_radio_button = Parser.parse(data.startRadioButton); - */ + */ } } diff --git a/lib/parser/contents/classes/MusicResponsiveListItem.js b/lib/parser/contents/classes/MusicResponsiveListItem.js index d26ac1cd..91ac3bb3 100644 --- a/lib/parser/contents/classes/MusicResponsiveListItem.js +++ b/lib/parser/contents/classes/MusicResponsiveListItem.js @@ -1,19 +1,26 @@ 'use strict'; const Parser = require('..'); +const Text = require('./Text'); const Utils = require('../../../utils/Utils'); const Thumbnail = require('./Thumbnail'); const NavigationEndpoint = require('./NavigationEndpoint'); class MusicResponsiveListItem { #flex_columns; + #fixed_columns; #playlist_item_data; constructor(data) { this.type = null; this.#flex_columns = Parser.parse(data.flexColumns); - this.#playlist_item_data = { video_id: data?.playlistItemData?.videoId || null }; + this.#fixed_columns = Parser.parse(data.fixedColumns); + + this.#playlist_item_data = { + video_id: data?.playlistItemData?.videoId || null, + playlist_set_video_id: data?.playlistItemData?.playlistSetVideoId || null + }; this.endpoint = data.navigationEndpoint && new NavigationEndpoint(data.navigationEndpoint) || null; @@ -36,7 +43,10 @@ class MusicResponsiveListItem { break; } - this.thumbnails = Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer.thumbnail); + data.index && + (this.index = new Text(data.index)); + + this.thumbnails = data.thumbnail && Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer.thumbnail) || []; this.badges = Parser.parse(data.badges) || []; this.menu = Parser.parse(data.menu); @@ -45,7 +55,7 @@ class MusicResponsiveListItem { #parseVideoOrSong() { const is_video = this.#flex_columns[1].title.runs - .some((run) => run.text.match(/(.*?) views/)); + ?.some((run) => run.text.match(/(.*?) views/)); if (is_video) { this.type = 'video'; @@ -60,15 +70,17 @@ class MusicResponsiveListItem { this.id = this.#playlist_item_data.video_id; this.title = this.#flex_columns[0].title.toString(); - const duration_text = this.#flex_columns[1].title.runs - .find((run) => /^\d+$/.test(run.text.replace(/:/g, '')))?.text; + const duration_text = + this.#flex_columns[1].title.runs?.find( + (run) => /^\d+$/.test(run.text.replace(/:/g, '')))?.text || + this.#fixed_columns[0].title.text; duration_text && (this.duration = { text: duration_text, seconds: Utils.timeToSeconds(duration_text) }); - const album = this.#flex_columns[1].title.runs.find((run) => run.endpoint.browse?.id.startsWith('MPR')); + const album = this.#flex_columns[1].title.runs?.find((run) => run.endpoint.browse?.id.startsWith('MPR')); album && (this.album = { id: album.endpoint.browse.id, @@ -76,7 +88,7 @@ class MusicResponsiveListItem { endpoint: album.endpoint }); - const artists = this.#flex_columns[1].title.runs.filter((run) => run.endpoint.browse?.id.startsWith('UC')); + const artists = this.#flex_columns[1].title.runs?.filter((run) => run.endpoint.browse?.id.startsWith('UC')); artists && (this.artists = artists.map((artist) => ({ name: artist.text, diff --git a/lib/parser/contents/classes/MusicResponsiveListItemFixedColumn.js b/lib/parser/contents/classes/MusicResponsiveListItemFixedColumn.js new file mode 100644 index 00000000..bfb2b201 --- /dev/null +++ b/lib/parser/contents/classes/MusicResponsiveListItemFixedColumn.js @@ -0,0 +1,14 @@ +'use strict'; + +const Text = require('./Text'); + +class MusicResponsiveListItemFixedColumn { + type = 'musicResponsiveListItemFlexColumnRenderer'; + + constructor(data) { + this.title = new Text(data.text); + this.display_priority = data.displayPriority; + } +} + +module.exports = MusicResponsiveListItemFixedColumn; \ No newline at end of file diff --git a/lib/parser/ytmusic/Album.js b/lib/parser/ytmusic/Album.js new file mode 100644 index 00000000..1a45655a --- /dev/null +++ b/lib/parser/ytmusic/Album.js @@ -0,0 +1,33 @@ +'use strict'; + +const Parser = require('../contents'); + +/** @namespace */ +class Album { + #page; + #actions; + + /** + * @param {object} response - API response. + * @param {import('../../core/Actions')} actions + */ + constructor(response, actions) { + this.#page = Parser.parseResponse(response.data); + this.#actions = actions; + + /** @type {import('../contents/classes/MusicDetailHeader')[]} */ + this.header = this.#page.header; + + /** @type {string} */ + this.url = this.#page.microformat.url_canonical; + + /** @type {import('../contents/classes/MusicResponsiveListItem')[]} */ + this.contents = this.#page.contents_memo.get('MusicShelf')?.[0].contents; + } + + get page() { + return this.#page; + } +} + +module.exports = Album; \ No newline at end of file