youtube-moosick
Version:
Unofficial Youtube music API, fully written in TypeScript
321 lines • 13.9 kB
JavaScript
;
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.YoutubeMoosick = void 0;
const axios_1 = __importDefault(require("axios"));
const http_js_1 = __importDefault(require("axios/lib/adapters/http.js"));
const tough_cookie_1 = __importDefault(require("tough-cookie"));
const enums_js_1 = require("./enums.js");
const utils_js_1 = require("./utils.js");
const index_js_1 = require("./resources/errors/index.js");
const url_1 = require("url");
const index_js_2 = require("./parsers/index.js");
const asyncConstructor_js_1 = require("./blocks/asyncConstructor.js");
const index_js_3 = require("./resources/resultTypes/index.js");
const index_js_4 = require("./resources/generalTypes/index.js");
__exportStar(require("./resources/resultTypes/index.js"), exports);
__exportStar(require("./resources/generalTypes/index.js"), exports);
axios_1.default.defaults.adapter = http_js_1.default;
// you found a kitten, please collect it
/**
* Main class to interact with methods
*
* @public
*/
class YoutubeMoosick extends asyncConstructor_js_1.AsyncConstructor {
client;
cookies;
config;
/**
* Creates a new instance of the searcher.
* @returns Adds with the original constructor
* @remarks Required to construct along with the class.
* @internal
* @beta
*/
async new() {
this.cookies = new tough_cookie_1.default.CookieJar();
this.client = axios_1.default.create({
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,
});
this.client.interceptors.request.use((req) => {
if (!req.baseURL
|| !req.headers) {
throw new index_js_1.IllegalStateError('Incomplete `req`');
}
const cookies = this.cookies.getCookieStringSync(req.baseURL);
if (cookies && cookies.length > 0) {
req.headers.Cookie = cookies;
}
return req;
}, async (err) => Promise.reject(err));
this.client.interceptors.response.use((res) => {
if (!res.config?.baseURL
|| (typeof res.headers !== 'object')) {
throw new index_js_1.IllegalStateError('Incomplete `req`');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { headers } = res;
if (headers['set-cookie'] == null) {
return res;
}
if (headers['set-cookie'] instanceof Array || typeof headers['set-cookie'] === 'object') {
headers['set-cookie'].forEach((value) => {
this.parseAndSetCookie(value, res.config.baseURL);
});
}
else {
this.parseAndSetCookie(headers['set-cookie'], res.config.baseURL);
}
return res;
});
const res = await this.client.get('/');
const dataString = /(?<=ytcfg\.set\().+(?=\);)/.exec(res.data)?.[0];
if (dataString == null) {
throw new index_js_1.IllegalStateError('API initialization returned a nullish value');
}
this.config = JSON.parse(dataString);
return this;
}
/**
* Overrides the constructor
* @internal
*/
static async new() {
void super.new();
return new YoutubeMoosick().new();
}
/**
* Sets the cookie that is called from the new method
* @param cookieString - Cookie string
* @param baseURL - The base URL of that the cookie should be applied
* @internal
*/
parseAndSetCookie(cookieString, baseURL) {
const cookie = tough_cookie_1.default.Cookie.parse(cookieString);
if (cookie == null) {
throw new index_js_1.IllegalArgumentError(`"${String(cookieString)}" is not a cookie`, 'cookieString');
}
this.cookies.setCookieSync(cookie, baseURL);
}
/**
* Creates a new api request to the specified endpoint.
* @param endpointName - The endpoint name?
* @param inputVariables - Any variable?
* @param inputQuery - Any queries?
* @returns The result of the endpoint reply
* @remarks Soonner or later destructure functions into individual files
*
* @internal
*/
async createApiRequest(endpointName, inputVariables = {}, inputQuery = {}) {
const res = await this.client.post(`youtubei/${this.config.INNERTUBE_API_VERSION}/${endpointName}?${new url_1.URLSearchParams({
alt: 'json',
key: this.config.INNERTUBE_API_KEY,
...inputQuery,
})
.toString()}`, {
...inputVariables,
...utils_js_1.utils.createApiContext(this.config),
}, {
responseType: 'json',
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,
...this.client.defaults.headers,
},
});
if (res.data?.responseContext != null) {
return res.data;
}
throw new index_js_1.IllegalStateError('Youtube Music API request failed (`res.data?.responseContext` was nullish)');
}
/**
* Get search suggestions from Youtube Music
* @param query - String query text to search
* @returns An object formatted by parsers.js
*
* Example
* ```typescript
* const suggestions = api.getSearchSuggestions("All We know");
* console.log(suggestions);
* ```
*/
async getSearchSuggestions(query) {
const res = await this.createApiRequest(enums_js_1.EndPoint.SUGGESTIONS, {
input: query,
});
if (res.contents == null) {
throw new index_js_1.IllegalStateError('No results found');
}
const { contents } = res.contents[0].searchSuggestionsSectionRenderer;
if (!contents) {
throw new index_js_1.IllegalStateError('Results array not found');
}
return contents
.map((searchSuggestionRenderer) => index_js_3.SearchSuggestions
.from({
title: searchSuggestionRenderer.searchSuggestionRenderer.suggestion.runs[0]?.text ?? '',
artist: searchSuggestionRenderer.searchSuggestionRenderer.suggestion.runs[1]?.text ?? '',
}));
}
async search(query, searchType) {
const URI = searchType ? `Eg-KAQwIA${utils_js_1.utils.mapCategoryToURL(searchType)}MABqChAEEAMQCRAFEAo%3D` : '';
const ctx = await this.createApiRequest(enums_js_1.EndPoint.SEARCH, {
query,
params: URI,
});
const { result, continuation, } = index_js_2.GeneralParser.parseSearchResult(ctx, searchType);
const continuableResult = new index_js_4.ContinuableResultFactory(searchType == null ? index_js_4.ContinuableUnsorted : index_js_4.ContinuableResult)
.create({
ctx: this,
getContent: (context) => context.result,
getContinuation: (context) => context.continuation,
parser: (context) => index_js_2.GeneralParser.parseSearchResult(context, searchType),
isDone: (context) => (context?.length ?? 0) === 0,
continuation,
})
.append(result);
return continuableResult;
}
/**
* Gets the album details
* @param browseId - The album Id only, without `https://....`
* @returns AlbumURL object
*
* Example:
* ```typescript
* const api = await MooSick.new();
* const results = await api.getAlbum('MPREb_REsMMqBZjZB');
*
* console.log(results)
* ```
*/
async getAlbum(browseId) {
if (!browseId.startsWith('MPREb')) {
throw new index_js_1.IllegalArgumentError('Album browse IDs must start with "MPREb"', 'browseId');
}
const ctx = await this.createApiRequest(enums_js_1.EndPoint.BROWSE, utils_js_1.utils.buildEndpointContext(browseId, enums_js_1.Category.ALBUM));
return index_js_2.GetAlbumParser.parseAlbumURLPage(ctx);
}
/**
* Gets the playlist using the Youtube Music API
* @param browseId - The playlist `browseId` only, without `https://....`
* @param contentLimit - Maximum amount of contents to get, defaults to 100
* @returns PlaylistURL object
*
* Example:
* ```typescript
* const api = await MooSick.new();
* const results = await api.getPlaylist('PLXs921kKn8XT5_bq5kR2gQ_blPZ7DgyS1');
*
* console.log(results);
* ```
*/
async getPlaylist(browseId, contentLimit = 100) {
if (!browseId.startsWith('VL')
&& !browseId.startsWith('PL')) {
throw new index_js_1.IllegalArgumentError('Playlist browse IDs must start with "VL" or "PL"', 'browseId');
}
if (browseId.startsWith('PL')) {
browseId = 'VL' + browseId;
}
const ctx = await this.createApiRequest(enums_js_1.EndPoint.BROWSE, utils_js_1.utils.buildEndpointContext(browseId, enums_js_1.Category.PLAYLIST));
const result = index_js_2.GetPlaylistParser.parsePlaylistURL(ctx);
// Results here are expected
const continuableResult = index_js_3.ContinuablePlaylistURL.from({
continuation: result.continuation,
headers: result.headers,
playlistContents: new index_js_4.ContinuableResultFactory()
.create({
ctx: this,
getContent: (context) => context.playlistContents,
getContinuation: (context) => context.continuation,
parser: index_js_2.GetPlaylistParser.parsePlaylistURL.bind(index_js_2.GetPlaylistParser),
continuation: result.continuation,
endpoint: enums_js_1.EndPoint.BROWSE,
})
.append(result.playlistContents),
});
await continuableResult.playlistContents
.loadUntil(contentLimit);
return continuableResult;
}
/**
* Gets the artist details from Youtube Music
* @param browseId - The artist `browseId` only, without `https://....`
* @returns ArtistURL object
*
* Example:
* ```typescript
* const api = await MooSick.new();
* const results = await api.getArtist('UCAq0pFGa2w9SjxOq0ZxKVIw');
*
* console.log(results);
* ```
*/
async getArtist(browseId) {
if (!browseId.startsWith('UC')) {
throw new index_js_1.IllegalArgumentError('Artist browse IDs must start with "UC"', 'browseId');
}
const ctx = await this.createApiRequest(enums_js_1.EndPoint.BROWSE, utils_js_1.utils.buildEndpointContext(browseId, enums_js_1.Category.ARTIST));
return index_js_2.GetArtistParser.parseArtistURLPage(ctx);
}
/**
* Gets the `browseId` for the album based on the newer `listID`
* @param listID - The `listID` of the album
* @returns String The `browseID` of the album
*
* Example:
* ```typescript
* const api = await MooSick.new();
* const results = await api.getAlbumBrowseId('OLAK5uy_ljhFMBuzqiynvNq_3dC2QhQaz12zkD0LE');
*
* console.log(results);
* ```
*
*/
async getAlbumBrowseId(listID) {
if (!listID.startsWith('OLAK')) {
throw new index_js_1.IllegalArgumentError('Artist browse IDs must start with "OLAK"', 'listID');
}
const res = await this.client.get(`https://music.youtube.com/playlist?${new url_1.URLSearchParams({
list: listID,
}).toString()}`, {});
const result = /"MPREb.+?"/g.exec(res.data) ?? [];
if (result.length > 0) {
return decodeURI(encodeURI(result[0])).replaceAll('"', '').replaceAll('\\', '');
}
throw new index_js_1.IllegalStateError('No Album ID was found');
}
}
exports.YoutubeMoosick = YoutubeMoosick;
//# sourceMappingURL=YoutubeMoosick.js.map