"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowsingMixin = void 0;
const utf8 = __importStar(require("utf8"));
const constants_1 = require("../constants");
const index_1 = require("../parsers/index");
const helpers = __importStar(require("../helpers"));
const pyLibraryMock_1 = require("../pyLibraryMock");
const albums_1 = require("../parsers/albums");
const browsing_1 = require("../parsers/browsing");
const library_1 = require("../parsers/library");
const playlists_1 = require("../parsers/playlists");
const searchParams_1 = require("../parsers/searchParams");
const utils_1 = require("../parsers/utils");
/**
* @module Browsing
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const BrowsingMixin = (Base) => {
return class Browsing extends Base {
/**
* Get the home page.
* The home page is structured as titled rows, returning 3 rows of music suggestions at a time.
* Content varies and may contain artist, album, song or playlist suggestions, sometimes mixed within the same row
*
* @param {number} [limit=3] Number of rows to return
* @returns List of objects keyed with 'title' text and 'contents' array
* @example
* [
* {
* "title": "Your morning music",
* "contents": [
* { //album result
* "title": "Sentiment",
* "year": "Said The Sky",
* "browseId": "MPREb_QtqXtd2xZMR",
* "thumbnails": [...]
* },
* { //playlist result
* "title": "r/EDM top submissions 01/28/2022",
* "playlistId": "PLz7-xrYmULdSLRZGk-6GKUtaBZcgQNwel",
* "thumbnails": [...],
* "description": "redditEDM • 161 songs",
* "count": "161",
* "author": [
* {
* "name": "redditEDM",
* "id": "UCaTrZ9tPiIGHrkCe5bxOGwA"
* }
* ]
* }
* ]
* },
* {
* "title": "Your favorites",
* "contents": [
* { //artist result
* "title": "Chill Satellite",
* "browseId": "UCrPLFBWdOroD57bkqPbZJog",
* "subscribers": "374",
* "thumbnails": [...]
* }
* { //album result
* "title": "Dragon",
* "year": "Two Steps From Hell",
* "browseId": "MPREb_M9aDqLRbSeg",
* "thumbnails": [...]
* }
* ]
* },
* {
* "title": "Quick picks",
* "contents": [
* { //song quick pick
* "title": "Gravity",
* "videoId": "EludZd6lfts",
* "artists": [{
* "name": "yetep",
* "id": "UCSW0r7dClqCoCvQeqXiZBlg"
* }],
* "thumbnails": [...],
* "album": {
* "title": "Gravity",
* "browseId": "MPREb_D6bICFcuuRY"
* }
* },
* { //video quick pick
* "title": "Gryffin & Illenium (feat. Daya) - Feel Good (L3V3LS Remix)",
* "videoId": "bR5l0hJDnX8",
* "artists": [
* {
* "name": "L3V3LS",
* "id": "UCCVNihbOdkOWw_-ajIYhAbQ"
* }
* ],
* "thumbnails": [...],
* "views": "10M"
* }
* ]
* }
* ]
*/
async getHome(limit = 3) {
const endpoint = 'browse';
const body = { browseId: 'FEmusic_home' };
const response = await this._sendRequest(endpoint, body);
const results = (0, utils_1.nav)(response, [...index_1.SINGLE_COLUMN_TAB, ...index_1.SECTION_LIST]);
let home = [...this._parser.parseHome(results)];
const sectionList = (0, utils_1.nav)(response, [
...index_1.SINGLE_COLUMN_TAB,
'sectionListRenderer',
]);
if ('continuations' in sectionList) {
const requestFunc = async (additionalParams) => await this._sendRequest(endpoint, body, additionalParams);
const parseFunc = (contents) => this._parser.parseHome(contents);
home = [
...home,
...(await (0, utils_1.getContinuations)(sectionList, 'sectionListContinuation', limit - home.length, requestFunc, parseFunc)),
];
}
return home;
}
async search(query, options) {
const _options = options ?? {};
let { filter } = _options;
const { scope, limit = 20, ignoreSpelling } = _options;
const body = { query: query };
const endpoint = 'search';
let searchResults = [];
const filters = [
'albums',
'artists',
'playlists',
'community_playlists',
'featured_playlists',
'songs',
'videos',
];
if (filter && !filters.includes(filter)) {
throw new Error(`Invalid filter provided. Please use one of the following filters or leave out the parameter: ${filters.join(', ')}`);
}
const scopes = ['library', 'uploads'];
if (scope && !scopes.includes(scope)) {
throw new Error(`Invalid scope provided. Please use one of the following scopes or leave out the parameter: ${scopes.join(', ')}`);
}
const params = (0, searchParams_1.getSearchParams)(filter, scope, ignoreSpelling);
if (params) {
body['params'] = params;
}
const response = await this._sendRequest(endpoint, body);
// no results
if (!response['contents']) {
return searchResults;
}
let results;
if ('tabbedSearchResultsRenderer' in response.contents) {
//0 if not scope or filter else scopes.index(scope) + 1
const tab_index = !scope || filter ? 0 : scopes.indexOf(scope) + 1;
results =
response['contents']['tabbedSearchResultsRenderer']['tabs'][tab_index]['tabRenderer']['content'];
}
else {
results = response['contents'];
}
const resultsNav = (0, utils_1.nav)(results, index_1.SECTION_LIST);
// no results
if (!resultsNav ||
(resultsNav.length == 1 && 'itemSectionRenderer' in resultsNav)) {
return searchResults;
}
//set filter for parser
if (filter && filter.split('_').includes('playlists')) {
filter = 'playlists';
}
else if (scope == 'uploads') {
filter = 'uploads';
}
for (const res of resultsNav) {
if ('musicShelfRenderer' in res) {
const resultsMusicShelfContents = res['musicShelfRenderer']['contents'];
const original_filter = filter;
const category = (0, utils_1.nav)(res, [...index_1.MUSIC_SHELF, ...index_1.TITLE_TEXT], true);
if (!filter && scope == scopes[0]) {
filter = category;
}
const type = filter
? filter.slice(undefined, -1).toLowerCase()
: null;
searchResults = [
...searchResults,
...this._parser.parseSearchResults(resultsMusicShelfContents, type, category),
];
filter = original_filter;
if ('continuations' in res['musicShelfRenderer']) {
const requestFunc = async (additionalParams) => await this._sendRequest(endpoint, body, additionalParams);
const parseFunc = (contents) => this._parser.parseSearchResults(contents, type, category);
searchResults = [
...searchResults,
...(await (0, utils_1.getContinuations)(res['musicShelfRenderer'], 'musicShelfContinuation', limit - searchResults.length, requestFunc, parseFunc)),
];
}
}
}
return searchResults;
}
/**
* Get information about an artist and their top releases (songs,
* albums, singles, videos, and related artists). The top lists
* contain pointers for getting the full list of releases. For
* songs/videos, pass the browseId to {@link https://codyduong.github.io/ytmusicapiJS/module-Playlists.html#getPlaylist | getPlaylist}.
* For albums/singles, pass browseId and params to {@link https://codyduong.github.io/ytmusicapiJS/module-Browsing.html#getArtistAlbums | getArtistAlbums}.
*
* @param {string} channelId channel id of the artist
* @return Object with requested information.
* @example
* {
* "description": "Oasis were ...",
* "views": "1838795605",
* "name": "Oasis",
* "channelId": "UCUDVBtnOQi4c7E8jebpjc9Q",
* "subscribers": "2.3M",
* "subscribed": false,
* "thumbnails": [...],
* "songs": {
* "browseId": "VLPLMpM3Z0118S42R1npOhcjoakLIv1aqnS1",
* "results": [
* {
* "videoId": "ZrOKjDZOtkA",
* "title": "Wonderwall (Remastered)",
* "thumbnails": [...],
* "artist": "Oasis",
* "album": "(What's The Story) Morning Glory? (Remastered)"
* }
* ]
* },
* "albums": {
* "results": [
* {
* "title": "Familiar To Millions",
* "thumbnails": [...],
* "year": "2018",
* "browseId": "MPREb_AYetWMZunqA"
* }
* ],
* "browseId": "UCmMUZbaYdNH0bEd1PAlAqsA",
* "params": "6gPTAUNwc0JDbndLYlFBQV..."
* },
* "singles": {
* "results": [
* {
* "title": "Stand By Me (Mustique Demo)",
* "thumbnails": [...],
* "year": "2016",
* "browseId": "MPREb_7MPKLhibN5G"
* }
* ],
* "browseId": "UCmMUZbaYdNH0bEd1PAlAqsA",
* "params": "6gPTAUNwc0JDbndLYlFBQV..."
* },
* "videos": {
* "results": [
* {
* "title": "Wonderwall",
* "thumbnails": [...],
* "views": "358M",
* "videoId": "bx1Bh8ZvH84",
* "playlistId": "PLMpM3Z0118S5xuNckw1HUcj1D021AnMEB"
* }
* ],
* "browseId": "VLPLMpM3Z0118S5xuNckw1HUcj1D021AnMEB"
* },
* "related": {
* "results": [
* {
* "browseId": "UCt2KxZpY5D__kapeQ8cauQw",
* "subscribers": "450K",
* "title": "The Verve"
* },
* {
* "browseId": "UCwK2Grm574W1u-sBzLikldQ",
* "subscribers": "341K",
* "title": "Liam Gallagher"
* },
* ...
* ]
* }
* }
*/
async getArtist(channelId) {
if (channelId.startsWith('MPLA')) {
channelId = channelId.slice(4);
}
const body = { browseId: channelId };
const endpoint = 'browse';
const response = await this._sendRequest(endpoint, body);
const results = (0, utils_1.nav)(response, [
...index_1.SINGLE_COLUMN_TAB,
...index_1.SECTION_LIST,
]);
if (results.length == 1) {
// not a YouTube Music Channel, a standard YouTube Channel ID with no music content was given
throw new ReferenceError(`The YouTube Channel ${channelId} has no music content.`);
}
let artist = {
description: null,
views: null,
};
const header = response['header']['musicImmersiveHeaderRenderer'];
artist['name'] = (0, utils_1.nav)(header, index_1.TITLE_TEXT);
const descriptionShelf = (0, utils_1.findObjectByKey)(results, 'musicDescriptionShelfRenderer', undefined, true);
if (descriptionShelf) {
artist['description'] = (0, utils_1.nav)(descriptionShelf, index_1.DESCRIPTION);
artist['views'] = !('subheader' in descriptionShelf)
? null
: descriptionShelf['subheader']['runs'][0]['text'];
}
const subscriptionButton = header['subscriptionButton']['subscribeButtonRenderer'];
artist['channelId'] = subscriptionButton['channelId'];
artist['shuffleId'] = (0, utils_1.nav)(header, ['playButton', 'buttonRenderer', ...index_1.NAVIGATION_WATCH_PLAYLIST_ID], true);
artist['radioId'] = (0, utils_1.nav)(header, ['startRadioButton', 'buttonRenderer', ...index_1.NAVIGATION_WATCH_PLAYLIST_ID], true);
artist['subscribers'] = (0, utils_1.nav)(subscriptionButton, ['subscriberCountText', 'runs', 0, 'text'], true);
artist['subscribed'] = subscriptionButton['subscribed'];
artist['thumbnails'] = (0, utils_1.nav)(header, index_1.THUMBNAILS, true);
artist['songs'] = { browseId: null };
if ('musicShelfRenderer' in results[0]) {
// API sometimes does not return songs
const musicShelf = (0, utils_1.nav)(results[0], index_1.MUSIC_SHELF);
if ('navigationEndpoint' in
(0, utils_1.nav)(musicShelf, index_1.TITLE)) {
artist['songs']['browseId'] = (0, utils_1.nav)(musicShelf, [
...index_1.TITLE,
...index_1.NAVIGATION_BROWSE_ID,
]);
}
//@ts-expect-error: We're overriding the shape here
artist['songs']['results'] = (0, playlists_1.parsePlaylistItems)(musicShelf['contents']);
}
artist = { ...artist, ...this._parser.parseArtistContents(results) };
return artist;
}
/**
* Get the full list of an artist's albums or singles
* @param {string} channelId channel Id of the artist
* @param {string} params params obtained by {@link https://codyduong.github.io/ytmusicapiJS/module-Browsing.html#getArtist | getArtist}
* @returns List of albums in the format of {@link https://codyduong.github.io/ytmusicapiJS/module-Library.html#getLibraryAlbums | getLibraryAlbums}, except artists key is missing.
*/
async getArtistAlbums(channelId, params) {
const body = { browseId: channelId, params: params };
const endpoint = 'browse';
const response = await this._sendRequest(endpoint, body);
const results = (0, utils_1.nav)(response, [
...index_1.SINGLE_COLUMN_TAB,
...index_1.SECTION_LIST_ITEM,
...index_1.GRID_ITEMS,
]);
const albums = (0, library_1.parseAlbums)(results);
return albums;
}
/**
* Retrieve a user's page. A user may own videos or playlists.
* @param {string} channelId channelId of the user
* @returns Object with information about a user.
* @example
* {
* "name": "4Tune – No Copyright Music",
* "videos": {
* "browseId": "UC44hbeRoCZVVMVg5z0FfIww",
* "results": [
* {
* "title": "Epic Music Soundtracks 2019",
* "videoId": "bJonJjgS2mM",
* "playlistId": "RDAMVMbJonJjgS2mM",
* "thumbnails": [
* {
* "url": "https://i.ytimg.com/vi/bJon...",
* "width": 800,
* "height": 450
* }
* ],
* "views": "19K"
* }
* ]
* },
* "playlists": {
* "browseId": "UC44hbeRoCZVVMVg5z0FfIww",
* "results": [
* {
* "title": "♚ Machinimasound | Playlist",
* "playlistId": "PLRm766YvPiO9ZqkBuEzSTt6Bk4eWIr3gB",
* "thumbnails": [
* {
* "url": "https://i.ytimg.com/vi/...",
* "width": 400,
* "height": 225
* }
* ]
* }
* ],
* "params": "6gO3AUNvWU..."
* }
* }
*/
async getUser(channelId) {
const endpoint = 'browse';
const body = { browseId: channelId };
const response = await this._sendRequest(endpoint, body);
const results = (0, utils_1.nav)(response, [...index_1.SINGLE_COLUMN_TAB, ...index_1.SECTION_LIST]);
const user = {
name: (0, utils_1.nav)(response, [
'header',
'musicVisualHeaderRenderer',
...index_1.TITLE_TEXT,
]),
...this._parser.parseArtistContents(results),
};
return user;
}
/**
* Retrieve a list of playlists for a given user.
* Call this function again with the returned ``params`` to get the full list.
* @param {string} [channelId] channelId of the user.
* @param {string} [params] params obtained by `getArtist`
* @returns List of user playlists in the format of `getLibraryPlaylists`
*/
async getUserPlaylists(channelId, params) {
const endpoint = 'browse';
const body = { browseId: channelId, params: params };
const response = await this._sendRequest(endpoint, body);
const results = (0, utils_1.nav)(response, [
...index_1.SINGLE_COLUMN_TAB,
...index_1.SECTION_LIST_ITEM,
...index_1.GRID_ITEMS,
]);
const userPlaylists = (0, browsing_1.parseContentList)(results, browsing_1.parsePlaylist);
return userPlaylists;
}
/**
* Get an album's browseId based on its audioPlaylistId
* @param {string} [audioPlaylistId] id of the audio playlist (starting with `OLAK5uy_`)
* @returns browseId (starting with `MPREb_`)
*/
async getAlbumBrowseId(audioPlaylistId) {
const params = { list: audioPlaylistId };
const response = await this._sendGetRequest(constants_1.YTM_DOMAIN + '/playlist', params);
const matches = pyLibraryMock_1.re.findall(/"MPRE.+?"/, response);
let browse_id = null;
if (matches.length > 0) {
browse_id = utf8
.decode(utf8.encode(matches[0]))
.replace(/^"+|"+$/g, '');
}
return browse_id;
}
/**
* Get information and tracks of an album
* @param {string} browseId of the album, for example returned by {@link search}
* @returns Object with album and track metadata.
* @example
* {
* "title": "Revival",
* "type": "Album",
* "thumbnails": [],
* "description": "Revival is the...",
* "artists": [
* {
* "name": "Eminem",
* "id": "UCedvOgsKFzcK3hA5taf3KoQ"
* }
* ],
* "year": "2017",
* "trackCount": 19,
* "duration": "1 hour, 17 minutes",
* "duration_seconds": 4657,
* "audioPlaylistId": "OLAK5uy_nMr9h2VlS-2PULNz3M3XVXQj_P3C2bqaY",
* "tracks": [
* {
* "videoId": "iKLU7z_xdYQ",
* "title": "Walk On Water (feat. Beyoncé)",
* "artists": [
* {
* "name": "Eminem",
* "id": "UCedvOgsKFzcK3hA5taf3KoQ"
* }
* ],
* "album": "Revival",
* "likeStatus": "INDIFFERENT",
* "thumbnails": null,
* "isAvailable": true,
* "isExplicit": true,
* "duration": "5:03",
* "duration_seconds": 303,
* "feedbackTokens": {
* "add": "AB9zfpK...",
* "remove": "AB9zfpK..."
* }
* }
* ],
* "duration_seconds": 4657
* }
*/
async getAlbum(browseId) {
const body = { browseId: browseId };
const endpoint = 'browse';
const response = await this._sendRequest(endpoint, body);
const results = (0, utils_1.nav)(response, [
...index_1.SINGLE_COLUMN_TAB,
...index_1.SECTION_LIST_ITEM,
...index_1.MUSIC_SHELF,
]);
const album = {
...(0, albums_1.parseAlbumHeader)(response),
//@ts-expect-error: We'll swap this out proper later.
tracks: (0, playlists_1.parsePlaylistItems)(results['contents']),
duration_seconds: undefined,
};
album['duration_seconds'] = helpers.sumTotalDuration(album);
for (const [i, _track] of album['tracks'].entries()) {
album['tracks'][i]['album'] = album['title'];
album['tracks'][i]['artists'] = album['artists'];
}
return album;
}
/**
* Returns metadata and streaming information about a song or video.
* @param {string} [videoId] Video id
* @param {number} [signatureTimestamp] Provide the current YouTube signatureTimestamp.
* If not provided a default value will be used, which might result in invalid streaming URLs
* @return Object with song metadata
* @example
* {
* "playabilityStatus": {
* "status": "OK",
* "playableInEmbed": true,
* "audioOnlyPlayability": {
* "audioOnlyPlayabilityRenderer": {
* "trackingParams": "CAEQx2kiEwiuv9X5i5H1AhWBvlUKHRoZAHk=",
* "audioOnlyAvailability": "FEATURE_AVAILABILITY_ALLOWED"
* }
* },
* "miniplayer": {
* "miniplayerRenderer": {
* "playbackMode": "PLAYBACK_MODE_ALLOW"
* }
* },
* "contextParams": "Q0FBU0FnZ0M="
* },
* "streamingData": {
* "expiresInSeconds": "21540",
* "adaptiveFormats": [
* {
* "itag": 140,
* "url": "https://rr1---sn-h0jelnez.c.youtube.com/videoplayback?expire=1641080272...",
* "mimeType": "audio/mp4; codecs=\"mp4a.40.2\"",
* "bitrate": 131007,
* "initRange": {
* "start": "0",
* "end": "667"
* },
* "indexRange": {
* "start": "668",
* "end": "999"
* },
* "lastModified": "1620321966927796",
* "contentLength": "3967382",
* "quality": "tiny",
* "projectionType": "RECTANGULAR",
* "averageBitrate": 129547,
* "highReplication": true,
* "audioQuality": "AUDIO_QUALITY_MEDIUM",
* "approxDurationMs": "245000",
* "audioSampleRate": "44100",
* "audioChannels": 2,
* "loudnessDb": -1.3000002
* }
* ]
* },
* "videoDetails": {
* "videoId": "AjXQiKP5kMs",
* "title": "Sparks",
* "lengthSeconds": "245",
* "channelId": "UCvCk2zFqkCYzpnSgWfx0qOg",
* "isOwnerViewing": false,
* "isCrawlable": false,
* "thumbnail": {
* "thumbnails": []
* },
* "allowRatings": true,
* "viewCount": "12",
* "author": "Thomas Bergersen",
* "isPrivate": true,
* "isUnpluggedCorpus": false,
* "musicVideoType": "MUSIC_VIDEO_TYPE_PRIVATELY_OWNED_TRACK",
* "isLiveContent": false
* },
* "microformat": {
* "microformatDataRenderer": {
* "urlCanonical": "https://music.youtube.com/watch?v=AjXQiKP5kMs",
* "title": "Sparks - YouTube Music",
* "description": "Uploaded to YouTube via YouTube Music Sparks",
* "thumbnail": {
* "thumbnails": [
* {
* "url": "https://i.ytimg.com/vi/AjXQiKP5kMs/hqdefault.jpg",
* "width": 480,
* "height": 360
* }
* ]
* },
* "siteName": "YouTube Music",
* "appName": "YouTube Music",
* "androidPackage": "com.google.android.apps.youtube.music",
* "iosAppStoreId": "1017492454",
* "iosAppArguments": "https://music.youtube.com/watch?v=AjXQiKP5kMs",
* "ogType": "video.other",
* "urlApplinksIos": "vnd.youtube.music://music.youtube.com/watch?v=AjXQiKP5kMs&feature=applinks",
* "urlApplinksAndroid": "vnd.youtube.music://music.youtube.com/watch?v=AjXQiKP5kMs&feature=applinks",
* "urlTwitterIos": "vnd.youtube.music://music.youtube.com/watch?v=AjXQiKP5kMs&feature=twitter-deep-link",
* "urlTwitterAndroid": "vnd.youtube.music://music.youtube.com/watch?v=AjXQiKP5kMs&feature=twitter-deep-link",
* "twitterCardType": "player",
* "twitterSiteHandle": "@YouTubeMusic",
* "schemaDotOrgType": "http://schema.org/VideoObject",
* "noindex": true,
* "unlisted": true,
* "paid": false,
* "familySafe": true,
* "pageOwnerDetails": {
* "name": "Music Library Uploads",
* "externalChannelId": "UCvCk2zFqkCYzpnSgWfx0qOg",
* "youtubeProfileUrl": "http://www.youtube.com/channel/UCvCk2zFqkCYzpnSgWfx0qOg"
* },
* "videoDetails": {
* "externalVideoId": "AjXQiKP5kMs",
* "durationSeconds": "246",
* "durationIso8601": "PT4M6S"
* },
* "linkAlternates": [
* {
* "hrefUrl": "android-app://com.google.android.youtube/http/youtube.com/watch?v=AjXQiKP5kMs"
* },
* {
* "hrefUrl": "ios-app://544007664/http/youtube.com/watch?v=AjXQiKP5kMs"
* },
* {
* "hrefUrl": "https://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fmusic.youtube.com%2Fwatch%3Fv%3DAjXQiKP5kMs",
* "title": "Sparks",
* "alternateType": "application/json+oembed"
* },
* {
* "hrefUrl": "https://www.youtube.com/oembed?format=xml&url=https%3A%2F%2Fmusic.youtube.com%2Fwatch%3Fv%3DAjXQiKP5kMs",
* "title": "Sparks",
* "alternateType": "text/xml+oembed"
* }
* ],
* "viewCount": "12",
* "publishDate": "1969-12-31",
* "category": "Music",
* "uploadDate": "1969-12-31"
* }
* }
* }
*/
async getSong(videoId, signatureTimestamp) {
const endpoint = 'player';
if (!signatureTimestamp) {
signatureTimestamp = helpers.getDatestamp() - 1;
}
const params = {
playbackContext: {
contentPlaybackContext: {
signatureTimestamp: signatureTimestamp,
},
},
video_id: videoId,
};
const response = await this._sendRequest(endpoint, params);
const keys = [
'videoDetails',
'playabilityStatus',
'streamingData',
'microformat',
];
for (const k of Object.keys(response)) {
if (!keys.includes(k)) {
delete response[k];
}
}
return response;
}
/**
* Returns lyrics of a song or video.
* @param browseId
* @return Object with song lyrics
* @example
* {
* "lyrics": "Today is gonna be the day\\nThat they're gonna throw it back to you\\n",
* "source": "Source: LyricFind"
* }
*/
async getLyrics(browseId) {
const lyrics = {};
// Is this inherited behavior good for typescript users? @codyduong
if (!browseId) {
throw new Error('Invalid browseId provided. This song might not have lyrics.');
}
const response = await this._sendRequest('browse', {
browseId: browseId,
});
lyrics['lyrics'] = (0, utils_1.nav)(response, [
'contents',
...index_1.SECTION_LIST_ITEM,
'musicDescriptionShelfRenderer',
...index_1.DESCRIPTION,
], true);
lyrics['source'] = (0, utils_1.nav)(response, [
'contents',
...index_1.SECTION_LIST_ITEM,
'musicDescriptionShelfRenderer',
'footer',
...index_1.RUN_TEXT,
], true);
return lyrics;
}
/**
* Extract the URL for the `base.js` script from YouTube Music.
* @return {string} URL to `base.js`
*/
async getBaseJSUrl() {
const response = await this._sendGetRequest(constants_1.YTM_DOMAIN);
const match = pyLibraryMock_1.re.search(/jsUrl"\s*:\s*"([^"]+)"/, response);
if (!match) {
throw new Error('Could not identify the URL for base.js player.');
}
return constants_1.YTM_DOMAIN + match[0].slice(8, -1);
}
/**
* Fetch the `base.js` script from YouTube Music and parse out the `signatureTimestamp` for use with {@link https://codyduong.github.io/ytmusicapiJS/module-Browsing.html#getSong | getSong}.
* @param url Optional. Provide the URL of the `base.js` script. If this isn't specified a call will be made to {@link https://codyduong.github.io/ytmusicapiJS/module-Browsing.html#getBaseJSUrl | getBaseJSUrl}.
* @returns `signatureTimestamp` string
*/
async getSignatureTimestamp(url) {
if (!url) {
url = await this.getBaseJSUrl();
}
const response = await this._sendGetRequest(url);
const match = pyLibraryMock_1.re.search(/signatureTimestamp[:=](\d+)/, response);
if (!match) {
throw new Error('Unable to identify the signatureTimestamp.');
}
if (match && match.groups) {
return Math.round(Number(match[1]));
}
return null;
}
};
};
exports.BrowsingMixin = BrowsingMixin;