From 9dbdd265ba98a46209004a2cff13c538b1ad2e82 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Fri, 27 Jun 2025 15:44:43 -0600 Subject: [PATCH] Removed spotify... its hard to maintain and also fuck spotify --- .env.example | 4 - astro.config.mjs | 1 - docker-compose.yml | 4 - src/components/SocialLinks.astro | 43 ++--- src/components/SpotifyIcon.tsx | 207 ------------------------ src/config/data.ts | 9 -- src/pages/api/spotify/config.ts | 48 ------ src/pages/api/spotify/stream.ts | 268 ------------------------------- src/types/index.ts | 2 +- src/utils/spotify.ts | 41 ----- 10 files changed, 16 insertions(+), 611 deletions(-) delete mode 100644 .env.example delete mode 100644 src/components/SpotifyIcon.tsx delete mode 100644 src/pages/api/spotify/config.ts delete mode 100644 src/pages/api/spotify/stream.ts delete mode 100644 src/utils/spotify.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index 8c8dfed..0000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -SPOTIFY_CLIENT_ID=your_client_id_here -SPOTIFY_CLIENT_SECRET=your_client_secret_here -SPOTIFY_ACCESS_TOKEN=BQA... -SPOTIFY_REFRESH_TOKEN=AQA... \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs index 68582ab..17e5128 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -42,7 +42,6 @@ export default defineConfig({ "arrow-left", ], "simple-icons": [ - "spotify", "gitea", "bluesky", "react", diff --git a/docker-compose.yml b/docker-compose.yml index f85fef5..438bdf1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,4 @@ services: - "${APP_PORT}:4321" environment: NODE_ENV: production - SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID} - SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET} - SPOTIFY_REFRESH_TOKEN: ${SPOTIFY_REFRESH_TOKEN} - SPOTIFY_ACCESS_TOKEN: ${SPOTIFY_ACCESS_TOKEN} restart: unless-stopped diff --git a/src/components/SocialLinks.astro b/src/components/SocialLinks.astro index 710bda6..b291a4c 100644 --- a/src/components/SocialLinks.astro +++ b/src/components/SocialLinks.astro @@ -1,44 +1,31 @@ --- import { Icon } from "astro-icon/components"; -import SpotifyIcon from "./SpotifyIcon"; import { socialLinks } from "../config/data"; // Helper function to check if icon is a string (Astro icon) function isAstroIcon(icon: any): icon is string { return typeof icon === "string"; } - -// Helper function to check if icon is SpotifyIcon component -function isSpotifyIcon(icon: any): boolean { - return icon === SpotifyIcon; -} ---
{ socialLinks.map((link) => { - if (isSpotifyIcon(link.icon)) { - return ; - } else if (isAstroIcon(link.icon)) { - return ( - - - - ); - } - return null; + return ( + + + + ); }) }
diff --git a/src/components/SpotifyIcon.tsx b/src/components/SpotifyIcon.tsx deleted file mode 100644 index 562b6a7..0000000 --- a/src/components/SpotifyIcon.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { useSignal } from "@preact/signals"; -import { useEffect } from "preact/hooks"; - -interface SpotifyTrack { - name: string; - artists: { name: string }[]; - is_playing: boolean; - external_urls?: { - spotify: string; - }; -} - -interface SpotifyResponse { - is_playing: boolean; - item: SpotifyTrack; -} - -interface SpotifyIconProps { - profileUrl?: string; -} - -// Spotify SVG icon component -const SpotifySVG = ({ className }: { className?: string }) => ( - - - -); - -export default function SpotifyIcon({ - profileUrl = "https://open.spotify.com", -}: SpotifyIconProps) { - const currentTrack = useSignal(null); - const isPlaying = useSignal(false); - const isDynamicEnabled = useSignal(false); - const hasLoggedError = useSignal(false); - - useEffect(() => { - // First, check if Spotify is properly configured - const checkConfiguration = async () => { - try { - const response = await fetch("/api/spotify/config"); - if (!response.ok) { - 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.", - ); - hasLoggedError.value = true; - } - isDynamicEnabled.value = false; - return; - } - isDynamicEnabled.value = true; - initializeSpotifyConnection(); - } catch (error: any) { - if (!hasLoggedError.value) { - console.error( - "[Spotify] Could not enable dynamic features:", - error?.message || error, - ); - hasLoggedError.value = true; - } - isDynamicEnabled.value = false; - } - }; - - checkConfiguration(); - }, []); - - const initializeSpotifyConnection = () => { - // Set up Server-Sent Events connection - 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."); - } - }; - - eventSource.onmessage = (event) => { - try { - 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, - "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.", - { - readyState: eventSource.readyState, - url: eventSource.url, - event: event, - }, - ); - hasLoggedError.value = true; - } - - // 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(); - }; - }; - - const getTooltipText = () => { - if (isDynamicEnabled.value && currentTrack.value && isPlaying.value) { - const artists = currentTrack.value.artists - .map((artist) => artist.name) - .join(", "); - return `♪ ${currentTrack.value.name} by ${artists} (click to open in Spotify)`; - } - return "Spotify"; - }; - - 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 - ) { - return currentTrack.value.external_urls.spotify; - } - // Otherwise, use the provided profile URL or fallback to general Spotify - return profileUrl; - }; - - return ( -
- -
- -
- {isDynamicEnabled.value && isPlaying.value && ( -
- )} -
-
- ); -} diff --git a/src/config/data.ts b/src/config/data.ts index 4efe55b..a6e630b 100644 --- a/src/config/data.ts +++ b/src/config/data.ts @@ -20,8 +20,6 @@ import { Megaphone, } from "lucide-preact"; -import SpotifyIcon from "../components/SpotifyIcon"; - import logo from "../assets/logo_real.webp"; // Astro Icon references @@ -222,13 +220,6 @@ export const socialLinks: SocialLink[] = [ icon: BLUESKY_ICON, ariaLabel: "Bluesky Profile", }, - { - id: "spotify", - name: "Spotify", - url: "https://open.spotify.com/user/31pjwuuqwnn5zr7fnhfjjmi7c4bi?si=1be2bfdc844c4d85", - icon: SpotifyIcon, - ariaLabel: "Spotify Profile", - }, ]; export const techLinks: TechLink[] = [ diff --git a/src/pages/api/spotify/config.ts b/src/pages/api/spotify/config.ts deleted file mode 100644 index 5ae7ee8..0000000 --- a/src/pages/api/spotify/config.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { APIRoute } from "astro"; -import { - getSpotifyCredentials, - isSpotifyConfigured, -} from "../../../utils/spotify"; - -export const GET: APIRoute = async () => { - try { - const isConfigured = isSpotifyConfigured(); - - if (!isConfigured) { - const credentials = getSpotifyCredentials(); - console.log( - "Spotify integration disabled - missing environment variables:", - { - hasClientId: !!credentials?.clientId, - hasClientSecret: !!credentials?.clientSecret, - hasRefreshToken: !!credentials?.refreshToken, - }, - ); - } - - return new Response( - JSON.stringify({ - configured: isConfigured, - }), - { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }, - ); - } catch (error) { - console.error("Error checking Spotify configuration:", error); - return new Response( - JSON.stringify({ - configured: false, - }), - { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }, - ); - } -}; diff --git a/src/pages/api/spotify/stream.ts b/src/pages/api/spotify/stream.ts deleted file mode 100644 index d0eb5a5..0000000 --- a/src/pages/api/spotify/stream.ts +++ /dev/null @@ -1,268 +0,0 @@ -import type { APIRoute } from "astro"; -import { getSpotifyCredentials } from "../../../utils/spotify"; - -// 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( - clientId: string, - clientSecret: string, - refreshToken: string, - accessToken?: string, -) { - try { - let currentAccessToken = accessToken; - - // Try to fetch current track with existing token - let spotifyResponse = await fetch( - "https://api.spotify.com/v1/me/player/currently-playing", - { - headers: { - Authorization: `Bearer ${currentAccessToken}`, - "Content-Type": "application/json", - }, - }, - ); - - // If token is expired (401), refresh it - if (spotifyResponse.status === 401) { - try { - const tokenData = await refreshSpotifyToken( - refreshToken, - clientId, - clientSecret, - ); - currentAccessToken = tokenData.access_token; - - // Retry the request with new token - spotifyResponse = await fetch( - "https://api.spotify.com/v1/me/player/currently-playing", - { - headers: { - Authorization: `Bearer ${currentAccessToken}`, - "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 }) => { - try { - // Get Spotify credentials - const credentials = getSpotifyCredentials(); - - if (!credentials) { - console.log( - "Spotify SSE stream disabled - missing environment variables", - ); - return new Response(JSON.stringify({ error: "Spotify not configured" }), { - status: 503, - headers: { - "Content-Type": "application/json", - }, - }); - } - - const { clientId, clientSecret, refreshToken, accessToken } = credentials; - - // 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; - // 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; - cleanup(); - }, - }); - - // 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( - clientId, - clientSecret, - refreshToken, - accessToken, - ); - - // Only send if data has changed and stream is still open - if ( - !isClosed && - JSON.stringify(currentTrack) !== JSON.stringify(lastTrackData) - ) { - lastTrackData = currentTrack; - sendMessage({ - type: "track", - data: currentTrack || { is_playing: false, item: null }, - timestamp: Date.now(), - }); - } - } catch (error) { - if (!isClosed) { - console.error("Polling error:", error); - } - } - }; - - // Send initial data immediately - poll(); - - // Poll every 3 seconds - pollInterval = setInterval(poll, 3000); - - // 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, 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) { - console.error("Error setting up Spotify SSE stream:", error); - return new Response( - JSON.stringify({ error: "Failed to initialize stream" }), - { - status: 500, - headers: { - "Content-Type": "application/json", - }, - }, - ); - } -}; diff --git a/src/types/index.ts b/src/types/index.ts index 44e3f14..26a893f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,7 +4,7 @@ import type { ComponentType } from "preact"; // Icon Types export type LucideIcon = ComponentType<{ size?: number; class?: string }>; export type AstroIconName = string; // For astro-icon string references like "mdi:email" -export type CustomIconComponent = ComponentType; // For custom components like SpotifyIcon +export type CustomIconComponent = ComponentType; export type IconType = LucideIcon | AstroIconName | CustomIconComponent; export interface Talk { diff --git a/src/utils/spotify.ts b/src/utils/spotify.ts deleted file mode 100644 index 80cde39..0000000 --- a/src/utils/spotify.ts +++ /dev/null @@ -1,41 +0,0 @@ -interface SpotifyCredentials { - clientId: string; - clientSecret: string; - refreshToken: string; - accessToken?: string; -} - -/** - * Get Spotify credentials from environment variables - * Checks both process.env and import.meta.env for compatibility - */ -export function getSpotifyCredentials(): SpotifyCredentials | null { - const clientId = - process.env.SPOTIFY_CLIENT_ID || import.meta.env.SPOTIFY_CLIENT_ID; - const clientSecret = - process.env.SPOTIFY_CLIENT_SECRET || - import.meta.env.SPOTIFY_CLIENT_SECRET; - const refreshToken = - process.env.SPOTIFY_REFRESH_TOKEN || - import.meta.env.SPOTIFY_REFRESH_TOKEN; - const accessToken = - process.env.SPOTIFY_ACCESS_TOKEN || import.meta.env.SPOTIFY_ACCESS_TOKEN; - - if (!clientId || !clientSecret || !refreshToken) { - return null; - } - - return { - clientId, - clientSecret, - refreshToken, - accessToken, - }; -} - -/** - * Check if Spotify integration is properly configured - */ -export function isSpotifyConfigured(): boolean { - return getSpotifyCredentials() !== null; -}