import { createContext, useCallback, useEffect, useState, PropsWithChildren } from 'react'; import { SpotifyAuth } from '@/lib/spotify/auth'; import { useAuth } from './auth-context'; import type { SpotifyContextType, SpotifyAuthState, SpotifyDevice, SpotifyPlaybackState, SpotifyTrack, } from '@/lib/spotify/types'; const defaultSpotifyState: SpotifyAuthState = { isAuthenticated: false, accessToken: null, refreshToken: null, expiresAt: null, scopes: [], }; export const SpotifyContext = createContext(null); export const SpotifyProvider: React.FC = ({ children }) => { const { roles } = useAuth(); const isAdmin = roles?.includes('Admin') || false; const [authState, setAuthState] = useState(defaultSpotifyState); const [currentTrack, setCurrentTrack] = useState(null); const [playbackState, setPlaybackState] = useState(null); const [devices, setDevices] = useState([]); const [activeDevice, setActiveDeviceState] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const spotifyAuth = new SpotifyAuth( import.meta.env.VITE_SPOTIFY_CLIENT_ID!, import.meta.env.VITE_SPOTIFY_REDIRECT_URI! ); useEffect(() => { if (isAdmin) { checkExistingAuth(); handleOAuthCallback(); } }, [isAdmin]); const checkExistingAuth = async () => { try { const response = await fetch('/api/spotify/token', { credentials: 'include', }); if (response.ok) { const data = await response.json(); if (data.access_token) { setAuthState({ isAuthenticated: true, accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date.now() + (3600 * 1000), scopes: [], }); // Load initial data await Promise.all([getDevices(), refreshPlaybackState()]); } } } catch (error) { console.error('Failed to check existing auth:', error); } }; const handleOAuthCallback = () => { const urlParams = new URLSearchParams(window.location.search); const spotifyAuth = urlParams.get('spotify_auth'); const error = urlParams.get('spotify_error'); const details = urlParams.get('details'); if (spotifyAuth === 'success') { checkExistingAuth(); const newUrl = new URL(window.location.href); newUrl.searchParams.delete('spotify_auth'); newUrl.searchParams.delete('state'); window.history.replaceState({}, '', newUrl.toString()); } if (error) { let errorMessage = `Authentication failed: ${error}`; if (details) { errorMessage += ` - ${decodeURIComponent(details)}`; } setError(errorMessage); console.error('Spotify OAuth Error:', { error, details }); const newUrl = new URL(window.location.href); newUrl.searchParams.delete('spotify_error'); newUrl.searchParams.delete('details'); window.history.replaceState({}, '', newUrl.toString()); } }; const login = useCallback(() => { if (!isAdmin) return; spotifyAuth.startAuthFlow(window.location.pathname); }, [isAdmin, spotifyAuth]); const logout = useCallback(() => { setAuthState(defaultSpotifyState); setCurrentTrack(null); setPlaybackState(null); setDevices([]); setActiveDeviceState(null); setError(null); document.cookie = 'spotify_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; document.cookie = 'spotify_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; }, []); const makeSpotifyRequest = async (endpoint: string, options: RequestInit = {}) => { const response = await fetch(`/api/spotify/${endpoint}`, { ...options, credentials: 'include', headers: { 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { let errorMessage = 'Request failed'; try { const errorData = await response.json(); errorMessage = errorData.error || errorMessage; } catch { errorMessage = `HTTP ${response.status}: ${response.statusText}`; } throw new Error(errorMessage); } if (response.status === 204 || response.headers.get('content-length') === '0') { return {}; } const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json')) { console.warn(`Non-JSON response from ${endpoint}:`, contentType); return {}; } try { return await response.json(); } catch (error) { console.warn(`Failed to parse JSON response from ${endpoint}:`, error); return {}; } }; const play = useCallback(async (deviceId?: string) => { if (!authState.isAuthenticated) return; setIsLoading(true); setError(null); try { await makeSpotifyRequest('playback', { method: 'POST', body: JSON.stringify({ action: 'play', deviceId }), }); setTimeout(refreshPlaybackState, 500); } catch (error) { if (error instanceof Error && !error.message.includes('JSON')) { setError(error.message); } console.warn('Playback action completed with warning:', error); } finally { setIsLoading(false); } }, [authState.isAuthenticated]); const pause = useCallback(async () => { if (!authState.isAuthenticated) return; setIsLoading(true); setError(null); try { await makeSpotifyRequest('playback', { method: 'POST', body: JSON.stringify({ action: 'pause' }), }); setTimeout(refreshPlaybackState, 500); } catch (error) { if (error instanceof Error && !error.message.includes('JSON')) { setError(error.message); } console.warn('Playback action completed with warning:', error); } finally { setIsLoading(false); } }, [authState.isAuthenticated]); const skipNext = useCallback(async () => { if (!authState.isAuthenticated) return; setIsLoading(true); setError(null); try { await makeSpotifyRequest('playback', { method: 'POST', body: JSON.stringify({ action: 'next' }), }); setTimeout(refreshPlaybackState, 500); } catch (error) { if (error instanceof Error && !error.message.includes('JSON')) { setError(error.message); } console.warn('Playback action completed with warning:', error); } finally { setIsLoading(false); } }, [authState.isAuthenticated]); const skipPrevious = useCallback(async () => { if (!authState.isAuthenticated) return; setIsLoading(true); setError(null); try { await makeSpotifyRequest('playback', { method: 'POST', body: JSON.stringify({ action: 'previous' }), }); setTimeout(refreshPlaybackState, 500); } catch (error) { if (error instanceof Error && !error.message.includes('JSON')) { setError(error.message); } console.warn('Playback action completed with warning:', error); } finally { setIsLoading(false); } }, [authState.isAuthenticated]); const setVolume = useCallback(async (volumePercent: number) => { if (!authState.isAuthenticated) return; setIsLoading(true); setError(null); try { await makeSpotifyRequest('playback', { method: 'POST', body: JSON.stringify({ action: 'volume', volumePercent }), }); } catch (error) { setError(error instanceof Error ? error.message : 'Failed to set volume'); } finally { setIsLoading(false); } }, [authState.isAuthenticated]); const getDevices = useCallback(async () => { if (!authState.isAuthenticated) return; setIsLoading(true); setError(null); try { const data = await makeSpotifyRequest('playback?type=devices'); setDevices(data.devices || []); const active = data.devices?.find((d: SpotifyDevice) => d.is_active); if (active) { setActiveDeviceState(active); } } catch (error) { setError(error instanceof Error ? error.message : 'Failed to get devices'); } finally { setIsLoading(false); } }, [authState.isAuthenticated]); const setActiveDevice = useCallback(async (deviceId: string) => { if (!authState.isAuthenticated) return; setIsLoading(true); setError(null); try { await makeSpotifyRequest('playback', { method: 'POST', body: JSON.stringify({ action: 'transfer', deviceId }), }); const device = devices.find(d => d.id === deviceId); if (device) { setActiveDeviceState(device); } setTimeout(getDevices, 1000); } catch (error) { setError(error instanceof Error ? error.message : 'Failed to set active device'); } finally { setIsLoading(false); } }, [authState.isAuthenticated, devices]); const refreshPlaybackState = useCallback(async () => { if (!authState.isAuthenticated) return; try { const data = await makeSpotifyRequest('playback?type=state'); const state = data.playbackState; setPlaybackState(state); setCurrentTrack(state?.item || null); if (state?.device) { setActiveDeviceState(state.device); } } catch (error) { console.warn('Failed to refresh playback state:', error); } }, [authState.isAuthenticated]); useEffect(() => { if (!authState.isAuthenticated) return; const interval = setInterval(refreshPlaybackState, 5000); return () => clearInterval(interval); }, [authState.isAuthenticated, refreshPlaybackState]); const contextValue: SpotifyContextType = { ...authState, currentTrack, playbackState, devices, activeDevice, isLoading, error, login, logout, play, pause, skipNext, skipPrevious, setVolume, getDevices, setActiveDevice, refreshPlaybackState, }; if (!isAdmin) { return <>{children}; } return ( {children} ); };