UNPKG

ytmusic-api-proxy

Version:
542 lines (541 loc) 21.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const axios_1 = __importDefault(require("axios")); const tough_cookie_1 = require("tough-cookie"); const https_proxy_agent_1 = require("https-proxy-agent"); const socks_proxy_agent_1 = require("socks-proxy-agent"); const constants_1 = require("./constants"); const AlbumParser_1 = __importDefault(require("./parsers/AlbumParser")); const ArtistParser_1 = __importDefault(require("./parsers/ArtistParser")); const Parser_1 = __importDefault(require("./parsers/Parser")); const PlaylistParser_1 = __importDefault(require("./parsers/PlaylistParser")); const SearchParser_1 = __importDefault(require("./parsers/SearchParser")); const SongParser_1 = __importDefault(require("./parsers/SongParser")); const VideoParser_1 = __importDefault(require("./parsers/VideoParser")); const traverse_1 = require("./utils/traverse"); axios_1.default.defaults.headers.common["Accept-Encoding"] = "gzip"; class YTMusic { cookiejar; config; client; proxyConfig; /** * Creates an instance of YTMusic * Make sure to call initialize() * @param proxyConfig - Optional proxy configuration */ constructor(proxyConfig) { this.cookiejar = new tough_cookie_1.CookieJar(); this.config = {}; this.proxyConfig = proxyConfig; const axiosConfig = { baseURL: "https://music.youtube.com/", headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36", "Accept-Language": "en-US,en;q=0.5", }, withCredentials: true, }; // Configure proxy if provided if (this.proxyConfig) { const { protocol = "http", host, port, auth } = this.proxyConfig; let proxyUrl = `${protocol}://`; if (auth) { proxyUrl += `${auth.username}:${auth.password}@`; } proxyUrl += `${host}:${port}`; if (protocol === "socks4" || protocol === "socks5") { axiosConfig.httpsAgent = new socks_proxy_agent_1.SocksProxyAgent(proxyUrl); axiosConfig.httpAgent = new socks_proxy_agent_1.SocksProxyAgent(proxyUrl); } else { axiosConfig.httpsAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl); axiosConfig.httpAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl); } } this.client = axios_1.default.create(axiosConfig); this.client.interceptors.request.use(req => { if (req.baseURL) { const cookieString = this.cookiejar.getCookieStringSync(req.baseURL); if (cookieString) { req.headers["cookie"] = cookieString; } } return req; }); this.client.interceptors.response.use(res => { if (res.headers && res.config.baseURL) { const cookieStrings = res.headers["set-cookie"] || []; for (const cookieString of cookieStrings) { const cookie = tough_cookie_1.Cookie.parse(cookieString); if (cookie) { this.cookiejar.setCookieSync(cookie, res.config.baseURL); } } } return res; }); } /** * Initializes the API */ async initialize(options) { const { cookies, GL, HL, proxy } = options ?? {}; // Update proxy configuration if provided in initialize if (proxy && !this.proxyConfig) { this.proxyConfig = proxy; // Recreate client with proxy configuration const axiosConfig = { baseURL: "https://music.youtube.com/", headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36", "Accept-Language": "en-US,en;q=0.5", }, withCredentials: true, }; const { protocol = "http", host, port, auth } = proxy; let proxyUrl = `${protocol}://`; if (auth) { proxyUrl += `${auth.username}:${auth.password}@`; } proxyUrl += `${host}:${port}`; if (protocol === "socks4" || protocol === "socks5") { axiosConfig.httpsAgent = new socks_proxy_agent_1.SocksProxyAgent(proxyUrl); axiosConfig.httpAgent = new socks_proxy_agent_1.SocksProxyAgent(proxyUrl); } else { axiosConfig.httpsAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl); axiosConfig.httpAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl); } this.client = axios_1.default.create(axiosConfig); // Re-add interceptors this.client.interceptors.request.use(req => { if (req.baseURL) { const cookieString = this.cookiejar.getCookieStringSync(req.baseURL); if (cookieString) { req.headers["cookie"] = cookieString; } } return req; }); this.client.interceptors.response.use(res => { if (res.headers && res.config.baseURL) { const cookieStrings = res.headers["set-cookie"] || []; for (const cookieString of cookieStrings) { const cookie = tough_cookie_1.Cookie.parse(cookieString); if (cookie) { this.cookiejar.setCookieSync(cookie, res.config.baseURL); } } } return res; }); } if (cookies) { for (const cookieString of cookies.split("; ")) { const cookie = tough_cookie_1.Cookie.parse(`${cookieString}`); if (!cookie) continue; this.cookiejar.setCookieSync(cookie, "https://www.youtube.com/"); } } const html = (await this.client.get("/")).data; const setConfigs = html.match(/ytcfg\.set\(.*\)/) || []; const configs = setConfigs .map(c => c.slice(10, -1)) .map(s => { try { return JSON.parse(s); } catch { return null; } }) .filter(j => !!j); for (const config of configs) { this.config = { ...this.config, ...config, }; } if (!this.config) { this.config = {}; } if (GL) this.config.GL = GL; if (HL) this.config.HL = HL; return this; } /** * Constructs a basic YouTube Music API request with all essential headers * and body parameters needed to make the API work * * @param endpoint Endpoint for the request * @param body Body * @param query Search params * @returns Raw response from YouTube Music API which needs to be parsed */ async constructRequest(endpoint, body = {}, query = {}) { if (!this.config) { throw new Error("API not initialized. Make sure to call the initialize() method first"); } const headers = { ...this.client.defaults.headers, "x-origin": this.client.defaults.baseURL, "X-Goog-Visitor-Id": this.config.VISITOR_DATA || "", "X-YouTube-Client-Name": this.config.INNERTUBE_CONTEXT_CLIENT_NAME, "X-YouTube-Client-Version": this.config.INNERTUBE_CLIENT_VERSION, "X-YouTube-Device": this.config.DEVICE, "X-YouTube-Page-CL": this.config.PAGE_CL, "X-YouTube-Page-Label": this.config.PAGE_BUILD_LABEL, "X-YouTube-Utc-Offset": String(-new Date().getTimezoneOffset()), "X-YouTube-Time-Zone": new Intl.DateTimeFormat().resolvedOptions().timeZone, }; const searchParams = new URLSearchParams({ ...query, alt: "json", key: this.config.INNERTUBE_API_KEY, }); const res = await this.client.post(`youtubei/${this.config.INNERTUBE_API_VERSION}/${endpoint}?${searchParams.toString()}`, { context: { capabilities: {}, client: { clientName: this.config.INNERTUBE_CLIENT_NAME, clientVersion: this.config.INNERTUBE_CLIENT_VERSION, experimentIds: [], experimentsToken: "", gl: this.config.GL, hl: this.config.HL, locationInfo: { locationPermissionAuthorizationStatus: "LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED", }, musicAppInfo: { musicActivityMasterSwitch: "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE", musicLocationMasterSwitch: "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE", pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_UNKNOWN", }, utcOffsetMinutes: -new Date().getTimezoneOffset(), }, request: { internalExperimentFlags: [ { key: "force_music_enable_outertube_tastebuilder_browse", value: "true", }, { key: "force_music_enable_outertube_playlist_detail_browse", value: "true", }, { key: "force_music_enable_outertube_search_suggestions", value: "true", }, ], sessionIndex: {}, }, user: { enableSafetyMode: false, }, }, ...body, }, { responseType: "json", headers, }); return "responseContext" in res.data ? res.data : res; } /** * Get a list of search suggestiong based on the query * * @param query Query string * @returns Search suggestions */ async getSearchSuggestions(query) { return (0, traverse_1.traverseList)(await this.constructRequest("music/get_search_suggestions", { input: query, }), "query"); } /** * Searches YouTube Music API for results * * @param query Query string */ async search(query) { const searchData = await this.constructRequest("search", { query, params: null, }); return (0, traverse_1.traverseList)(searchData, "musicResponsiveListItemRenderer") .map(SearchParser_1.default.parse) .filter(Boolean); } /** * Searches YouTube Music API for songs * * @param query Query string */ async searchSongs(query) { const searchData = await this.constructRequest("search", { query, params: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D", }); return (0, traverse_1.traverseList)(searchData, "musicResponsiveListItemRenderer").map(SongParser_1.default.parseSearchResult); } /** * Searches YouTube Music API for videos * * @param query Query string */ async searchVideos(query) { const searchData = await this.constructRequest("search", { query, params: "Eg-KAQwIABABGAAgACgAMABqChAEEAMQCRAFEAo%3D", }); return (0, traverse_1.traverseList)(searchData, "musicResponsiveListItemRenderer").map(VideoParser_1.default.parseSearchResult); } /** * Searches YouTube Music API for artists * * @param query Query string */ async searchArtists(query) { const searchData = await this.constructRequest("search", { query, params: "Eg-KAQwIABAAGAAgASgAMABqChAEEAMQCRAFEAo%3D", }); return (0, traverse_1.traverseList)(searchData, "musicResponsiveListItemRenderer").map(ArtistParser_1.default.parseSearchResult); } /** * Searches YouTube Music API for albums * * @param query Query string */ async searchAlbums(query) { const searchData = await this.constructRequest("search", { query, params: "Eg-KAQwIABAAGAEgACgAMABqChAEEAMQCRAFEAo%3D", }); return (0, traverse_1.traverseList)(searchData, "musicResponsiveListItemRenderer").map(AlbumParser_1.default.parseSearchResult); } /** * Searches YouTube Music API for playlists * * @param query Query string */ async searchPlaylists(query) { const searchData = await this.constructRequest("search", { query, params: "Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D", }); return (0, traverse_1.traverseList)(searchData, "musicResponsiveListItemRenderer").map(PlaylistParser_1.default.parseSearchResult); } /** * Get all possible information of a Song * * @param videoId Video ID * @returns Song Data */ async getSong(videoId) { if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId"); const data = await this.constructRequest("player", { videoId }); const song = SongParser_1.default.parse(data); if (song.videoId !== videoId) throw new Error("Invalid videoId"); return song; } /** * Get all possible information of a Up Nexts Song * * @param videoId Video ID * @returns Up Nexts Data */ async getUpNexts(videoId) { if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId"); const data = await this.constructRequest("next", { videoId, playlistId: `RDAMVM${videoId}`, isAudioOnly: true }); const tabs = data?.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs[0]?.tabRenderer?.content?.musicQueueRenderer?.content?.playlistPanelRenderer?.contents; if (!tabs) throw new Error("Invalid response structure"); return tabs.slice(1).map((item) => { const { videoId, title, shortBylineText, lengthText, thumbnail } = item.playlistPanelVideoRenderer; return { type: "SONG", videoId, title: title?.runs[0]?.text || "Unknown", artists: shortBylineText?.runs[0]?.text || "Unknown", duration: lengthText?.runs[0]?.text || "Unknown", thumbnail: thumbnail?.thumbnails.at(-1)?.url || "Unknown", }; }); } /** * Get all possible information of a Video * * @param videoId Video ID * @returns Video Data */ async getVideo(videoId) { if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId"); const data = await this.constructRequest("player", { videoId }); const video = VideoParser_1.default.parse(data); if (video.videoId !== videoId) throw new Error("Invalid videoId"); return video; } /** * Get lyrics of a specific Song * * @param videoId Video ID * @returns Lyrics */ async getLyrics(videoId) { if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId"); const data = await this.constructRequest("next", { videoId }); const browseId = (0, traverse_1.traverse)((0, traverse_1.traverseList)(data, "tabs", "tabRenderer")[1], "browseId"); const lyricsData = await this.constructRequest("browse", { browseId }); const lyrics = (0, traverse_1.traverseString)(lyricsData, "description", "runs", "text"); return lyrics ? lyrics .replaceAll("\r", "") .split("\n") .filter((v) => !!v) : null; } /** * Get all possible information of an Artist * * @param artistId Artist ID * @returns Artist Data */ async getArtist(artistId) { const data = await this.constructRequest("browse", { browseId: artistId, }); return ArtistParser_1.default.parse(data, artistId); } /** * Get all of Artist's Songs * * @param artistId Artist ID * @returns Artist's Songs */ async getArtistSongs(artistId) { const artistData = await this.constructRequest("browse", { browseId: artistId, }); const browseToken = (0, traverse_1.traverse)(artistData, "musicShelfRenderer", "title", "browseId"); if (browseToken instanceof Array) return []; const songsData = await this.constructRequest("browse", { browseId: browseToken, }); const continueToken = (0, traverse_1.traverse)(songsData, "continuation"); const moreSongsData = await this.constructRequest("browse", {}, { continuation: continueToken }); return [ ...(0, traverse_1.traverseList)(songsData, "musicResponsiveListItemRenderer"), ...(0, traverse_1.traverseList)(moreSongsData, "musicResponsiveListItemRenderer"), ].map(s => SongParser_1.default.parseArtistSong(s, { artistId, name: (0, traverse_1.traverseString)(artistData, "header", "title", "text"), })); } /** * Get all of Artist's Albums * * @param artistId Artist ID * @returns Artist's Albums */ async getArtistAlbums(artistId) { const artistData = await this.constructRequest("browse", { browseId: artistId, }); const artistAlbumsData = (0, traverse_1.traverseList)(artistData, "musicCarouselShelfRenderer")[0]; const browseBody = (0, traverse_1.traverse)(artistAlbumsData, "moreContentButton", "browseEndpoint"); const albumsData = await this.constructRequest("browse", browseBody); return (0, traverse_1.traverseList)(albumsData, "musicTwoRowItemRenderer").map(item => AlbumParser_1.default.parseArtistAlbum(item, { artistId, name: (0, traverse_1.traverseString)(albumsData, "header", "runs", "text"), })); } /** * Get all possible information of an Album * * @param albumId Album ID * @returns Album Data */ async getAlbum(albumId) { const data = await this.constructRequest("browse", { browseId: albumId, }); return AlbumParser_1.default.parse(data, albumId); } /** * Get all possible information of a Playlist except the tracks * * @param playlistId Playlist ID * @returns Playlist Data */ async getPlaylist(playlistId) { if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId; const data = await this.constructRequest("browse", { browseId: playlistId, }); return PlaylistParser_1.default.parse(data, playlistId); } /** * Get all videos in a Playlist * * @param playlistId Playlist ID * @returns Playlist's Videos */ async getPlaylistVideos(playlistId) { if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId; const playlistData = await this.constructRequest("browse", { browseId: playlistId, }); const songs = (0, traverse_1.traverseList)(playlistData, "musicPlaylistShelfRenderer", "musicResponsiveListItemRenderer"); let continuation = (0, traverse_1.traverse)(playlistData, "continuation"); // Sometimes it returns array, dunno why if (continuation instanceof Array) { continuation = continuation[0]; } while (!(continuation instanceof Array)) { const songsData = await this.constructRequest("browse", {}, { continuation }); songs.push(...(0, traverse_1.traverseList)(songsData, "musicResponsiveListItemRenderer")); continuation = (0, traverse_1.traverse)(songsData, "continuation"); } return songs.map(VideoParser_1.default.parsePlaylistVideo).filter((video) => video !== undefined); } /** * Get sections for the home page. * * @returns Mixed HomeSection */ async getHomeSections() { const data = await this.constructRequest("browse", { browseId: constants_1.FE_MUSIC_HOME, }); const sections = (0, traverse_1.traverseList)("sectionListRenderer", "contents"); let continuation = (0, traverse_1.traverseString)(data, "continuation"); while (continuation) { const data = await this.constructRequest("browse", {}, { continuation }); sections.push(...(0, traverse_1.traverseList)(data, "sectionListContinuation", "contents")); continuation = (0, traverse_1.traverseString)(data, "continuation"); } return sections.map(Parser_1.default.parseHomeSection); } } exports.default = YTMusic;