spotify-api-lib
Version:
A modern, TypeScript-first wrapper for the Spotify Web API with organized endpoint categories and full type safety
1 lines • 70.6 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/httpClient.ts","../src/baseEndpoint.ts","../src/endpoints/playlists.ts","../src/endpoints/tracks.ts","../src/endpoints/albums.ts","../src/endpoints/artists.ts","../src/endpoints/search.ts","../src/endpoints/player.ts","../src/endpoints/user.ts","../src/main.ts"],"sourcesContent":["/**\n * @file index.ts\n * @description Main entry point for spotify-api-lib\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\n// Main API class\nexport { SpotifyApi } from './main'\n\n// HTTP client for advanced usage\nexport { SpotifyHttpClient } from './httpClient'\n\n// All endpoint classes\nexport { PlaylistEndpoints } from './endpoints/playlists'\nexport { TrackEndpoints } from './endpoints/tracks'\nexport { AlbumEndpoints } from './endpoints/albums'\nexport { ArtistEndpoints } from './endpoints/artists'\nexport { SearchEndpoints } from './endpoints/search'\nexport { PlayerEndpoints } from './endpoints/player'\nexport { UserEndpoints } from './endpoints/user'\n\n// All types\nexport * from './types'\n\n// Default export\nexport { SpotifyApi as default } from './main'\n","/**\n * @file httpClient.ts\n * @description HTTP client for making requests to Spotify API\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\nimport axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'\n\nexport interface RequestOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE'\n data?: any\n params?: Record<string, any>\n headers?: Record<string, string>\n retries?: number\n}\n\nexport interface SpotifyApiError extends Error {\n status?: number\n response?: any\n}\n\n// HTTP status codes that should be retried\nconst RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])\n\nexport class SpotifyHttpClient {\n private client: AxiosInstance\n private accessToken: string\n\n constructor(accessToken?: string) {\n this.accessToken = accessToken || ''\n \n this.client = axios.create({\n baseURL: 'https://api.spotify.com/v1',\n timeout: 10000,\n headers: {\n 'Content-Type': 'application/json',\n },\n })\n\n // Set up request interceptor to add auth header\n this.client.interceptors.request.use((config: any) => {\n if (this.accessToken) {\n config.headers.Authorization = `Bearer ${this.accessToken}`\n }\n return config\n })\n\n // Set up response interceptor for error handling\n this.client.interceptors.response.use(\n (response: any) => response,\n (error: any) => {\n if (error.response?.status === 401) {\n console.error('Spotify API: Unauthorized - token may be expired')\n } else if (error.response?.status === 429) {\n console.error('Spotify API: Rate limited')\n } else if (error.response?.status >= 500) {\n console.error('Spotify API: Server error')\n }\n return Promise.reject(error)\n }\n )\n }\n\n /**\n * Set the access token for authentication\n */\n setAccessToken(token: string): void {\n this.accessToken = token\n }\n\n /**\n * Clear the access token\n */\n clearAccessToken(): void {\n this.accessToken = ''\n }\n\n /**\n * Make an HTTP request to the Spotify API with improved error handling\n */\n async request<T = any>(endpoint: string, options?: RequestOptions): Promise<T> {\n const maxRetries = options?.retries ?? 3\n let attempt = 0\n let lastError: SpotifyApiError | null = null\n\n while (attempt <= maxRetries) {\n try {\n const config: AxiosRequestConfig = {\n url: endpoint,\n method: options?.method || 'GET',\n data: options?.data,\n params: options?.params,\n headers: options?.headers,\n }\n \n const response: AxiosResponse<T> = await this.client.request(config)\n return response.data\n } catch (error: any) {\n const status = error?.response?.status\n lastError = this.createSpotifyError(error)\n \n // Don't retry on first attempt for non-retryable errors\n if (attempt === 0 && !this.isRetryableError(status)) {\n throw lastError\n }\n \n // Handle rate limiting with exponential backoff\n if (status === 429) {\n const retryAfter = this.getRetryAfterDelay(error.response.headers?.['retry-after'])\n console.warn(`Spotify API rate limited. Retrying after ${retryAfter}ms (attempt ${attempt + 1}/${maxRetries + 1})`)\n await this.delay(retryAfter)\n } else if (this.isRetryableError(status)) {\n // Exponential backoff for other retryable errors\n const delay = Math.min(1000 * Math.pow(2, attempt), 10000) + Math.random() * 1000\n console.warn(`Spotify API error ${status}. Retrying after ${delay}ms (attempt ${attempt + 1}/${maxRetries + 1})`)\n await this.delay(delay)\n } else {\n // Non-retryable error\n throw lastError\n }\n \n attempt++\n }\n }\n \n throw lastError || new Error('Spotify API request failed after all retries')\n }\n\n /**\n * Check if an HTTP status code should be retried\n */\n private isRetryableError(status?: number): boolean {\n return status ? RETRYABLE_STATUS_CODES.has(status) : false\n }\n\n /**\n * Get retry delay from Retry-After header or default\n */\n private getRetryAfterDelay(retryAfterHeader?: string): number {\n if (retryAfterHeader) {\n const parsed = parseInt(retryAfterHeader, 10)\n if (!isNaN(parsed)) {\n return parsed * 1000 // Convert seconds to milliseconds\n }\n }\n return 1000 // Default 1 second\n }\n\n /**\n * Create a standardized error object\n */\n private createSpotifyError(error: any): SpotifyApiError {\n const spotifyError: SpotifyApiError = new Error(\n error?.response?.data?.error?.message || \n error?.message || \n 'Spotify API request failed'\n )\n spotifyError.status = error?.response?.status\n spotifyError.response = error?.response?.data\n return spotifyError\n }\n\n /**\n * Promise-based delay utility\n */\n private delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n }\n\n /**\n * Get the underlying axios instance for advanced usage\n */\n getClient(): AxiosInstance {\n return this.client\n }\n}\n","/**\n * @file baseEndpoint.ts\n * @description Base class for API endpoint groups with convenience methods\n * @author Caleb Price\n * @version 1.1.0\n * @date 2025-07-21\n */\n\nimport { SpotifyHttpClient, RequestOptions } from './httpClient'\n\nexport abstract class BaseEndpoint {\n protected client: SpotifyHttpClient\n\n constructor(client: SpotifyHttpClient) {\n this.client = client\n }\n\n // Generic request method\n protected async makeRequest<T = any>(endpoint: string, options?: RequestOptions): Promise<T> {\n return this.client.request<T>(endpoint, options)\n }\n\n // Convenience methods for common HTTP verbs\n protected async get<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> {\n return this.makeRequest<T>(endpoint, { method: 'GET', params })\n }\n\n protected async post<T = any>(endpoint: string, data?: any, params?: Record<string, any>): Promise<T> {\n return this.makeRequest<T>(endpoint, { method: 'POST', data, params })\n }\n\n protected async put<T = any>(endpoint: string, data?: any, params?: Record<string, any>): Promise<T> {\n return this.makeRequest<T>(endpoint, { method: 'PUT', data, params })\n }\n\n protected async delete<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> {\n return this.makeRequest<T>(endpoint, { method: 'DELETE', params })\n }\n\n // Standard error handling\n protected handleError(error: any, operation: string): never {\n const message = error?.response?.data?.error?.message || error?.message || 'Unknown error'\n throw new Error(`${operation} failed: ${message}`)\n }\n}\n","/**\n * @file playlists.ts\n * @description Playlist-related API endpoints\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\nimport { BaseEndpoint } from '../baseEndpoint'\nimport { SpotifyPlaylist, SpotifyPagingObject } from '../types'\n\nexport class PlaylistEndpoints extends BaseEndpoint {\n /**\n * Get current user's playlists\n */\n async getUserPlaylists(options?: {\n limit?: number\n offset?: number\n }): Promise<SpotifyPlaylist[]> {\n let allPlaylists: SpotifyPlaylist[] = []\n let offset = options?.offset || 0\n const limit = options?.limit || 50\n\n try {\n while (true) {\n const response = await this.makeRequest<SpotifyPagingObject<SpotifyPlaylist>>('/me/playlists', {\n params: { limit, offset },\n })\n\n allPlaylists = allPlaylists.concat(response.items)\n\n if (!response.next || response.items.length === 0) {\n break\n }\n\n offset += limit\n }\n\n return allPlaylists\n } catch (error) {\n console.error('Error fetching playlists:', error)\n throw new Error('Failed to fetch playlists')\n }\n }\n\n /**\n * Create a new playlist\n */\n async create(\n name: string,\n options?: {\n description?: string\n public?: boolean\n collaborative?: boolean\n },\n ): Promise<SpotifyPlaylist> {\n try {\n // Get current user info first\n const userResponse = await this.makeRequest('/me')\n const userId = userResponse.id\n\n // Create the playlist\n const playlistData = {\n name,\n description: options?.description || `Created on ${new Date().toLocaleDateString()}`,\n public: options?.public ?? false,\n collaborative: options?.collaborative ?? false,\n }\n\n const response = await this.makeRequest<SpotifyPlaylist>(`/users/${userId}/playlists`, {\n method: 'POST',\n data: playlistData,\n })\n\n return response\n } catch (error) {\n console.error('Error creating playlist:', error)\n throw new Error('Failed to create playlist')\n }\n }\n\n /**\n * Add tracks to a playlist\n */\n async addTracks(\n playlistId: string,\n trackUris: string[],\n options?: {\n position?: number\n },\n ): Promise<{ snapshot_id: string }> {\n try {\n const batchSize = 100\n let lastResponse: any\n\n for (let i = 0; i < trackUris.length; i += batchSize) {\n const batch = trackUris.slice(i, i + batchSize)\n const data: any = { uris: batch }\n\n if (options?.position !== undefined) {\n data.position = options.position + i\n }\n\n lastResponse = await this.makeRequest(`/playlists/${playlistId}/tracks`, {\n method: 'POST',\n data,\n })\n }\n\n return lastResponse\n } catch (error) {\n console.error('Error adding tracks to playlist:', error)\n throw new Error('Failed to add tracks to playlist')\n }\n }\n\n /**\n * Get tracks from a playlist\n */\n async getTracks(\n playlistId: string,\n options?: {\n limit?: number\n offset?: number\n fields?: string\n },\n ): Promise<any> {\n try {\n const params: any = {\n limit: options?.limit || 50,\n offset: options?.offset || 0,\n }\n\n if (options?.fields) {\n params.fields = options.fields\n }\n\n const response = await this.makeRequest(`/playlists/${playlistId}/tracks`, {\n params,\n })\n\n return response\n } catch (error) {\n console.error('Error fetching playlist tracks:', error)\n throw new Error('Failed to fetch playlist tracks')\n }\n }\n\n /**\n * Get a playlist by ID\n */\n async getById(\n playlistId: string,\n options?: {\n fields?: string\n market?: string\n },\n ): Promise<SpotifyPlaylist> {\n try {\n const params: any = {}\n\n if (options?.fields) {\n params.fields = options.fields\n }\n if (options?.market) {\n params.market = options.market\n }\n\n const response = await this.makeRequest<SpotifyPlaylist>(`/playlists/${playlistId}`, {\n params,\n })\n\n return response\n } catch (error) {\n console.error('Error fetching playlist:', error)\n throw new Error('Failed to fetch playlist')\n }\n }\n\n /**\n * Update playlist details\n */\n async update(\n playlistId: string,\n options: {\n name?: string\n description?: string\n public?: boolean\n collaborative?: boolean\n },\n ): Promise<void> {\n try {\n await this.makeRequest(`/playlists/${playlistId}`, {\n method: 'PUT',\n data: options,\n })\n } catch (error) {\n console.error('Error updating playlist:', error)\n throw new Error('Failed to update playlist')\n }\n }\n\n /**\n * Remove tracks from a playlist\n */\n async removeTracks(\n playlistId: string,\n tracks: Array<{ uri: string; positions?: number[] }>,\n ): Promise<{ snapshot_id: string }> {\n try {\n const response = await this.makeRequest<{ snapshot_id: string }>(`/playlists/${playlistId}/tracks`, {\n method: 'DELETE',\n data: { tracks },\n })\n\n return response\n } catch (error) {\n console.error('Error removing tracks from playlist:', error)\n throw new Error('Failed to remove tracks from playlist')\n }\n }\n}\n","/**\n * @file tracks.ts\n * @description Track-related API endpoints\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\nimport { BaseEndpoint } from '../baseEndpoint'\nimport { SpotifyTrack, SpotifyPagingObject } from '../types'\n\nexport class TrackEndpoints extends BaseEndpoint {\n /**\n * Get user's saved tracks (liked songs)\n */\n async getSavedTracks(options?: {\n limit?: number\n offset?: number\n market?: string\n }): Promise<SpotifyPagingObject<{ added_at: string; track: SpotifyTrack }>> {\n // Validate parameters\n const limit = this.validateLimit(options?.limit, 50)\n const offset = this.validateOffset(options?.offset)\n \n const params = {\n limit,\n offset,\n ...(options?.market && { market: options.market }),\n }\n\n return await this.get<SpotifyPagingObject<{ added_at: string; track: SpotifyTrack }>>('/me/tracks', params)\n }\n\n /**\n * Validate limit parameter\n */\n private validateLimit(limit?: number, defaultValue: number = 20): number {\n if (!limit) return defaultValue\n return Math.min(Math.max(1, limit), 50) // Spotify API limit is 50\n }\n\n /**\n * Validate offset parameter\n */\n private validateOffset(offset?: number): number {\n return Math.max(0, offset || 0)\n }\n\n /**\n * Get albums from user's liked songs\n */\n async getLikedSongsAlbums(fetchLimit?: number): Promise<any[]> {\n const albumsMap = new Map<string, any>()\n const singlesMap = new Map<string, any>()\n let offset = 0\n let fetchedItems = 0\n const limit = 50\n\n while (!fetchLimit || fetchedItems < fetchLimit) {\n const response = await this.get('/me/tracks', { limit, offset })\n const items = response.items\n\n if (items.length === 0) break\n\n fetchedItems += items.length\n\n items.forEach((item: any) => {\n if (item.track && item.track.album) {\n const album = item.track.album\n if (album.album_type === 'single') {\n singlesMap.set(album.id, {\n id: album.id,\n name: album.name,\n artists: album.artists,\n artistIds: album.artists.map((a: any) => a.id),\n track: item.track.name,\n images: album.images,\n release_date: album.release_date,\n album_type: album.album_type,\n })\n }\n if (!albumsMap.has(album.id)) {\n albumsMap.set(album.id, {\n id: album.id,\n name: album.name,\n artists: album.artists.map((a: any) => a.name).join(', '),\n release_date: album.release_date,\n total_tracks: album.total_tracks,\n images: album.images,\n album_type: album.album_type,\n })\n }\n }\n })\n\n offset += limit\n }\n\n return Array.from(albumsMap.values())\n }\n\n /**\n * Get track by ID\n */\n async getById(\n trackId: string,\n options?: {\n market?: string\n },\n ): Promise<SpotifyTrack> {\n const params: any = {}\n if (options?.market) {\n params.market = options.market\n }\n\n return await this.get<SpotifyTrack>(`/tracks/${trackId}`, params)\n }\n\n /**\n * Get multiple tracks by IDs\n */\n async getByIds(\n trackIds: string[],\n options?: {\n market?: string\n },\n ): Promise<{ tracks: SpotifyTrack[] }> {\n const params: any = {\n ids: trackIds.join(','),\n }\n if (options?.market) {\n params.market = options.market\n }\n\n return await this.get<{ tracks: SpotifyTrack[] }>('/tracks', params)\n }\n\n /**\n * Save tracks for current user\n */\n async saveTracks(trackIds: string[]): Promise<void> {\n await this.put('/me/tracks', { ids: trackIds })\n }\n\n /**\n * Remove tracks from current user's saved tracks\n */\n async removeTracks(trackIds: string[]): Promise<void> {\n await this.delete('/me/tracks', { ids: trackIds })\n }\n\n /**\n * Check if tracks are saved for current user\n */\n async checkSavedTracks(trackIds: string[]): Promise<boolean[]> {\n return await this.get<boolean[]>('/me/tracks/contains', { ids: trackIds.join(',') })\n }\n\n /**\n * Get audio features for a track\n */\n async getAudioFeatures(trackId: string): Promise<any> {\n return await this.get(`/audio-features/${trackId}`)\n }\n\n /**\n * Get audio analysis for a track\n */\n async getAudioAnalysis(trackId: string): Promise<any> {\n return await this.get(`/audio-analysis/${trackId}`)\n }\n}\n","/**\n * @file albums.ts\n * @description Album-related API endpoints\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\nimport { BaseEndpoint } from '../baseEndpoint'\nimport { SpotifyAlbum, SpotifyTrack, SpotifyPagingObject } from '../types'\n\nexport class AlbumEndpoints extends BaseEndpoint {\n /**\n * Get album by ID\n */\n async getById(\n albumId: string,\n options?: {\n market?: string\n },\n ): Promise<SpotifyAlbum> {\n try {\n const params: any = {}\n if (options?.market) {\n params.market = options.market\n }\n\n const response = await this.makeRequest<SpotifyAlbum>(`/albums/${albumId}`, { params })\n return response\n } catch (error) {\n console.error('Error fetching album:', error)\n throw new Error('Failed to fetch album')\n }\n }\n\n /**\n * Get multiple albums by IDs\n */\n async getByIds(\n albumIds: string[],\n options?: {\n market?: string\n },\n ): Promise<{ albums: SpotifyAlbum[] }> {\n try {\n const params: any = {\n ids: albumIds.join(','),\n }\n if (options?.market) {\n params.market = options.market\n }\n\n const response = await this.makeRequest<{ albums: SpotifyAlbum[] }>('/albums', { params })\n return response\n } catch (error) {\n console.error('Error fetching albums:', error)\n throw new Error('Failed to fetch albums')\n }\n }\n\n /**\n * Get album tracks\n */\n async getTracks(\n albumId: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifyPagingObject<SpotifyTrack>> {\n try {\n const params = {\n limit: options?.limit || 50,\n offset: options?.offset || 0,\n ...(options?.market && { market: options.market }),\n }\n\n const response = await this.makeRequest<SpotifyPagingObject<SpotifyTrack>>(`/albums/${albumId}/tracks`, { params })\n return response\n } catch (error) {\n console.error('Error fetching album tracks:', error)\n throw new Error('Failed to fetch album tracks')\n }\n }\n\n /**\n * Get user's saved albums\n */\n async getSavedAlbums(options?: {\n limit?: number\n offset?: number\n market?: string\n }): Promise<SpotifyPagingObject<{ added_at: string; album: SpotifyAlbum }>> {\n try {\n const params = {\n limit: options?.limit || 50,\n offset: options?.offset || 0,\n ...(options?.market && { market: options.market }),\n }\n\n const response = await this.makeRequest<SpotifyPagingObject<{ added_at: string; album: SpotifyAlbum }>>('/me/albums', { params })\n return response\n } catch (error) {\n console.error('Error fetching saved albums:', error)\n throw new Error('Failed to fetch saved albums')\n }\n }\n\n /**\n * Save albums for current user\n */\n async saveAlbums(albumIds: string[]): Promise<void> {\n try {\n await this.makeRequest('/me/albums', {\n method: 'PUT',\n data: { ids: albumIds },\n })\n } catch (error) {\n console.error('Error saving albums:', error)\n throw new Error('Failed to save albums')\n }\n }\n\n /**\n * Remove albums from current user's saved albums\n */\n async removeAlbums(albumIds: string[]): Promise<void> {\n try {\n await this.makeRequest('/me/albums', {\n method: 'DELETE',\n data: { ids: albumIds },\n })\n } catch (error) {\n console.error('Error removing albums:', error)\n throw new Error('Failed to remove albums')\n }\n }\n\n /**\n * Check if albums are saved for current user\n */\n async checkSavedAlbums(albumIds: string[]): Promise<boolean[]> {\n try {\n const response = await this.makeRequest<boolean[]>('/me/albums/contains', {\n params: { ids: albumIds.join(',') },\n })\n return response\n } catch (error) {\n console.error('Error checking saved albums:', error)\n throw new Error('Failed to check saved albums')\n }\n }\n\n /**\n * Get new album releases\n */\n async getNewReleases(options?: {\n country?: string\n limit?: number\n offset?: number\n }): Promise<SpotifyPagingObject<SpotifyAlbum>> {\n try {\n const params = {\n limit: options?.limit || 20,\n offset: options?.offset || 0,\n ...(options?.country && { country: options.country }),\n }\n\n const response = await this.makeRequest<{ albums: SpotifyPagingObject<SpotifyAlbum> }>('/browse/new-releases', { params })\n return response.albums\n } catch (error) {\n console.error('Error fetching new releases:', error)\n throw new Error('Failed to fetch new releases')\n }\n }\n}\n","/**\n * @file artists.ts\n * @description Artist-related API endpoints\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\nimport { BaseEndpoint } from '../baseEndpoint'\nimport { SpotifyArtist, SpotifyAlbum, SpotifyTrack, SpotifyPagingObject } from '../types'\n\nexport class ArtistEndpoints extends BaseEndpoint {\n /**\n * Get artist by ID\n */\n async getById(artistId: string): Promise<SpotifyArtist> {\n try {\n const response = await this.makeRequest<SpotifyArtist>(`/artists/${artistId}`)\n return response\n } catch (error) {\n console.error('Error fetching artist:', error)\n throw new Error('Failed to fetch artist')\n }\n }\n\n /**\n * Get multiple artists by IDs\n */\n async getByIds(artistIds: string[]): Promise<{ artists: SpotifyArtist[] }> {\n try {\n const params = {\n ids: artistIds.join(','),\n }\n\n const response = await this.makeRequest<{ artists: SpotifyArtist[] }>('/artists', { params })\n return response\n } catch (error) {\n console.error('Error fetching artists:', error)\n throw new Error('Failed to fetch artists')\n }\n }\n\n /**\n * Get artist's albums\n */\n async getAlbums(\n artistId: string,\n options?: {\n include_groups?: string[]\n market?: string\n limit?: number\n offset?: number\n },\n ): Promise<SpotifyPagingObject<SpotifyAlbum>> {\n try {\n const params = {\n include_groups: options?.include_groups?.join(',') || 'album',\n limit: options?.limit || 50,\n offset: options?.offset || 0,\n ...(options?.market && { market: options.market }),\n }\n\n const response = await this.makeRequest<SpotifyPagingObject<SpotifyAlbum>>(`/artists/${artistId}/albums`, { params })\n return response\n } catch (error) {\n console.error('Error fetching artist albums:', error)\n throw new Error('Failed to fetch artist albums')\n }\n }\n\n /**\n * Get artist's top tracks\n */\n async getTopTracks(\n artistId: string,\n options?: {\n market?: string\n },\n ): Promise<{ tracks: SpotifyTrack[] }> {\n try {\n const params = {\n market: options?.market || 'US',\n }\n\n const response = await this.makeRequest<{ tracks: SpotifyTrack[] }>(`/artists/${artistId}/top-tracks`, { params })\n return response\n } catch (error) {\n console.error('Error fetching artist top tracks:', error)\n throw new Error('Failed to fetch artist top tracks')\n }\n }\n\n /**\n * Get related artists\n */\n async getRelatedArtists(artistId: string): Promise<{ artists: SpotifyArtist[] }> {\n try {\n const response = await this.makeRequest<{ artists: SpotifyArtist[] }>(`/artists/${artistId}/related-artists`)\n return response\n } catch (error) {\n console.error('Error fetching related artists:', error)\n throw new Error('Failed to fetch related artists')\n }\n }\n\n /**\n * Follow artists\n */\n async follow(artistIds: string[]): Promise<void> {\n try {\n await this.makeRequest('/me/following', {\n method: 'PUT',\n params: { type: 'artist' },\n data: { ids: artistIds },\n })\n } catch (error) {\n console.error('Error following artists:', error)\n throw new Error('Failed to follow artists')\n }\n }\n\n /**\n * Unfollow artists\n */\n async unfollow(artistIds: string[]): Promise<void> {\n try {\n await this.makeRequest('/me/following', {\n method: 'DELETE',\n params: { type: 'artist' },\n data: { ids: artistIds },\n })\n } catch (error) {\n console.error('Error unfollowing artists:', error)\n throw new Error('Failed to unfollow artists')\n }\n }\n\n /**\n * Check if user follows artists\n */\n async checkFollowing(artistIds: string[]): Promise<boolean[]> {\n try {\n const response = await this.makeRequest<boolean[]>('/me/following/contains', {\n params: { \n type: 'artist',\n ids: artistIds.join(',') \n },\n })\n return response\n } catch (error) {\n console.error('Error checking following:', error)\n throw new Error('Failed to check following')\n }\n }\n\n /**\n * Get user's followed artists\n */\n async getFollowed(options?: {\n limit?: number\n after?: string\n }): Promise<{ artists: SpotifyPagingObject<SpotifyArtist> }> {\n try {\n const params = {\n type: 'artist',\n limit: options?.limit || 20,\n ...(options?.after && { after: options.after }),\n }\n\n const response = await this.makeRequest<{ artists: SpotifyPagingObject<SpotifyArtist> }>('/me/following', { params })\n return response\n } catch (error) {\n console.error('Error fetching followed artists:', error)\n throw new Error('Failed to fetch followed artists')\n }\n }\n}\n","/**\n * @file search.ts\n * @description Search-related API endpoints\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\nimport { BaseEndpoint } from '../baseEndpoint'\nimport { SpotifySearchResponse } from '../types'\n\nexport class SearchEndpoints extends BaseEndpoint {\n /**\n * Search for items\n */\n async search(\n query: string,\n options?: {\n type?: string[]\n limit?: number\n offset?: number\n market?: string\n include_external?: string\n },\n ): Promise<SpotifySearchResponse> {\n try {\n const params = {\n q: query,\n type: options?.type?.join(',') || 'track',\n limit: options?.limit || 20,\n offset: options?.offset || 0,\n ...(options?.market && { market: options.market }),\n ...(options?.include_external && { include_external: options.include_external }),\n }\n\n const response = await this.makeRequest<SpotifySearchResponse>('/search', { params })\n return response\n } catch (error) {\n console.error('Error searching:', error)\n throw new Error('Failed to search')\n }\n }\n\n /**\n * Search for tracks\n */\n async tracks(\n query: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifySearchResponse> {\n return this.search(query, {\n ...options,\n type: ['track'],\n })\n }\n\n /**\n * Search for albums\n */\n async albums(\n query: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifySearchResponse> {\n return this.search(query, {\n ...options,\n type: ['album'],\n })\n }\n\n /**\n * Search for artists\n */\n async artists(\n query: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifySearchResponse> {\n return this.search(query, {\n ...options,\n type: ['artist'],\n })\n }\n\n /**\n * Search for playlists\n */\n async playlists(\n query: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifySearchResponse> {\n return this.search(query, {\n ...options,\n type: ['playlist'],\n })\n }\n\n /**\n * Search for shows\n */\n async shows(\n query: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifySearchResponse> {\n return this.search(query, {\n ...options,\n type: ['show'],\n })\n }\n\n /**\n * Search for episodes\n */\n async episodes(\n query: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifySearchResponse> {\n return this.search(query, {\n ...options,\n type: ['episode'],\n })\n }\n\n /**\n * Search for audiobooks\n */\n async audiobooks(\n query: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifySearchResponse> {\n return this.search(query, {\n ...options,\n type: ['audiobook'],\n })\n }\n\n /**\n * Search everything\n */\n async all(\n query: string,\n options?: {\n limit?: number\n offset?: number\n market?: string\n },\n ): Promise<SpotifySearchResponse> {\n return this.search(query, {\n ...options,\n type: ['track', 'album', 'artist', 'playlist', 'show', 'episode', 'audiobook'],\n })\n }\n}\n","/**\n * @file player.ts\n * @description Player-related API endpoints\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\nimport { BaseEndpoint } from '../baseEndpoint'\n\nexport interface SpotifyPlaybackState {\n device: {\n id: string\n is_active: boolean\n is_private_session: boolean\n is_restricted: boolean\n name: string\n type: string\n volume_percent: number\n }\n repeat_state: 'off' | 'track' | 'context'\n shuffle_state: boolean\n context: {\n type: string\n href: string\n external_urls: { spotify: string }\n uri: string\n } | null\n timestamp: number\n progress_ms: number\n is_playing: boolean\n item: any // Track or Episode\n currently_playing_type: 'track' | 'episode' | 'ad' | 'unknown'\n actions: {\n interrupting_playback?: boolean\n pausing?: boolean\n resuming?: boolean\n seeking?: boolean\n skipping_next?: boolean\n skipping_prev?: boolean\n toggling_repeat_context?: boolean\n toggling_shuffle?: boolean\n toggling_repeat_track?: boolean\n transferring_playback?: boolean\n }\n}\n\nexport class PlayerEndpoints extends BaseEndpoint {\n /**\n * Get currently playing track\n */\n async getCurrentlyPlaying(options?: {\n market?: string\n additional_types?: string[]\n }): Promise<any> {\n try {\n const params: any = {}\n if (options?.market) {\n params.market = options.market\n }\n if (options?.additional_types) {\n params.additional_types = options.additional_types.join(',')\n }\n\n const response = await this.makeRequest('/me/player/currently-playing', { params })\n return response\n } catch (error) {\n console.error('Error fetching currently playing:', error)\n throw new Error('Failed to fetch currently playing track')\n }\n }\n\n /**\n * Get playback state\n */\n async getPlaybackState(options?: { \n market?: string \n additional_types?: string[] \n }): Promise<SpotifyPlaybackState> {\n try {\n const params: any = {}\n if (options?.market) {\n params.market = options.market\n }\n if (options?.additional_types) {\n params.additional_types = options.additional_types.join(',')\n }\n\n const response = await this.makeRequest<SpotifyPlaybackState>('/me/player', { params })\n return response\n } catch (error) {\n console.error('Error fetching playback state:', error)\n throw new Error('Failed to fetch playback state')\n }\n }\n\n /**\n * Transfer playback to a device\n */\n async transferPlayback(deviceIds: string[], play?: boolean): Promise<void> {\n try {\n await this.makeRequest('/me/player', {\n method: 'PUT',\n data: {\n device_ids: deviceIds,\n play: play ?? false,\n },\n })\n } catch (error) {\n console.error('Error transferring playback:', error)\n throw new Error('Failed to transfer playback')\n }\n }\n\n /**\n * Get available devices\n */\n async getDevices(): Promise<{ devices: any[] }> {\n try {\n const response = await this.makeRequest<{ devices: any[] }>('/me/player/devices')\n return response\n } catch (error) {\n console.error('Error fetching devices:', error)\n throw new Error('Failed to fetch devices')\n }\n }\n\n /**\n * Start/Resume playback\n */\n async play(options?: {\n device_id?: string\n context_uri?: string\n uris?: string[]\n offset?: { position: number } | { uri: string }\n position_ms?: number\n }): Promise<void> {\n try {\n const params: any = {}\n if (options?.device_id) {\n params.device_id = options.device_id\n }\n\n const data: any = {}\n if (options?.context_uri) {\n data.context_uri = options.context_uri\n }\n if (options?.uris) {\n data.uris = options.uris\n }\n if (options?.offset) {\n data.offset = options.offset\n }\n if (options?.position_ms) {\n data.position_ms = options.position_ms\n }\n\n await this.makeRequest('/me/player/play', {\n method: 'PUT',\n params,\n data: Object.keys(data).length > 0 ? data : undefined,\n })\n } catch (error) {\n console.error('Error starting playback:', error)\n throw new Error('Failed to start playback')\n }\n }\n\n /**\n * Pause playback\n */\n async pause(device_id?: string): Promise<void> {\n try {\n const params: any = {}\n if (device_id) {\n params.device_id = device_id\n }\n\n await this.makeRequest('/me/player/pause', {\n method: 'PUT',\n params,\n })\n } catch (error) {\n console.error('Error pausing playback:', error)\n throw new Error('Failed to pause playback')\n }\n }\n\n /**\n * Skip to next track\n */\n async next(device_id?: string): Promise<void> {\n try {\n const params: any = {}\n if (device_id) {\n params.device_id = device_id\n }\n\n await this.makeRequest('/me/player/next', {\n method: 'POST',\n params,\n })\n } catch (error) {\n console.error('Error skipping to next:', error)\n throw new Error('Failed to skip to next track')\n }\n }\n\n /**\n * Skip to previous track\n */\n async previous(device_id?: string): Promise<void> {\n try {\n const params: any = {}\n if (device_id) {\n params.device_id = device_id\n }\n\n await this.makeRequest('/me/player/previous', {\n method: 'POST',\n params,\n })\n } catch (error) {\n console.error('Error skipping to previous:', error)\n throw new Error('Failed to skip to previous track')\n }\n }\n\n /**\n * Seek to position in track\n */\n async seek(position_ms: number, device_id?: string): Promise<void> {\n try {\n const params: any = { position_ms }\n if (device_id) {\n params.device_id = device_id\n }\n\n await this.makeRequest('/me/player/seek', {\n method: 'PUT',\n params,\n })\n } catch (error) {\n console.error('Error seeking:', error)\n throw new Error('Failed to seek')\n }\n }\n\n /**\n * Set repeat mode\n */\n async setRepeat(state: 'track' | 'context' | 'off', device_id?: string): Promise<void> {\n try {\n const params: any = { state }\n if (device_id) {\n params.device_id = device_id\n }\n\n await this.makeRequest('/me/player/repeat', {\n method: 'PUT',\n params,\n })\n } catch (error) {\n console.error('Error setting repeat:', error)\n throw new Error('Failed to set repeat mode')\n }\n }\n\n /**\n * Set shuffle mode\n */\n async setShuffle(state: boolean, device_id?: string): Promise<void> {\n try {\n const params: any = { state }\n if (device_id) {\n params.device_id = device_id\n }\n\n await this.makeRequest('/me/player/shuffle', {\n method: 'PUT',\n params,\n })\n } catch (error) {\n console.error('Error setting shuffle:', error)\n throw new Error('Failed to set shuffle mode')\n }\n }\n\n /**\n * Set volume\n */\n async setVolume(volume_percent: number, device_id?: string): Promise<void> {\n try {\n const params: any = { volume_percent }\n if (device_id) {\n params.device_id = device_id\n }\n\n await this.makeRequest('/me/player/volume', {\n method: 'PUT',\n params,\n })\n } catch (error) {\n console.error('Error setting volume:', error)\n throw new Error('Failed to set volume')\n }\n }\n\n /**\n * Add item to playback queue\n */\n async addToQueue(uri: string, device_id?: string): Promise<void> {\n try {\n const params: any = { uri }\n if (device_id) {\n params.device_id = device_id\n }\n\n await this.makeRequest('/me/player/queue', {\n method: 'POST',\n params,\n })\n } catch (error) {\n console.error('Error adding to queue:', error)\n throw new Error('Failed to add to queue')\n }\n }\n\n /**\n * Get the user's queue\n */\n async getQueue(): Promise<any> {\n try {\n const response = await this.makeRequest('/me/player/queue')\n return response\n } catch (error) {\n console.error('Error fetching queue:', error)\n throw new Error('Failed to fetch queue')\n }\n }\n\n /**\n * Get recently played tracks\n */\n async getRecentlyPlayed(options?: {\n limit?: number\n after?: number\n before?: number\n }): Promise<any> {\n try {\n const params: any = {\n limit: options?.limit || 20,\n }\n if (options?.after) {\n params.after = options.after\n }\n if (options?.before) {\n params.before = options.before\n }\n\n const response = await this.makeRequest('/me/player/recently-played', { params })\n return response\n } catch (error) {\n console.error('Error fetching recently played:', error)\n throw new Error('Failed to fetch recently played tracks')\n }\n }\n}\n","/**\n * @file user.ts\n * @description User-related API endpoints\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n */\n\nimport { BaseEndpoint } from '../baseEndpoint'\nimport { SpotifyUser } from '../types'\n\nexport class UserEndpoints extends BaseEndpoint {\n /**\n * Get current user's profile\n */\n async getCurrentUser(): Promise<SpotifyUser> {\n try {\n const response = await this.makeRequest<SpotifyUser>('/me')\n return response\n } catch (error) {\n console.error('Error fetching current user:', error)\n throw new Error('Failed to fetch current user')\n }\n }\n\n /**\n * Get user's profile by ID\n */\n async getById(userId: string): Promise<SpotifyUser> {\n try {\n const response = await this.makeRequest<SpotifyUser>(`/users/${userId}`)\n return response\n } catch (error) {\n console.error('Error fetching user:', error)\n throw new Error('Failed to fetch user')\n }\n }\n\n /**\n * Get user's top items (tracks or artists)\n */\n async getTopItems(\n type: 'tracks' | 'artists',\n options?: {\n time_range?: 'short_term' | 'medium_term' | 'long_term'\n limit?: number\n offset?: number\n },\n ): Promise<any> {\n try {\n const params = {\n time_range: options?.time_range || 'medium_term',\n limit: options?.limit || 20,\n offset: options?.offset || 0,\n }\n\n const response = await this.makeRequest(`/me/top/${type}`, { params })\n return response\n } catch (error) {\n console.error(`Error fetching top ${type}:`, error)\n throw new Error(`Failed to fetch top ${type}`)\n }\n }\n\n /**\n * Get user's top tracks\n */\n async getTopTracks(options?: {\n time_range?: 'short_term' | 'medium_term' | 'long_term'\n limit?: number\n offset?: number\n }): Promise<any> {\n return this.getTopItems('tracks', options)\n }\n\n /**\n * Get user's top artists\n */\n async getTopArtists(options?: {\n time_range?: 'short_term' | 'medium_term' | 'long_term'\n limit?: number\n offset?: number\n }): Promise<any> {\n return this.getTopItems('artists', options)\n }\n\n /**\n * Follow a user\n */\n async followUser(userId: string): Promise<void> {\n try {\n await this.makeRequest('/me/following', {\n method: 'PUT',\n params: { type: 'user' },\n data: { ids: [userId] },\n })\n } catch (error) {\n console.error('Error following user:', error)\n throw new Error('Failed to follow user')\n }\n }\n\n /**\n * Unfollow a user\n */\n async unfollowUser(userId: string): Promise<void> {\n try {\n await this.makeRequest('/me/following', {\n method: 'DELETE',\n params: { type: 'user' },\n data: { ids: [userId] },\n })\n } catch (error) {\n console.error('Error unfollowing user:', error)\n throw new Error('Failed to unfollow user')\n }\n }\n\n /**\n * Check if current user follows users\n */\n async checkFollowingUsers(userIds: string[]): Promise<boolean[]> {\n try {\n const response = await this.makeRequest<boolean[]>('/me/following/contains', {\n params: { \n type: 'user',\n ids: userIds.join(',') \n },\n })\n return response\n } catch (error) {\n console.error('Error checking following users:', error)\n throw new Error('Failed to check following users')\n }\n }\n}\n","/**\n * @file main.ts\n * @description Main Spotify API class with constructor pattern for organized API access\n * @author Caleb Price\n * @version 1.0.0\n * @date 2025-07-14\n *\n * @description\n * Constructor-based Spotify API wrapper that provides organized methods for\n * interacting with the Spotify Web API. Supports playlists, tracks, albums, artists, search, player, and user operations.\n *\n * @usage\n * ```javascript\n * const spotify = new SpotifyApi(accessToken);\n * const playlists = await spotify.playlists.getUserPlaylists();\n * const album = await spotify.albums.getById(albumId);\n * const tracks = await spotify.tracks.getSavedTracks();\n * ```\n *\n * @ChangeLog\n * - 1.0.0: Initial implementation with all endpoint categories\n */\n\nimport { SpotifyHttpClient } from './httpClient'\nimport { PlaylistEndpoints } from './endpoints/playlists'\nimport { TrackEndpoints } from './endpoints/tracks'\nimport { AlbumEndpoints } from './endpoints/albums'\nimport { ArtistEndpoints } from './endpoints/artists'\nimport { SearchEndpoints } from './endpoints/search'\nimport { PlayerEndpoints } from './endpoints/player'\nimport { UserEndpoints } from './endpoints/user'\n\n// Endpoint classes mapping for optimized initialization\nconst ENDPOINT_CLASSES = {\n playlists: PlaylistEndpoints,\n tracks: TrackEndpoints,\n albums: AlbumEndpoints,\n artists: ArtistEndpoints,\n search: SearchEndpoints,\n player: PlayerEndpoints,\n user: UserEndpoints,\n} as const\n\nexport class SpotifyApi {\n private httpClient: SpotifyHttpClient\n\n // Endpoint groups\n public playlists!: PlaylistEndpoints\n public tracks!: TrackEndpoints\n public albums!: AlbumEndpoints\n public artists!: ArtistEndpoints\n public search!: SearchEndpoints\n public player!: PlayerEndpoints\n public user!: UserEndpoints\n\n constructor(accessToken?: string) {\n // Validate access token format if provided\n if (accessToken && !this.isValidTokenFormat(accessToken)) {\n console.warn('SpotifyApi: Access token format appears invalid')\n }\n\n this.httpClient = new SpotifyHttpClient(accessToken)\n\n // Initialize endpoint groups using optimized pattern\n this.initializeEndpoints()\n }\n\n /**\n * Initialize all endpoint groups\n */\n private initializeEndpoints(): void {\n for (const [name, EndpointClass] of Object.entries(ENDPOINT_CLASSES)) {\n ;(this as any)[name] = new EndpointClass(this.httpClient)\n }\n }\n\n /**\n * Basic token format validation\n */\n private isValidTokenFormat(token: string): boolean {\n // Spotify access tokens are typically 100+ characters and alphanumeric with some special chars\n return typeof token === 'string' && token.length > 50 && /^[A-Za-z0-9_-]+$/.test(token)\n }\n\n /**\n * Set access token for API requests\n */\n public setAccessToken(token: string): void {\n this.httpClient.setAccessToken(token)\n }\n\n /**\n * Clear the access token\n */\n public clearAccessToken(): void {\n this.httpClient.clearAccessToken()\n }\n\n /**\n * Make a raw API request\n */\n public async request(\n endpoint: string,\n options?: {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE'\n data?: any\n params?: Record<string, any>\n },\n ): Promise<any> {\n return this.httpClient.request(endpoint, options)\n }\n\n /**\n * Get the HTTP client instance for advanced usage\n */\n public getHttpClient(): SpotifyHttpClient {\n return this.httpClient\n }\n\n /**\n * Clean up resources and clear tokens\n */\n public destroy(): void {\n this.clearAccessToken()\n // Clear any cached data if endpoints have cleanup methods\n Object.values(ENDPOINT_CLASSES).forEach((_, key) => {\n const endpoint = (this as any)[Object.keys(ENDPOINT_CLASSES)[key]]\n if (endpoint && typeof endpoint.cleanup === 'function') {\n endpoint.cleanup()\n }\n })\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACQA,mBAAwE;AAgBxE,IAAM,yBAAyB,oBAAI,IAAI,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG,CAAC;AAE9D,IAAM,oBAAN,MAAwB;AAAA,EAI7B,YAAY,aAAsB;AAChC,SAAK,cAAc,eAAe;AAElC,SAAK,SAAS,aAAAA,QAAM,OAAO;AAAA,MACzB,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,IACF,CAAC;AAGD,SAAK,OAAO,aAAa,QAAQ,IAAI,CAAC,WAAgB;AACpD,UAAI,KAAK,aAAa;AACpB,eAAO,QAAQ,gBAAgB,UAAU,KAAK,WAAW;AAAA,MAC3D;AACA,aAAO;AAAA,IACT,CAAC;AAGD,SAAK,OAAO,aAAa,SAAS;AAAA,MAChC,CAAC,aAAkB;AAAA,MACnB,CAAC,UAAe;AACd,YAAI,MAAM,UAAU,WAAW,KAAK;AAClC,kBAAQ,MAAM,kDAAkD;AAAA,QAClE,WAAW,MAAM,UAAU,WAAW,KAAK;AACzC,kBAAQ,MAAM,2BAA2B;AAAA,QAC3C,WAAW,MAAM,UAAU,UAAU,KAAK;AACxC,kBAAQ,MAAM,2BAA2B;AAAA,QAC3C;AACA,eAAO,QAAQ,OAAO,KAAK;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,OAAqB;AAClC,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAyB;AACvB,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAiB,UAAkB,SAAsC;AAC7E,UAAM,aAAa,SAAS,WAAW;AACvC,QAAI,UAAU;AACd,QAAI,YAAoC;AAExC,WAAO,WAAW,YAAY;AAC5B,UAAI;AACF,cAAM,SAA6B;AAAA,UACjC,KAAK;AAAA,UACL,QAAQ,SAAS,UAAU;AAAA,UAC3B,MAAM,SAAS;AAAA,UACf,QAAQ,SAAS;AAAA,UACjB,SAAS,SAAS;AAAA,QACpB;AAEA,cAAM,WAA6B,MAAM,KAAK,OAAO,QAAQ,MAAM;AACnE,eAAO,SAAS;AAAA,MAClB,SAAS,OAAY;AACnB,cAAM,SAAS,OAAO,UAAU;AAChC,oBAAY,KAAK,mBAAmB,KAAK;AAGzC,YAAI,YAAY,KAAK,CAAC,KAAK,iBAAiB,MAAM,GAAG;AACnD,gBAAM;AAAA,QACR;AAGA,YAAI,WAAW,KAAK;AAClB,gBAAM,aAAa,KAAK,mBAAmB,MAAM,SAAS,UAAU,aAAa,CAAC;AAClF,kBAAQ,KAAK,4CAA4C,UAAU,eAAe,UAAU,CAAC,IAAI,aAAa,CAAC,GAAG;AAClH,gBAAM,KAAK,MAAM,UAAU;AAAA,QAC7B,WAAW,KAAK,iBAAiB,MAAM,GAAG;AAExC,gBAAM,QAAQ,KAAK,IAAI,MAAO,KAAK,IAAI,GAAG,OAAO,GAAG,GAAK,IAAI,KAAK,OAAO,IAAI;AAC7E,kBAAQ,KAAK,qBAAqB,MAAM,oBAAoB,KAAK,