mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-23 23:09:28 +00:00
feat(ytmusic): add support for artists
Available through `Innertube#music.getArtist(id: string)` #78
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
23
lib/parser/contents/classes/MusicImmersiveHeader.js
Normal file
23
lib/parser/contents/classes/MusicImmersiveHeader.js
Normal 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;
|
||||
22
lib/parser/contents/classes/MusicPlaylistShelf.js
Normal file
22
lib/parser/contents/classes/MusicPlaylistShelf.js
Normal 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;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
lib/parser/contents/classes/MusicThumbnail.js
Normal file
13
lib/parser/contents/classes/MusicThumbnail.js
Normal 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;
|
||||
@@ -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' &&
|
||||
|
||||
39
lib/parser/ytmusic/Artist.js
Normal file
39
lib/parser/ytmusic/Artist.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user