import fetch from "node-fetch"; import queryString from "querystring"; export interface Album { album_type: string, artists: SimpleArtist[], available_markets: string[], copyrights: { text: string, type: string }[], external_ids: { isrc?: string, ean?: string, upc?: string }, external_urls: any, genres: string[], href: string, id: string, images: Image[], label: string, name: string, popularity: number, release_date: string, release_date_precision: string, tracks: PagingObject, type: string, uri: string } export interface SimpleAlbum { album_group?: string, album_type: string, artists: SimpleArtist[], available_markets: string[], external_urls: any, href: string, id: string, images: Image[], name: string, release_date: string, release_date_precision: string, type: string, uri: string } export interface Artist { external_urls: any, followers: { href: string, total: number }, genres: string[], href: string, id: string, images: Image[], name: string, popularity: number, type: string, uri: string } export interface SimpleArtist { external_urls: any, href: string, id: string, name: string, type: string, uri: string } export interface AudioFeatures { acousticness: number, analysis_url: string, danceability: number, duration_ms: number, energy: number, id: string, instrumentalness: number, key: number, liveness: number, loudness: number, mode: number, speechiness: number, tempo: string, time_signature: number, track_href: string, type: string, uri: string, valence: number } export interface Category { href: string, icons: Image[], id: string, name: string } export interface Context { type: string, href: string, external_urls: any, uri: string } export interface Error { status: number, message: string, reason?: string } export interface Image { height: number, url: string, width: number } export interface PagingObject { href: string, items: T[], limit: number, next: string, offset: number, previous: string, total: number } export interface Playlist { collaborative: boolean, description: string, external_urls: any, followers: { href: string, total: number }, href: string, id: string, images: Image[], name: string, owner: PublicUser, public: boolean snapshot_id: string, tracks: PagingObject>, type: string, uri: string } export interface SimplePlaylist { collaborative: boolean, description: string, external_urls: { spotify: string }, href: string, id: string, images: Image[], name: string, owner: PublicUser, public: boolean, snapshot_id: string, tracks: PagingObject>, type: string, uri: string } export interface PlaylistTrack { added_at: string, added_by: PublicUser, is_local: boolean, track: T } export interface Recommendation { seeds: Seed[], tracks: SimpleTrack[] } export interface Seed { afterFilteringSize: number, afterRelinkingSize: number, href: string, id: string, initialPoolSize: number, type: string } export interface SavedTrack { added_at: string, track: Track } export interface SavedAlbum { added_at: string, album: Album } export interface SavedShow { added_at: string, show: Show } export interface Track { album: SimpleAlbum, artists: SimpleArtist[], available_markets: string[], disc_number: number, duration_ms: number, explicit: boolean, external_ids: { isrc?: string, ean?: string, upc?: string } external_urls: any, href: string, id: string, is_playable: boolean, linked_from?: LinkedTrack, name: string, popularity: number, preview_url: string, track_number: number, type: string, uri: string, is_local: boolean } export interface SimpleTrack { artists: SimpleArtist[], available_markets: string[], disc_number: number, duration_ms: number, explicit: boolean, external_urls: any, href: string, id: string, is_playable: boolean, linked_from?: LinkedTrack, name: string, preview_url: string, track_number: number, type: string, uri: string, is_local: boolean } export interface LinkedTrack { external_urls: any, href: string, id: string, type: string, uri: string } export interface Episode { audio_preview_url: string, description: string, duration_ms: number, explicit: boolean, external_urls: any, href: string, id: string, images: Image[], is_externally_hosted: boolean, is_playable: boolean, language: string, languages: string[], name: string, release_date: string, release_date_precision: string, resume_point: ResumePoint, show: SimpleShow, type: string, uri: string } export interface SimpleEpisode { audio_preview_url: string, description: string, duration_ms: number, explicit: boolean, external_urls: any, href: string, id: string, images: Image[], is_externally_hosted: boolean, is_playable: boolean, language: string, languages: string[], name: string, release_date: string, release_date_precision: string, resume_point: ResumePoint, type: string, uri: string } export interface ResumePoint { fully_played: boolean, resume_position_ms: number } export interface Show { available_markets: string[], copyrights: { text: string, type: string }, description: string, explicit: boolean, episodes: PagingObject, external_urls: any, href: string, id: string, images: Image[], is_externally_hosted: boolean, languages: string[], media_type: string, name: string, publisher: string, type: string, uri: string } export interface SimpleShow { available_markets: string[], copyrights: { text: string, type: string }, description: string, explicit: boolean, external_urls: any, href: string, id: string, images: Image[], is_externally_hosted: boolean, languages: string[], media_type: string, name: string, publisher: string, type: string, uri: string } export interface PrivateUser { country: string, display_name: string, email: string, explicit_content: { filter_enabled: boolean, filter_locked: boolean }, external_urls: any, followers: { href: string, total: number }, href: string, id: string, images: Image[], product: string, type: string, uri: string } export interface PublicUser { display_name: string, external_urls: any, followers: { href: string, total: number }, href: string, id: string, images: Image[], type: string, uri: string } export interface CursorPager { href: string, items: T[], limit: number, next: string, cursors: Cursor, total: number } export interface Cursor { after: string } export default class Spotify { private access_token: string; constructor(access_token: string) { this.access_token = access_token; } fullRequest(url: string, method: string = 'GET'): Promise { return fetch(url, { method: method, headers: { 'Authorization': 'Bearer ' + this.access_token } }).then(response => { if (response.status === 204) return {}; return response.json(); }); } makeRequest(endpoint: string, queryList: any, method: string = 'GET'): Promise { Object.keys(queryList).forEach(key => queryList[key] === undefined ? delete queryList[key] : {}); let query = queryString.stringify(queryList); let url = `https://api.spotify.com/v1${endpoint}?${query}`; return this.fullRequest(url, method); } makeBodyRequest(endpoint: string, body: any, queryList: any, method: string = 'GET'): Promise { Object.keys(queryList).forEach(key => queryList[key] === undefined ? delete queryList[key] : {}); let query = queryString.stringify(queryList); Object.keys(body).forEach(key => body[key] === undefined ? delete body[key] : {}); return fetch(`https://api.spotify.com/v1${endpoint}?${query}`, { method: method, headers: { 'Authorization': 'Bearer ' + this.access_token, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(response => { if (response.status === 204) return {}; return response.json(); }); } static requestRefreshToken(refreshToken: string, clientId: string, clientSecret: string): Promise { let bodyList = { grant_type: 'refresh_token', refresh_token: refreshToken }; let body = queryString.stringify(bodyList); return fetch('https://accounts.spotify.com/api/token', { method: 'POST', headers: { 'Authorization': 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'), 'Content-Type': 'application/x-www-form-urlencoded' }, body: body }).then(response => { if (response.status === 200) return response.json(); else return false; }); } next(result: any): Promise { return this.fullRequest(result.next, 'GET'); } previous(result: any): Promise { return this.fullRequest(result.previous, 'GET'); } getMe(): Promise { return this.makeRequest('/me', {}, 'GET'); } getUser(user_id: string): Promise { return this.makeRequest(`/users/${user_id}`, {}, 'GET'); } getAlbum(id: string): Promise { return this.makeRequest(`/albums/${id}`, {}, 'GET'); } getAlbums(ids: string[]): Promise<{ albums: Album[] } | Error> { return this.makeRequest('/albums', {ids: ids.join(',')}, 'GET'); } getAlbumTracks(id: string, limit?: number, offset?: number, market?: string): Promise | Error> { return this.makeRequest(`/albums/${id}/tracks`, {limit: limit, offset: offset, market: market}, 'GET') } getArtist(id: string): Promise { return this.makeRequest(`/artists/${id}`, {}, 'GET'); } getArtists(ids: string[]): Promise<{ artists: Artist[] } | Error> { return this.makeRequest('/artists', {ids: ids.join(',')}, 'GET'); } getArtistAlbums(id: string, limit: number, offset: number, includeGroups: string[] = ['album', 'single', 'appears_on', 'compilation'], country: string): Promise | Error> { return this.makeRequest(`/artists/${id}/albums`, { limit: limit, offset: offset, include_groups: includeGroups.join(','), country: country }, 'GET'); } getArtistTopTracks(id: string, country: string): Promise<{ tracks: Track[] } | Error> { return this.makeRequest(`/artists/${id}/top-tracks`, {country: country}, 'GET'); } getRelatedArtists(id: string): Promise<{ artists: Artist[] } | Error> { return this.makeRequest(`/artists/${id}/related-artists`, {}, 'GET'); } getCategory(id: string, country?: string, locale?: string): Promise { return this.makeRequest(`/browse/categories/${id}`, {country: country, locale: locale}, 'GET'); } getCategoryPlaylists(id: string, country?: string, limit?: number, offset?: number): Promise<{ playlists: PagingObject } | Error> { return this.makeRequest(`/browse/categories/${id}/playlists`, { country: country, limit: limit, offset: offset }, 'GET') } getCategories(country?: string, locale?: string, limit?: number, offset?: number): Promise<{ categories: PagingObject } | Error> { return this.makeRequest('/browse/categories', { country: country, locale: locale, limit: limit, offset: offset }, 'GET'); } getFeaturedPlaylists(locale?: string, country?: string, timestamp?: string, limit?: number, offset?: number): Promise<{ message: string, playlists: PagingObject } | Error> { return this.makeRequest('/browse/featured-playlists', { locale: locale, country: country, timestamp: timestamp, limit: limit, offset: offset }, 'GET'); } getNewReleases(country?: string, limit?: number, offset?: number): Promise<{ message: string, albums: PagingObject } | Error> { return this.makeRequest('/browse/new-releases', {country: country, limit: limit, offset: offset}, 'GET') } // TODO: Handle get Recommendations getEpisode(id: string, market?: string): Promise { return this.makeRequest(`/episodes/${id}`, {market: market}, 'GET') } getEpisodes(ids: string[], market?: string): Promise<{ episodes: Episode[] } | Error> { return this.makeRequest(`/episodes`, {ids: ids.join(','), market: market}, 'GET') } checkUserFollowing(type: string, ids: string[]): Promise { return this.makeRequest(`/me/following/contains`, {type: type, ids: ids.join(',')}, 'GET'); } checkUserFollowingPlaylist(playlist_id: string, ids: string[]): Promise { return this.makeRequest(`/playlists/${playlist_id}/followers/contains`, {ids: ids.join(',')}, 'GET'); } userFollow(type: string, ids: string[]): Promise<{} | Error> { return this.makeRequest(`/me/following`, {type: type, ids: ids.join(',')}, 'PUT'); } // TODO: user follow playlist // userFollowPlaylist(playlist_id, public = True) getFollowedArtists(limit?: number, after?: string): Promise | Error> { return this.makeRequest(`/me/following`, {type: 'artist', limit: limit, after: after}, 'GET'); } checkUserSavedAlbums(ids: string[]): Promise { return this.makeRequest(`/me/albums/contains`, {ids: ids.join(',')}, 'GET'); } checkUserSavedShows(ids: string[]): Promise { return this.makeRequest(`/me/shows/contains`, {ids: ids.join(',')}, 'GET'); } checkUserSavedTracks(ids: string[]): Promise { return this.makeRequest(`/me/tracks/contains`, {ids: ids.join(',')}, 'GET'); } getUserSavedAlbums(limit?: number, offset?: number, market?: string): Promise | Error> { return this.makeRequest(`/me/albums`, {limit: limit, offset: offset, market: market}, 'GET'); } getUserSavedShows(limit?: number, offset?: number): Promise | Error> { return this.makeRequest(`/me/shows`, {limit: limit, offset: offset}, 'GET'); } getUserSavedTracks(limit?: number, offset?: number, market?: string): Promise | Error> { return this.makeRequest(`/me/tracks`, {limit: limit, offset: offset, market: market}, 'GET'); } removeUserSavedAlbums(ids: string[]): Promise { return this.makeRequest(`/me/albums`, {ids: ids.join(',')}, 'DELETE'); } removeUserSavedShows(ids: string[], market?: string): Promise { return this.makeRequest(`/me/shows`, {ids: ids.join(','), market: market}, 'DELETE'); } removeUserSavedTracks(ids: string[]): Promise { return this.makeRequest(`/me/tracks`, {ids: ids.join(',')}, 'DELETE'); } addUserSavedAlbums(ids: string[]): Promise { return this.makeRequest(`/me/albums`, {ids: ids.join(',')}, 'PUT'); } addUserSavedShows(ids: string[]): Promise { return this.makeRequest(`/me/shows`, {ids: ids.join(',')}, 'PUT'); } addUserSavedTracks(ids: string[]): Promise { return this.makeRequest(`/me/tracks`, {ids: ids.join(',')}, 'PUT'); } getUserTopTracks(time_range?: string, limit?: number, offset?: number): Promise | Error> { return this.makeRequest('/me/top/tracks', {time_range: time_range, limit: limit, offset: offset}, 'GET'); } getUserTopArtists(time_range?: string, limit?: number, offset?: number): Promise | Error> { return this.makeRequest('/me/top/artists', {time_range: time_range, limit: limit, offset: offset}, 'GET'); } addTrackToPlaylist(playlist_id: string, uris: string[], position: number): Promise<{ snapshot_id: string } | Error> { return this.makeBodyRequest(`/playlists/${playlist_id}/tracks`, {uris: uris, position: position}, {}, 'POST') } changePlaylistDetails(playlist_id: string, name?: string, public_?: boolean, collaborative?: boolean, description?: string): Promise { return this.makeBodyRequest(`/playlist/${playlist_id}`, { name: name, public: public_, collaborative: collaborative, description: description }, {}, 'PUT'); } createPlaylist(user_id: string, name: string, public_?: boolean, collaborative?: boolean, description?: string): Promise { return this.makeBodyRequest(`/users/${user_id}/playlists`, { name: name, public: public_, collaborative: collaborative, description: description }, {}, 'POST'); } getUserPlaylists(limit?: number, offset?: number): Promise { return this.makeRequest('/me/playlists', {limit: limit, offset: offset}, 'GET'); } getPlaylists(user_id: string, limit?: number, offset?: number): Promise { return this.makeRequest(`/users/${user_id}/playlists`, {limit: limit, offset: offset}, 'GET'); } getPlaylist(playlist_id: string, market?: string): Promise { return this.makeRequest(`/playlists/${playlist_id}`, {market: market}, 'GET'); } getPlaylistImage(playlist_id: string): Promise { return this.makeRequest(`/playlists/${playlist_id}/images`, {}, 'GET'); } getPlaylistTracks(playlist_id: string, limit?: number, offset?: number, market?: string): Promise { return this.makeRequest(`/playlists/${playlist_id}/tracks`, { limit: limit, offset: offset, market: market }, 'GET'); } removePlaylistTracks(playlist_id: string, uris: any, positions?: any, snapshot_id?: string): Promise { if (positions) { Object.keys(positions).forEach(key => Array.isArray(positions[key]) ? {} : positions[key] = [positions[key]]); Object.keys(uris).forEach(key => uris[key] = {uri: uris[key], positions: positions[key]}); } else { Object.keys(uris).forEach(key => uris[key] = {uri: uris[key]}); } return this.makeBodyRequest(`/playlists/${playlist_id}/tracks`, { tracks: uris, snapshot_id: snapshot_id }, {}, 'DELETE'); } reorderPlaylistTracks( playlist_id: string, range_start: number, insert_before: number, range_length?: number, snapshot_id?: string ): Promise { return this.makeBodyRequest(`/playlists/${playlist_id}/tracks`, { range_start: range_start, range_length: range_length, insert_before: insert_before, snapshot_id: snapshot_id }, {}, 'PUT'); } search(q: string, type: string, market?: string, limit?: number, offset?: number, include_external?: string): Promise { return this.makeRequest('/search', { q: q, type: type, market: market, limit: limit, offset: offset, include_external: include_external }, 'GET'); } getShow(show_id: string, market?: string): Promise { return this.makeRequest(`/shows/${show_id}`, {market: market}, 'GET'); } getShows(show_ids: string[], market?: string): Promise { return this.makeRequest('/shows', {ids: show_ids.join(','), market: market}) } getShowEpisodes(show_id: string, limit?: number, offset?: number, market?: string): Promise { return this.makeRequest(`/shows/${show_id}/episodes`, {limit: limit, offset: offset, market: market}, 'GET'); } getAudioAnalysis(track_id: string): Promise { return this.makeRequest(`/audio-analysis/${track_id}`, {}, 'GET'); } getAudioFeatures(track_id: string): Promise { return this.makeRequest(`/audio-features/${track_id}`, {}, 'GET'); } getMultipleAudioFeatures(ids: string[]): Promise { return this.makeRequest('/audio-features', {ids: ids.join(',')}, 'GET'); } getTrack(track_id: string, market?: string): Promise { return this.makeRequest(`/tracks/${track_id}`, {market: market}, 'GET'); } getTracks(ids: string[], market?: string): Promise { return this.makeRequest('/tracks', {ids: ids.join(','), market: market}, 'GET'); } async getFullPlaylistTracks(playlist_id: string, market?: string) { let items = []; let json = await this.getPlaylistTracks(playlist_id, undefined, undefined, market); if (json.items == null) { return null; } while (json.next != null) { items.push(...json.items); json = await this.next(json); } items.push(...json.items); // console.log(items.length); return items; } addQueue(uri: string, device_id?: string) { return this.makeRequest('/me/player/queue', {uri: uri, device_id: device_id}, 'POST'); } getPlayback(market?: string, additional_types?: string) { return this.makeRequest('/me/player', {market: market, additional_types: additional_types}, 'GET'); } getRecentlyPlayed(limit?: string, after?: number, before?: number) { return this.makeRequest('/me/player/recently-played', {limit: limit, after: after, before: before}, 'GET'); } getCurrentlyPlaying(market?: string, additional_types?: string) { return this.makeRequest('/me/player/currently-playing', { market: market, additional_types: additional_types }, 'GET'); } pausePlayback(device_id?: string) { return this.makeRequest('/me/player/pause', {device_id}, 'PUT'); } startPlayback(device_id?: string, context_uri?: string, uris?: any, offset?: any, position_ms?: number) { return this.makeBodyRequest('/me/player/play', {context_uri, uris, offset, position_ms}, {device_id}, 'PUT'); } }