feat(ytmusic): add support for retrieving albums

This commit is contained in:
LuanRT
2022-06-20 16:33:51 -03:00
parent e90285bfab
commit 97d4cc1056
6 changed files with 114 additions and 9 deletions

View File

@@ -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.<Artist>}
*/
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.<Album>}
*/
async getAlbum(album_id) {
const response = await this.#actions.browse(album_id, { client: 'YTMUSIC' });
return new Album(response, this.#actions);
}
/**
* Retrieves song lyrics.
*

View File

@@ -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;

View File

@@ -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);
*/
*/
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;