import type { SpotifyDevice, SpotifyDevicesResponse, SpotifyPlaybackState, SpotifyPlaybackSnapshot, SpotifyError, SpotifyTrack, } from './types'; const SPOTIFY_API_BASE = 'https://api.spotify.com/v1'; export class SpotifyWebApiClient { private accessToken: string; constructor(accessToken: string) { this.accessToken = accessToken; } private async request( endpoint: string, options: RequestInit = {} ): Promise { const url = `${SPOTIFY_API_BASE}${endpoint}`; const response = await fetch(url, { ...options, headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { try { const error: SpotifyError = await response.json(); throw new Error(`Spotify API Error: ${error.error?.message || 'Unknown error'}`); } catch (parseError) { throw new Error(`Spotify API Error: ${response.status} ${response.statusText}`); } } if (response.status === 204 || response.status === 202) { return {} as T; } const contentLength = response.headers.get('content-length'); if (contentLength === '0') { return {} as T; } const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { console.warn('Non-JSON response from Spotify API:', contentType, response.status); return {} as T; } try { return await response.json(); } catch (error) { console.warn('Failed to parse Spotify API JSON response:', error); return {} as T; } } async getDevices(): Promise { const response = await this.request('/me/player/devices'); return response.devices; } async transferPlayback(deviceId: string, play: boolean = false): Promise { await this.request('/me/player', { method: 'PUT', body: JSON.stringify({ device_ids: [deviceId], play, }), }); } async getPlaybackState(): Promise { try { return await this.request('/me/player'); } catch (error) { if (error instanceof Error && error.message.includes('204')) { return null; } throw error; } } async play(deviceId?: string): Promise { const endpoint = deviceId ? `/me/player/play?device_id=${deviceId}` : '/me/player/play'; await this.request(endpoint, { method: 'PUT', }); } async playTrack(trackId: string, deviceId?: string, positionMs?: number): Promise { const endpoint = deviceId ? `/me/player/play?device_id=${deviceId}` : '/me/player/play'; await this.request(endpoint, { method: 'PUT', body: JSON.stringify({ uris: [`spotify:track:${trackId}`], position_ms: positionMs || 0, }), }); } async pause(): Promise { await this.request('/me/player/pause', { method: 'PUT', }); } async skipToNext(): Promise { await this.request('/me/player/next', { method: 'POST', }); } async skipToPrevious(): Promise { await this.request('/me/player/previous', { method: 'POST', }); } async setVolume(volumePercent: number): Promise { await this.request(`/me/player/volume?volume_percent=${volumePercent}`, { method: 'PUT', }); } async getCurrentUser(): Promise<{ id: string; display_name: string }> { return this.request<{ id: string; display_name: string }>('/me'); } async searchTracks(query: string, limit: number = 20): Promise<{ tracks: { items: SpotifyTrack[] } }> { const encodedQuery = encodeURIComponent(query); return this.request<{ tracks: { items: SpotifyTrack[] } }>(`/search?q=${encodedQuery}&type=track&limit=${limit}`); } updateAccessToken(accessToken: string): void { this.accessToken = accessToken; } async createPlaybackSnapshot(): Promise { const state = await this.getPlaybackState(); if (!state || !state.item) { return null; } const snapshot: SpotifyPlaybackSnapshot = { contextUri: state.context?.uri, trackUri: state.item.external_urls.spotify, trackId: state.item.id, positionMs: state.progress_ms || 0, shuffleState: state.shuffle_state, repeatState: state.repeat_state, volumePercent: state.device.volume_percent || undefined, deviceId: state.device.id, timestamp: Date.now(), isPlaying: state.is_playing, trackName: state.item.name, artistName: state.item.artists.map(a => a.name).join(', '), albumName: state.item.album.name, albumImageUrl: state.item.album.images[2]?.url, }; return snapshot; } async restorePlaybackSnapshot(snapshot: SpotifyPlaybackSnapshot): Promise { if (snapshot.deviceId) { await this.transferPlayback(snapshot.deviceId, false); await new Promise(resolve => setTimeout(resolve, 500)); } const playbackRequest: any = { position_ms: snapshot.positionMs, }; if (snapshot.contextUri && snapshot.trackId) { playbackRequest.context_uri = snapshot.contextUri; playbackRequest.offset = { uri: `spotify:track:${snapshot.trackId}` }; } else if (snapshot.trackId) { playbackRequest.uris = [`spotify:track:${snapshot.trackId}`]; } const endpoint = snapshot.deviceId ? `/me/player/play?device_id=${snapshot.deviceId}` : '/me/player/play'; await this.request(endpoint, { method: 'PUT', body: JSON.stringify(playbackRequest), }); await Promise.all([ this.request(`/me/player/shuffle?state=${snapshot.shuffleState}`, { method: 'PUT', }), this.request(`/me/player/repeat?state=${snapshot.repeatState}`, { method: 'PUT', }), snapshot.volumePercent !== undefined ? this.setVolume(snapshot.volumePercent) : Promise.resolve(), ]); if (!snapshot.isPlaying) { await new Promise(resolve => setTimeout(resolve, 1000)); await this.pause(); } } }