mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-17 19:42:14 +00:00
241 lines
7.3 KiB
TypeScript
241 lines
7.3 KiB
TypeScript
import Session from './Session';
|
|
|
|
import TrackInfo from '../parser/ytmusic/TrackInfo';
|
|
|
|
import Search from '../parser/ytmusic/Search';
|
|
import HomeFeed from '../parser/ytmusic/HomeFeed';
|
|
import Explore from '../parser/ytmusic/Explore';
|
|
import Library from '../parser/ytmusic/Library';
|
|
import Artist from '../parser/ytmusic/Artist';
|
|
import Album from '../parser/ytmusic/Album';
|
|
import Playlist from '../parser/ytmusic/Playlist';
|
|
|
|
import Parser from '../parser/index';
|
|
import { observe, YTNode } from '../parser/helpers';
|
|
|
|
import MusicQueue from '../parser/classes/MusicQueue';
|
|
import PlaylistPanel from '../parser/classes/PlaylistPanel';
|
|
import Message from '../parser/classes/Message';
|
|
import MusicDescriptionShelf from '../parser/classes/MusicDescriptionShelf';
|
|
import MusicCarouselShelf from '../parser/classes/MusicCarouselShelf';
|
|
import SearchSuggestionsSection from '../parser/classes/SearchSuggestionsSection';
|
|
|
|
import Tab from '../parser/classes/Tab';
|
|
import Tabbed from '../parser/classes/Tabbed';
|
|
import SingleColumnMusicWatchNextResults from '../parser/classes/SingleColumnMusicWatchNextResults';
|
|
import WatchNextTabbedResults from '../parser/classes/WatchNextTabbedResults';
|
|
import SectionList from '../parser/classes/SectionList';
|
|
|
|
import { InnertubeError, throwIfMissing } from '../utils/Utils';
|
|
|
|
class Music {
|
|
#actions;
|
|
|
|
constructor(session: Session) {
|
|
this.#actions = session.actions;
|
|
}
|
|
|
|
/**
|
|
* Retrieves track info.
|
|
*/
|
|
async getInfo(video_id: string) {
|
|
const initial_info = this.#actions.execute('/player', { client: 'YTMUSIC', videoId: video_id });
|
|
const continuation = this.#actions.execute('/next', { client: 'YTMUSIC', videoId: video_id });
|
|
|
|
const response = await Promise.all([ initial_info, continuation ]);
|
|
return new TrackInfo(response, this.#actions);
|
|
}
|
|
|
|
/**
|
|
* Searches on YouTube Music.
|
|
*/
|
|
async search(query: string, filters: {
|
|
type?: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist';
|
|
} = {}) {
|
|
throwIfMissing({ query });
|
|
const response = await this.#actions.search({ query, filters, client: 'YTMUSIC' });
|
|
return new Search(response, this.#actions, { is_filtered: Reflect.has(filters, 'type') && filters.type !== 'all' });
|
|
}
|
|
|
|
/**
|
|
* Retrieves the home feed.
|
|
*/
|
|
async getHomeFeed() {
|
|
const response = await this.#actions.browse('FEmusic_home', { client: 'YTMUSIC' });
|
|
return new HomeFeed(response, this.#actions);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Explore feed.
|
|
*/
|
|
async getExplore() {
|
|
const response = await this.#actions.browse('FEmusic_explore', { client: 'YTMUSIC' });
|
|
return new Explore(response);
|
|
// TODO: return new Explore(response, this.#actions);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Library.
|
|
*/
|
|
async getLibrary() {
|
|
const response = await this.#actions.browse('FEmusic_liked_albums', { client: 'YTMUSIC' });
|
|
return new Library(response);
|
|
// TODO: return new Library(response, this.#actions);
|
|
}
|
|
|
|
/**
|
|
* Retrieves artist's info & content.
|
|
*/
|
|
async getArtist(artist_id: string) {
|
|
throwIfMissing({ artist_id });
|
|
|
|
if (!artist_id.startsWith('UC'))
|
|
throw new InnertubeError('Invalid artist id', artist_id);
|
|
|
|
const response = await this.#actions.browse(artist_id, { client: 'YTMUSIC' });
|
|
return new Artist(response, this.#actions);
|
|
}
|
|
|
|
/**
|
|
* Retrieves album.
|
|
*/
|
|
async getAlbum(album_id: string) {
|
|
throwIfMissing({ album_id });
|
|
|
|
if (!album_id.startsWith('MPR'))
|
|
throw new InnertubeError('Invalid album id', album_id);
|
|
|
|
const response = await this.#actions.browse(album_id, { client: 'YTMUSIC' });
|
|
return new Album(response, this.#actions);
|
|
}
|
|
|
|
/**
|
|
* Retrieves playlist.
|
|
*/
|
|
async getPlaylist(playlist_id: string) {
|
|
throwIfMissing({ playlist_id });
|
|
|
|
const response = await this.#actions.browse(`VL${playlist_id.replace(/VL/g, '')}`, { client: 'YTMUSIC' });
|
|
return new Playlist(response, this.#actions);
|
|
}
|
|
|
|
/**
|
|
* Retrieves song lyrics.
|
|
*/
|
|
async getLyrics(video_id: string) {
|
|
throwIfMissing({ video_id });
|
|
|
|
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
|
|
|
const data = Parser.parseResponse(response.data);
|
|
|
|
const tabs = data.contents.item()
|
|
.as(SingleColumnMusicWatchNextResults).contents.item()
|
|
.as(Tabbed).contents.item()
|
|
.as(WatchNextTabbedResults)
|
|
.tabs.array().as(Tab);
|
|
|
|
const tab = tabs.get({ title: 'Lyrics' });
|
|
|
|
if (!tab)
|
|
throw new InnertubeError('Could not find target tab.');
|
|
|
|
const page = await tab.endpoint.call(this.#actions, 'YTMUSIC', true);
|
|
|
|
if (!page)
|
|
throw new InnertubeError('Could not retrieve tab contents, the given id may be invalid or is not a song.');
|
|
|
|
if (page.contents.item().key('type').string() === 'Message')
|
|
throw new InnertubeError(page.contents.item().as(Message).text, video_id);
|
|
|
|
const section_list = page.contents.item().as(SectionList).contents.array();
|
|
const description_shelf = section_list.firstOfType(MusicDescriptionShelf);
|
|
|
|
return {
|
|
text: description_shelf?.description.toString(),
|
|
footer: description_shelf?.footer
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieves up next.
|
|
*/
|
|
async getUpNext(video_id: string) {
|
|
throwIfMissing({ video_id });
|
|
|
|
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
|
|
|
const data = Parser.parseResponse(response.data);
|
|
|
|
const tabs = data.contents.item()
|
|
.as(SingleColumnMusicWatchNextResults).contents.item()
|
|
.as(Tabbed).contents.item()
|
|
.as(WatchNextTabbedResults)
|
|
.tabs.array().as(Tab);
|
|
|
|
const tab = tabs.get({ title: 'Up next' });
|
|
|
|
if (!tab)
|
|
throw new InnertubeError('Could not find target tab.');
|
|
|
|
const music_queue = tab.content?.as(MusicQueue);
|
|
|
|
if (!music_queue || !music_queue.content)
|
|
throw new InnertubeError('Music queue was empty, the given id is probably invalid.', music_queue);
|
|
|
|
const playlist_panel = music_queue.content.item().as(PlaylistPanel);
|
|
|
|
return playlist_panel;
|
|
}
|
|
|
|
/**
|
|
* Retrieves related content.
|
|
*/
|
|
async getRelated(video_id: string) {
|
|
throwIfMissing({ video_id });
|
|
|
|
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
|
|
|
const data = Parser.parseResponse(response.data);
|
|
|
|
const tabs = data.contents.item()
|
|
.as(SingleColumnMusicWatchNextResults).contents.item()
|
|
.as(Tabbed).contents.item()
|
|
.as(WatchNextTabbedResults)
|
|
.tabs.array().as(Tab);
|
|
|
|
const tab = tabs.get({ title: 'Related' });
|
|
|
|
if (!tab)
|
|
throw new InnertubeError('Could not find target tab.');
|
|
|
|
const page = await tab.endpoint.call(this.#actions, 'YTMUSIC', true);
|
|
|
|
if (!page)
|
|
throw new InnertubeError('Could not retrieve tab contents, the given id may be invalid or is not a song.');
|
|
|
|
const shelves = page.contents.item().as(SectionList).contents.array().as(MusicCarouselShelf, MusicDescriptionShelf);
|
|
|
|
return shelves;
|
|
}
|
|
|
|
/**
|
|
* Retrieves search suggestions for the given query.
|
|
*/
|
|
async getSearchSuggestions(query: string) {
|
|
const response = await this.#actions.execute('/music/get_search_suggestions', {
|
|
parse: true,
|
|
input: query,
|
|
client: 'YTMUSIC'
|
|
});
|
|
|
|
const search_suggestions_section = response.contents_memo.getType(SearchSuggestionsSection)?.[0];
|
|
|
|
if (!search_suggestions_section.contents.is_array)
|
|
return observe([] as YTNode[]);
|
|
|
|
return search_suggestions_section?.contents.array();
|
|
}
|
|
}
|
|
|
|
export default Music; |