feat(ytmusic): add support for artists

Available through `Innertube#music.getArtist(id: string)`

#78
This commit is contained in:
LuanRT
2022-06-20 06:08:24 -03:00
parent 99b88e2684
commit 7fc9b526b0
13 changed files with 142 additions and 22 deletions

View File

@@ -5,6 +5,7 @@ const Search = require('../parser/ytmusic/Search');
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 { InnertubeError, observe } = require('../utils/Utils');
@@ -64,6 +65,16 @@ class Music {
return new Library(response, this.#actions);
}
/**
* Retrieves artist's info & content.
*
* @param {string} id
*/
async getArtist(artist_id) {
const response = await this.#actions.browse(artist_id, { client: 'YTMUSIC' });
return new Artist(response, this.#actions);
}
/**
* Retrieves song lyrics.
*

View File

@@ -9,9 +9,6 @@ class MusicCarouselShelf {
this.header = Parser.parse(data.header);
this.contents = Parser.parse(data.contents);
data.itemSize &&
(this.item_size = data.itemSize);
data.numItemsPerColumn &&
(this.num_items_per_column = data.numItemsPerColumn);
}

View File

@@ -7,8 +7,13 @@ class MusicDescriptionShelf {
constructor(data) {
this.description = new Text(data.description);
this.max_collapsed_lines = data.maxCollapsedLines || null;
this.max_expanded_lines = data.maxExpandedLines || null;
this.max_collapsed_lines &&
(this.max_collapsed_lines = data.maxCollapsedLines);
this.max_expanded_lines &&
(this.max_expanded_lines = data.maxExpandedLines);
this.footer = new Text(data.footer);
}
}

View File

@@ -0,0 +1,23 @@
'use strict';
const Text = require('./Text');
const Parser = require('..');
class MusicImmersiveHeader {
type = 'MusicImmersiveHeader';
constructor(data) {
this.title = new Text(data.title);
this.description = new Text(data.description);
this.thumbnails = Parser.parse(data.thumbnail);
/**
Not useful for now.
this.menu = Parser.parse(data.menu);
this.play_button = Parser.parse(data.playButton);
this.start_radio_button = Parser.parse(data.startRadioButton);
*/
}
}
module.exports = MusicImmersiveHeader;

View File

@@ -0,0 +1,22 @@
'use strict';
const Parser = require('..');
class MusicPlaylistShelf {
type = 'MusicPlaylistShelf';
#continuations;
constructor(data) {
this.playlist_id = data.playlistId;
this.contents = Parser.parse(data.contents);
this.collapsed_item_count = data.collapsedItemCount;
this.#continuations = data.continuations;
}
get continuation() {
return this.#continuations?.[0]?.nextContinuationData;
}
}
module.exports = MusicPlaylistShelf;

View File

@@ -111,7 +111,7 @@ class MusicResponsiveListItem {
#parseArtist() {
this.id = this.endpoint.browse.id;
this.name = this.#flex_columns[0].title.toString();
this.subscribers = this.#flex_columns[1].title.runs[2].text;
this.subscribers = this.#flex_columns[1].title.runs[2]?.text || '';
}
#parseAlbum() {

View File

@@ -8,13 +8,18 @@ class MusicShelf {
type = 'MusicShelf';
constructor(data) {
this.title = new Text(data.title);
this.title = new Text(data.title).toString();
this.contents = Parser.parse(data.contents);
this.endpoint = data.bottomEndpoint && new NavigationEndpoint(data.bottomEndpoint) || null;
this.continuation = data.continuations?.[0].nextContinuationData.continuation || null;
this.bottom_text = data.bottomText && new Text(data.bottomText) || null;
data.bottomEndpoint &&
(this.endpoint = new NavigationEndpoint(data.bottomEndpoint));
this.continuation &&
(this.continuation = data.continuations?.[0].nextContinuationData.continuation);
data.bottomText &&
(this.bottom_text = new Text(data.bottomText));
}
}

View File

@@ -0,0 +1,13 @@
'use strict';
const Thumbnail = require('./Thumbnail');
class MusicThumbnail {
type = 'MusicThumbnail';
constructor(data) {
return Thumbnail.fromResponse(data.thumbnail);
}
}
module.exports = MusicThumbnail;

View File

@@ -16,7 +16,7 @@ class ToggleButton {
this.icon_type = data.defaultIcon.iconType;
const acc_label =
data.defaultText?.accessibility.accessibilityData.label ||
data.defaultText?.accessibility?.accessibilityData.label ||
data?.accessibility?.label;
this.icon_type == 'LIKE' &&

View File

@@ -0,0 +1,39 @@
'use strict';
const Parser = require('../contents');
const { observe } = require('../../utils/Utils');
/** @namespace */
class Artist {
#page;
#actions;
/**
* @param {object} response - API response.
* @param {import('../../core/Actions')} actions
*/
constructor(response, actions) {
this.#page = Parser.parseResponse(response.data);
this.#actions = actions;
this.header = this.page.header;
const music_shelf = this.#page.contents_memo.get('MusicShelf');
const music_carousel_shelf = this.#page.contents_memo.get('MusicCarouselShelf');
/** @type {import('../contents/classes/MusicShelf')[] | import('../contents/classes/MusicCarouselShelf')[]} */
this.sections = observe([ ...music_shelf, ...music_carousel_shelf ]);
}
async getAllSongs() {
const shelf = this.sections.get({ type: 'MusicShelf' });
const page = await shelf.endpoint.call(this.#actions, 'YTMUSIC');
return page.contents_memo.get('MusicPlaylistShelf')?.[0] || [];
}
get page() {
return this.#page;
}
}
module.exports = Artist;

View File

@@ -54,7 +54,7 @@ class Search {
/** @type {{ title: string; items: object[]; getMore: Promise.<Search>; }} */
this.sections = observe(shelves.map((shelf) => ({
title: shelf.title.toString(),
title: shelf.title,
contents: shelf.contents,
getMore: () => this.#getMore(shelf)
})));
@@ -62,7 +62,7 @@ class Search {
async #getMore(shelf) {
if (!shelf.endpoint)
throw new InnertubeError(shelf.title.toString() + ' doesn\'t have more items');
throw new InnertubeError(shelf.title + ' doesn\'t have more items');
const response = await shelf.endpoint.call(this.#actions, 'YTMUSIC');
return new Search(response, this.#actions, { is_continuation: true });
@@ -86,12 +86,12 @@ class Search {
return this;
}
/**
* Applies given filter to the search.
*
* @param {string} name
* @returns {Promise.<Search>}
*/
/**
* Applies given filter to the search.
*
* @param {string} name
* @returns {Promise.<Search>}
*/
async selectFilter(name) {
if (!this.filters.includes(name))
throw new InnertubeError('Invalid filter', { available_filters: this.filters });
@@ -139,7 +139,6 @@ class Search {
return this.sections.get({ title: 'Community playlists' });
}
/** @type {import('../contents/classes/MusicResponsiveListItem')[]} */
get page() {
return this.#page;
}

View File

@@ -119,7 +119,13 @@ function deepCompare(obj1, obj2) {
const keys = Reflect.ownKeys(obj1);
return keys.some((key) => {
return obj1[key] === (obj2[key].constructor.name === 'Text' ? obj2[key].toString() : obj2[key]);
const is_text = obj2[key]?.constructor.name === 'Text';
if (!is_text && typeof obj2[key] === 'object') {
return JSON.stringify(obj1[key]) === JSON.stringify(obj2[key]);
}
return obj1[key] === (is_text ? obj2[key].toString() : obj2[key]);
});
}

View File

@@ -68,7 +68,7 @@ describe('YouTube.js Tests', () => {
describe('General', () => {
it('Should retrieve home feed', async () => {
const homefeed = await this.session.getHomeFeed();
expect(homefeed.videos.length).toBeLessThanOrEqual(30);
expect(homefeed.videos.length).toBeLessThanOrEqual(40);
});
it('Should retrieve trending content', async () => {