ym-api-meowed
Version:
A Node.js wrapper for the Yandex.Music API (Unofficial) http://music.yandex.ru
771 lines (680 loc) • 20.9 kB
text/typescript
import {
authRequest,
apiRequest,
directLinkRequest
} from "./PreparedRequest/index";
import fallbackConfig from "./PreparedRequest/config";
import HttpClient from "./Network/HttpClient";
import { parseStringPromise } from "xml2js";
import * as crypto from "crypto";
import {
ApiConfig,
ApiInitConfig,
InitResponse,
GetGenresResponse,
SearchResponse,
Playlist,
GetTrackResponse,
Language,
GetTrackSupplementResponse,
GetTrackDownloadInfoResponse,
GetFeedResponse,
GetAccountStatusResponse,
Track,
TrackId,
ApiUser,
SearchOptions,
ConcreteSearchOptions,
SearchAllResponse,
SearchArtistsResponse,
SearchTracksResponse,
SearchAlbumsResponse,
AlbumId,
Album,
AlbumWithTracks,
FilledArtist,
Artist,
ArtistId,
ArtistTracksResponse,
DisOrLikedTracksResponse,
ChartType,
ChartTracksResponse,
NewReleasesResponse,
NewPlaylistsResponse,
PodcastsResponse,
SimilarTracksResponse,
StationTracksResponse,
StationInfoResponse,
AllStationsListResponse,
RecomendedStationsListResponse,
QueuesResponse,
QueueResponse
} from "./Types";
import { HttpClientInterface, ObjectResponse } from "./Types/request";
import shortenLink from "./ClckApi";
export default class YMApi {
private user: ApiUser = {
password: "",
token: "",
uid: 0,
username: ""
};
constructor(
private httpClient: HttpClientInterface = new HttpClient(),
private config: ApiConfig = fallbackConfig
) {}
private getAuthHeader(): { Authorization: string } {
return {
Authorization: `OAuth ${this.user.token}`
};
}
private getFakeDeviceHeader(): { "X-Yandex-Music-Device": string } {
return {
"X-Yandex-Music-Device":
"os=unknown; os_version=unknown; manufacturer=unknown; model=unknown; clid=; device_id=unknown; uuid=unknown"
};
}
/**
* Authentication
* @returns access_token & uid
*/
async init(config: ApiInitConfig): Promise<InitResponse> {
// Skip auth if access_token and uid are present
if (config.access_token && config.uid) {
this.user.token = config.access_token;
this.user.uid = config.uid;
return {
access_token: config.access_token,
uid: config.uid
};
}
if (!config.username || !config.password) {
throw new Error(
"username && password || access_token && uid must be set"
);
}
this.user.username = config.username;
this.user.password = config.password;
const data = (await this.httpClient.get(
authRequest().setPath("/token").setQuery({
grant_type: "password",
username: this.user.username,
password: this.user.password,
client_id: this.config.oauth.CLIENT_ID,
client_secret: this.config.oauth.CLIENT_SECRET
})
)) as ObjectResponse;
this.user.token = data.access_token;
this.user.uid = data.uid;
return data as InitResponse;
}
/**
* GET: /account/status
* @returns account status for current user
*/
getAccountStatus(): Promise<GetAccountStatusResponse> {
const request = apiRequest()
.setPath("/account/status")
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<GetAccountStatusResponse>;
}
/**
* GET: /feed
* @returns the user's feed
*/
getFeed(): Promise<GetFeedResponse> {
const request = apiRequest()
.setPath("/feed")
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<GetFeedResponse>;
}
/**
*
* @param ChartType Type of chart.
* GET: /landing3/chart/{ChartType}
* @returns chart of songs.
*/
getChart(ChartType: ChartType): Promise<ChartTracksResponse> {
const request = apiRequest()
.setPath(`/landing3/chart/${ChartType}`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<ChartTracksResponse>;
}
/**
* GET: /landing3/new-playlists
* @returns new playlists (for you).
*/
getNewPlaylists(): Promise<NewPlaylistsResponse> {
const request = apiRequest()
.setPath("/landing3/new-playlists")
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<NewPlaylistsResponse>;
}
/**
* GET: /landing3/new-releases
* @returns new releases.
*/
getNewReleases(): Promise<NewReleasesResponse> {
const request = apiRequest()
.setPath("/landing3/new-releases")
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<NewReleasesResponse>;
}
/**
* GET: /landing3/podcasts
* @returns all podcasts.
*/
getPodcasts(): Promise<PodcastsResponse> {
const request = apiRequest()
.setPath("/landing3/podcasts")
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<PodcastsResponse>;
}
/**
* GET: /genres
* @returns a list of music genres
*/
getGenres(): Promise<GetGenresResponse> {
const request = apiRequest()
.setPath("/genres")
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<GetGenresResponse>;
}
/**
* GET: /search
* Search artists, tracks, albums.
* @returns Every {type} with query in it's title.
*/
search(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
const type = !options.type ? "all" : options.type;
const page = String(!options.page ? 0 : options.page);
const nococrrect = String(
options.nococrrect == null ? false : options.nococrrect
);
const request = apiRequest()
.setPath("/search")
.addHeaders(this.getAuthHeader())
.setQuery({
type,
text: query,
page,
nococrrect
});
if (options.pageSize !== void 0) {
request.addQuery({ pageSize: String(options.pageSize) });
}
return this.httpClient.get(request) as Promise<SearchResponse>;
}
/**
* @param query Query
* @param options Options
* @returns Every artist with query in it's title.
*/
searchArtists(
query: string,
options: ConcreteSearchOptions = {}
): Promise<SearchArtistsResponse> {
return this.search(query, {
...options,
type: "artist"
}) as Promise<SearchArtistsResponse>;
}
/**
* @param query Query
* @param options Options
* @returns Every track with query in it's title.
*/
searchTracks(
query: string,
options: ConcreteSearchOptions = {}
): Promise<SearchTracksResponse> {
return this.search(query, {
...options,
type: "track"
}) as Promise<SearchTracksResponse>;
}
/**
* @param query Query
* @param options Options
* @returns Every album with query in it's title.
*/
searchAlbums(
query: string,
options: ConcreteSearchOptions = {}
): Promise<SearchAlbumsResponse> {
return this.search(query, {
...options,
type: "album"
}) as Promise<SearchAlbumsResponse>;
}
/**
* @param query Query
* @param options Options
* @returns Everything with query in it's title.
*/
searchAll(
query: string,
options: ConcreteSearchOptions = {}
): Promise<SearchAllResponse> {
return this.search(query, {
...options,
type: "all"
}) as Promise<SearchAllResponse>;
}
/**
* GET: /users/[user_id]/playlists/list
* @returns a user's playlists.
*/
getUserPlaylists(
user: number | string | null = null
): Promise<Array<Playlist>> {
const uid = [null, 0, ""].includes(user) ? this.user.uid : user;
const request = apiRequest()
.setPath(`/users/${uid}/playlists/list`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<Array<Playlist>>;
}
/**
* GET: /users/[user_id]/playlists/[playlist_kind]
* @returns a playlist without tracks
*/
getPlaylist(
playlistId: number,
user: number | string | null = null
): Promise<Playlist> {
const uid = [null, 0, ""].includes(user) ? this.user.uid : user;
const request = apiRequest()
.setPath(`/users/${uid}/playlists/${playlistId}`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<Playlist>;
}
/**
* GET: /users/[user_id]/playlists
* @returns an array of playlists with tracks
*/
getPlaylists(
playlists: Array<number>,
user: number | string | null = null,
options: { mixed?: boolean; "rich-tracks"?: boolean } = {}
): Promise<Array<Playlist>> {
const uid = [null, 0, ""].includes(user) ? this.user.uid : user;
const kinds = playlists.join();
const mixed = String(options.mixed == null ? false : options.mixed);
const richTracks = String(
options["rich-tracks"] == null ? false : options["rich-tracks"]
);
const request = apiRequest()
.setPath(`/users/${uid}/playlists`)
.addHeaders(this.getAuthHeader())
.setQuery({
kinds,
mixed,
"rich-tracks": richTracks
});
return this.httpClient.get(request) as Promise<Array<Playlist>>;
}
/**
* POST: /users/[user_id]/playlists/create
* Create a new playlist
* @returns Playlist
*/
createPlaylist(
name: string,
options: { visibility?: "public" | "private" } = {}
): Promise<Playlist> {
const visibility = !options.visibility ? "private" : options.visibility;
const request = apiRequest()
.setPath(`/users/${this.user.uid}/playlists/create`)
.addHeaders(this.getAuthHeader())
.setBodyData({
title: name,
visibility
});
return this.httpClient.post(request) as Promise<Playlist>;
}
/**
* POST: /users/[user_id]/playlists/[playlist_kind]/delete
* Remove a playlist
* @returns "ok" | string
*/
removePlaylist(playlistId: number): Promise<"ok" | string> {
const request = apiRequest()
.setPath(`/users/${this.user.uid}/playlists/${playlistId}/delete`)
.addHeaders(this.getAuthHeader());
return this.httpClient.post(request) as Promise<"ok" | string>;
}
/**
* POST: /users/[user_id]/playlists/[playlist_kind]/name
* Change playlist name
* @returns Playlist
*/
renamePlaylist(playlistId: number, name: string): Promise<Playlist> {
const request = apiRequest()
.setPath(`/users/${this.user.uid}/playlists/${playlistId}/name`)
.addHeaders(this.getAuthHeader())
.setBodyData({
value: name
});
return this.httpClient.post(request) as Promise<Playlist>;
}
/**
* POST: /users/[user_id]/playlists/[playlist_kind]/change-relative
* Add tracks to the playlist
* @returns Playlist
*/
addTracksToPlaylist(
playlistId: number,
tracks: Array<{ id: number; albumId: number }>,
revision: number,
options: { at?: number } = {}
): Promise<Playlist> {
const at = !options.at ? 0 : options.at;
const request = apiRequest()
.setPath(
`/users/${this.user.uid}/playlists/${playlistId}/change-relative`
)
.addHeaders(this.getAuthHeader())
.setBodyData({
diff: JSON.stringify([
{
op: "insert",
at,
tracks: tracks
}
]),
revision: String(revision)
});
return this.httpClient.post(request) as Promise<Playlist>;
}
/**
* POST: /users/[user_id]/playlists/[playlist_kind]/change-relative
* Remove tracks from the playlist
* @returns Playlist
*/
removeTracksFromPlaylist(
playlistId: number,
tracks: Array<{ id: number; albumId: number }>,
revision: number,
options: { from?: number; to?: number } = {}
): Promise<Playlist> {
const from = !options.from ? 0 : options.from;
const to = !options.to ? tracks.length : options.to;
const request = apiRequest()
.setPath(
`/users/${this.user.uid}/playlists/${playlistId}/change-relative`
)
.addHeaders(this.getAuthHeader())
.setBodyData({
diff: JSON.stringify([
{
op: "delete",
from,
to,
tracks
}
]),
revision: String(revision)
});
return this.httpClient.post(request) as Promise<Playlist>;
}
/**
* GET: /tracks/[track_id]
* @returns an array of playlists with tracks
*/
getTrack(trackId: TrackId): Promise<GetTrackResponse> {
const request = apiRequest()
.setPath(`/tracks/${trackId}`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<GetTrackResponse>;
}
/**
* GET: /tracks/[track_id]
* @returns single track
*/
async getSingleTrack(trackId: TrackId): Promise<Track> {
const tracks = await this.getTrack(trackId);
if (tracks.length !== 1) {
throw new Error(`More than one result received`);
}
return tracks.pop() as Track;
}
/**
* GET: /tracks/[track_id]/supplement
* @returns an array of playlists with tracks
*/
getTrackSupplement(trackId: TrackId): Promise<GetTrackSupplementResponse> {
const request = apiRequest()
.setPath(`/tracks/${trackId}/supplement`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<GetTrackSupplementResponse>;
}
/**
* GET: /tracks/[track_id]/download-info
* @returns track download information
*/
getTrackDownloadInfo(
trackId: TrackId
): Promise<GetTrackDownloadInfoResponse> {
const request = apiRequest()
.setPath(`/tracks/${trackId}/download-info`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(
request
) as Promise<GetTrackDownloadInfoResponse>;
}
/**
* @returns track direct link
*/
async getTrackDirectLink(
trackDownloadUrl: string,
short: Boolean = false
): Promise<string> {
const request = directLinkRequest(trackDownloadUrl);
const xml = await this.httpClient.get(request);
const parsedXml = await parseStringPromise(xml);
const host = parsedXml["download-info"].host[0];
const path = parsedXml["download-info"].path[0];
const ts = parsedXml["download-info"].ts[0];
const s = parsedXml["download-info"].s[0];
const sign = crypto
.createHash("md5")
.update("XGRlBW9FXlekgbPrRHuSiA" + path.slice(1) + s)
.digest("hex");
const link = `https://${host}/get-mp3/${sign}/${ts}${path}`;
if (short) return await shortenLink(link);
else return link;
}
/**
* @returns track sharing link
*/
async getTrackShareLink(track: TrackId | Track): Promise<string> {
let albumid = 0,
trackid = 0;
if (typeof track === "object") {
albumid = track.albums[0].id;
trackid = track.id;
} else {
albumid = (await this.getSingleTrack(track)).albums[0].id;
trackid = track;
}
return `https://music.yandex.ru/album/${albumid}/track/${trackid}`;
}
/**
* GET: /tracks/{track_id}/similar
* @returns simmilar tracks
*/
getSimilarTracks(trackId: TrackId): Promise<SimilarTracksResponse> {
const request = apiRequest()
.setPath(`/tracks/${trackId}/similar`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<SimilarTracksResponse>;
}
/**
* GET: /albums/[album_id]
* @returns an album
*/
getAlbum(albumId: AlbumId, withTracks: boolean = false): Promise<Album> {
const request = apiRequest()
.setPath(`/albums/${albumId}${withTracks ? "/with-tracks" : ""}`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<Album>;
}
getAlbumWithTracks(albumId: AlbumId): Promise<AlbumWithTracks> {
return this.getAlbum(albumId, true) as Promise<AlbumWithTracks>;
}
/**
* GET: /albums
* @returns an albums
*/
getAlbums(albumIds: Array<AlbumId>): Promise<Array<Album>> {
const request = apiRequest()
.setPath(`/albums`)
.setBodyData({ albumIds: albumIds.join() })
.addHeaders(this.getAuthHeader());
return this.httpClient.post(request) as Promise<Array<Album>>;
}
/**
* GET: /artists/[artist_id]
* @returns an artist
*/
getArtist(artistId: ArtistId): Promise<FilledArtist> {
const request = apiRequest()
.setPath(`/artists/${artistId}`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<FilledArtist>;
}
/**
* GET: /artists
* @returns an artists
*/
getArtists(artistIds: Array<ArtistId>): Promise<Array<Artist>> {
const request = apiRequest()
.setPath(`/artists`)
.setBodyData({ artistIds: artistIds.join() })
.addHeaders(this.getAuthHeader());
return this.httpClient.post(request) as Promise<Array<Artist>>;
}
/**
* GET: /artists/[artist_id]/tracks
* @returns Tracks by artist id
*/
getArtistTracks(
artistId: ArtistId,
options: SearchOptions = {}
): Promise<ArtistTracksResponse> {
const page = String(!options.page ? 0 : options.page);
const request = apiRequest()
.setPath(`/artists/${artistId}/tracks`)
.addHeaders(this.getAuthHeader())
.setQuery({
page
});
if (options.pageSize !== void 0) {
request.addQuery({ pageSize: String(options.pageSize) });
}
return this.httpClient.get(request) as Promise<ArtistTracksResponse>;
}
/**
* GET: /users/{userId}/likes/tracks
* @param userId User id. Nullable.
* @returns Liked Tracks
*/
getLikedTracks(
userId: number | string | null = null
): Promise<DisOrLikedTracksResponse> {
const uid = [null, 0, ""].includes(userId) ? this.user.uid : userId;
const request = apiRequest()
.setPath(`/users/${uid}/likes/tracks`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<DisOrLikedTracksResponse>;
}
/**
* GET: /users/{userId}/dislikes/tracks
* @param userId User id. Nullable.
* @returns Disliked Tracks
*/
getDislikedTracks(
userId: number | string | null = null
): Promise<DisOrLikedTracksResponse> {
const uid = [null, 0, ""].includes(userId) ? this.user.uid : userId;
const request = apiRequest()
.setPath(`/users/${uid}/dislikes/tracks`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<DisOrLikedTracksResponse>;
}
/**
* GET: /rotor/stations/list
* @param language Language of station list
* @returns list of stations.
*/
getAllStationsList(language?: Language): Promise<AllStationsListResponse> {
const request = apiRequest()
.setPath(`/rotor/stations/list`)
.addHeaders(this.getAuthHeader())
.setQuery(language ? { language } : {});
return this.httpClient.get(request) as Promise<AllStationsListResponse>;
}
/**
* GET: /rotor/stations/dashboard
* REQUIRES YOU TO BE LOGGED IN!
* @returns list of recomended stations.
*/
getRecomendedStationsList(): Promise<RecomendedStationsListResponse> {
const request = apiRequest()
.setPath("/rotor/stations/dashboard")
.addHeaders(this.getAuthHeader());
return this.httpClient.get(
request
) as Promise<RecomendedStationsListResponse>;
}
/**
* GET: /rotor/station/{stationId}/tracks
* REQUIRES YOU TO BE LOGGED IN!
* @param stationId Id of station. Example: user:onyourwave
* @param queue Unique id of prev track.
* @returns tracks from station.
*/
getStationTracks(
stationId: string,
queue?: string
): Promise<StationTracksResponse> {
const request = apiRequest()
.setPath(`/rotor/station/${stationId}/tracks`)
.addHeaders(this.getAuthHeader())
.addQuery(queue ? { queue } : {});
return this.httpClient.get(request) as Promise<StationTracksResponse>;
}
/**
* GET: /rotor/station/{stationId}/info
* @param stationId Id of station. Example: user:onyourwave
* @returns info of the station.
*/
getStationInfo(stationId: string): Promise<StationInfoResponse> {
const request = apiRequest()
.setPath(`/rotor/station/${stationId}/info`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<StationInfoResponse>;
}
/**
* GET: /queues
* @returns queues without tracks
*/
getQueues(): Promise<QueuesResponse> {
const request = apiRequest()
.setPath("/queues")
.addHeaders(this.getFakeDeviceHeader())
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<QueuesResponse>;
}
/**
* GET: /queues/{queueId}
* @param queueId Queue id.
* @returns queue data with(?) tracks.
*/
getQueue(queueId: string): Promise<QueueResponse> {
const request = apiRequest()
.setPath(`/queues/${queueId}`)
.addHeaders(this.getAuthHeader());
return this.httpClient.get(request) as Promise<QueueResponse>;
}
}