import type { APIRoute } from 'astro'; // Helper function to refresh the access token async function refreshSpotifyToken(refreshToken: string, clientId: string, clientSecret: string) { const response = await fetch('https://accounts.spotify.com/api/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`, }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, }), }); if (!response.ok) { throw new Error('Failed to refresh token'); } return await response.json(); } // Function to fetch current track from Spotify async function fetchCurrentTrack() { try { const clientId = import.meta.env.SPOTIFY_CLIENT_ID; const clientSecret = import.meta.env.SPOTIFY_CLIENT_SECRET; let accessToken = import.meta.env.SPOTIFY_ACCESS_TOKEN; const refreshToken = import.meta.env.SPOTIFY_REFRESH_TOKEN; if (!clientId || !clientSecret || !refreshToken) { return null; } // Try to fetch current track with existing token let spotifyResponse = await fetch('https://api.spotify.com/v1/me/player/currently-playing', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); // If token is expired (401), refresh it if (spotifyResponse.status === 401) { try { const tokenData = await refreshSpotifyToken(refreshToken, clientId, clientSecret); accessToken = tokenData.access_token; // Retry the request with new token spotifyResponse = await fetch('https://api.spotify.com/v1/me/player/currently-playing', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); } catch (refreshError) { console.error('Failed to refresh token:', refreshError); return null; } } if (spotifyResponse.status === 204) { // Nothing is currently playing return { is_playing: false, item: null }; } if (!spotifyResponse.ok) { return null; } const data = await spotifyResponse.json(); return { is_playing: data.is_playing, item: data.item ? { name: data.item.name, artists: data.item.artists, is_playing: data.is_playing, external_urls: data.item.external_urls } : null }; } catch (error) { console.error('Spotify API Error:', error); return null; } } export const GET: APIRoute = async ({ request }) => { // Set up Server-Sent Events const encoder = new TextEncoder(); let controller: ReadableStreamDefaultController; let isClosed = false; let pollInterval: NodeJS.Timeout | null = null; const stream = new ReadableStream({ start(ctrl) { controller = ctrl; }, cancel() { // Client disconnected console.log('SSE stream cancelled by client'); isClosed = true; if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } } }); // Function to send SSE message const sendMessage = (data: any) => { if (isClosed) { return; // Don't try to send if stream is closed } try { const message = `data: ${JSON.stringify(data)}\n\n`; controller.enqueue(encoder.encode(message)); } catch (error) { if (error instanceof TypeError && error.message.includes('Controller is already closed')) { console.log('SSE controller is closed, stopping polling'); isClosed = true; if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } } else { console.error('Error sending SSE message:', error); } } }; // Start polling and sending updates let lastTrackData: any = null; const poll = async () => { if (isClosed) { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } return; } try { const currentTrack = await fetchCurrentTrack(); // Only send if data has changed and stream is still open if (!isClosed && JSON.stringify(currentTrack) !== JSON.stringify(lastTrackData)) { lastTrackData = currentTrack; sendMessage(currentTrack || { is_playing: false, item: null }); } } catch (error) { if (!isClosed) { console.error('Polling error:', error); } } }; // Send initial data poll(); // Poll every 3 seconds pollInterval = setInterval(poll, 3000); // Clean up when client disconnects (abort signal) request.signal.addEventListener('abort', () => { console.log('SSE request aborted'); isClosed = true; if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Cache-Control', }, }); };