(null);
const isPlaying = useSignal(false);
const isDynamicEnabled = useSignal(false);
@@ -42,15 +44,19 @@ export default function SpotifyIcon({ profileUrl = "https://open.spotify.com" }:
// First, check if Spotify is properly configured
const checkConfiguration = async () => {
try {
- const response = await fetch('/api/spotify/config');
+ const response = await fetch("/api/spotify/config");
if (!response.ok) {
- throw new Error(`Spotify config endpoint returned status ${response.status}`);
+ throw new Error(
+ `Spotify config endpoint returned status ${response.status}`,
+ );
}
const { configured } = await response.json();
-
+
if (!configured) {
if (!hasLoggedError.value) {
- console.warn('[Spotify] Dynamic features disabled: missing or invalid environment variables.');
+ console.warn(
+ "[Spotify] Dynamic features disabled: missing or invalid environment variables.",
+ );
hasLoggedError.value = true;
}
isDynamicEnabled.value = false;
@@ -60,7 +66,10 @@ export default function SpotifyIcon({ profileUrl = "https://open.spotify.com" }:
initializeSpotifyConnection();
} catch (error: any) {
if (!hasLoggedError.value) {
- console.error('[Spotify] Could not enable dynamic features:', error?.message || error);
+ console.error(
+ "[Spotify] Could not enable dynamic features:",
+ error?.message || error,
+ );
hasLoggedError.value = true;
}
isDynamicEnabled.value = false;
@@ -72,42 +81,71 @@ export default function SpotifyIcon({ profileUrl = "https://open.spotify.com" }:
const initializeSpotifyConnection = () => {
// Set up Server-Sent Events connection
- const eventSource = new EventSource('/api/spotify/stream');
-
+ const eventSource = new EventSource("/api/spotify/stream");
+
eventSource.onopen = () => {
// Only log on first open
if (!hasLoggedError.value) {
- console.info('[Spotify] SSE connected for dynamic icon.');
+ console.info("[Spotify] SSE connected for dynamic icon.");
}
};
-
+
eventSource.onmessage = (event) => {
try {
- const data: SpotifyResponse = JSON.parse(event.data);
- if (data.is_playing && data.item) {
- currentTrack.value = data.item;
- isPlaying.value = data.is_playing;
- } else {
- currentTrack.value = null;
- isPlaying.value = false;
+ const message = JSON.parse(event.data);
+
+ // Handle different message types
+ if (message.type === "keepalive") {
+ // Just a keepalive, no need to do anything
+ return;
+ }
+
+ if (message.type === "track" && message.data) {
+ const data: SpotifyResponse = message.data;
+ if (data.is_playing && data.item) {
+ currentTrack.value = data.item;
+ isPlaying.value = data.is_playing;
+ } else {
+ currentTrack.value = null;
+ isPlaying.value = false;
+ }
}
} catch (err) {
if (!hasLoggedError.value) {
- console.error('[Spotify] Error parsing SSE data:', err);
+ console.error(
+ "[Spotify] Error parsing SSE data:",
+ err,
+ "Raw data:",
+ event.data,
+ );
hasLoggedError.value = true;
}
// Fail silently - will revert to static mode
}
};
-
+
eventSource.onerror = (event) => {
if (!hasLoggedError.value) {
- console.error('[Spotify] SSE connection error. Falling back to static icon.');
+ console.error(
+ "[Spotify] SSE connection error. Falling back to static icon.",
+ {
+ readyState: eventSource.readyState,
+ url: eventSource.url,
+ event: event,
+ },
+ );
hasLoggedError.value = true;
}
- // If SSE fails, we just fall back to static mode
+
+ // Close the connection to prevent retries
+ eventSource.close();
+
+ // Set to static mode
+ isDynamicEnabled.value = false;
+ currentTrack.value = null;
+ isPlaying.value = false;
};
-
+
// Cleanup on unmount
return () => {
eventSource.close();
@@ -116,7 +154,9 @@ export default function SpotifyIcon({ profileUrl = "https://open.spotify.com" }:
const getTooltipText = () => {
if (isDynamicEnabled.value && currentTrack.value && isPlaying.value) {
- const artists = currentTrack.value.artists.map(artist => artist.name).join(', ');
+ const artists = currentTrack.value.artists
+ .map((artist) => artist.name)
+ .join(", ");
return `♪ ${currentTrack.value.name} by ${artists} (click to open in Spotify)`;
}
return "Spotify";
@@ -124,7 +164,12 @@ export default function SpotifyIcon({ profileUrl = "https://open.spotify.com" }:
const getSpotifyUrl = () => {
// If we have a current track with external URL, use that
- if (isDynamicEnabled.value && currentTrack.value && isPlaying.value && currentTrack.value.external_urls?.spotify) {
+ if (
+ isDynamicEnabled.value &&
+ currentTrack.value &&
+ isPlaying.value &&
+ currentTrack.value.external_urls?.spotify
+ ) {
return currentTrack.value.external_urls.spotify;
}
// Otherwise, use the provided profile URL or fallback to general Spotify
@@ -140,12 +185,15 @@ export default function SpotifyIcon({ profileUrl = "https://open.spotify.com" }:
aria-label="Spotify"
className="hover:text-primary transition-colors relative inline-block"
>
-
@@ -156,4 +204,4 @@ export default function SpotifyIcon({ profileUrl = "https://open.spotify.com" }:
);
-}
\ No newline at end of file
+}
diff --git a/src/pages/api/spotify/stream.ts b/src/pages/api/spotify/stream.ts
index 31379d0..d0eb5a5 100644
--- a/src/pages/api/spotify/stream.ts
+++ b/src/pages/api/spotify/stream.ts
@@ -132,15 +132,15 @@ export const GET: APIRoute = async ({ request }) => {
const stream = new ReadableStream({
start(ctrl) {
controller = ctrl;
+ // Send initial data immediately to establish connection
+ const message = `data: ${JSON.stringify({ type: "connected", timestamp: Date.now() })}\n\n`;
+ controller.enqueue(encoder.encode(message));
},
cancel() {
// Client disconnected
console.log("SSE stream cancelled by client");
isClosed = true;
- if (pollInterval) {
- clearInterval(pollInterval);
- pollInterval = null;
- }
+ cleanup();
},
});
@@ -196,7 +196,11 @@ export const GET: APIRoute = async ({ request }) => {
JSON.stringify(currentTrack) !== JSON.stringify(lastTrackData)
) {
lastTrackData = currentTrack;
- sendMessage(currentTrack || { is_playing: false, item: null });
+ sendMessage({
+ type: "track",
+ data: currentTrack || { is_playing: false, item: null },
+ timestamp: Date.now(),
+ });
}
} catch (error) {
if (!isClosed) {
@@ -205,29 +209,48 @@ export const GET: APIRoute = async ({ request }) => {
}
};
- // Send initial data
+ // Send initial data immediately
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;
+ // Send keepalive every 30 seconds to prevent connection timeout
+ const keepaliveInterval = setInterval(() => {
+ if (!isClosed) {
+ sendMessage({ type: "keepalive", timestamp: Date.now() });
+ }
+ }, 30000);
+
+ // Clean up keepalive interval too
+ const cleanup = () => {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
+ if (keepaliveInterval) {
+ clearInterval(keepaliveInterval);
+ }
+ };
+
+ // Clean up when client disconnects (abort signal)
+ request.signal.addEventListener("abort", () => {
+ console.log("SSE request aborted");
+ isClosed = true;
+ cleanup();
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
- "Cache-Control": "no-cache",
+ "Cache-Control": "no-cache, no-store, must-revalidate",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control",
+ "X-Accel-Buffering": "no",
+ "CF-Cache-Status": "BYPASS",
+ "Transfer-Encoding": "chunked",
+ Vary: "Accept-Encoding",
},
});
} catch (error) {