jedmund-svelte/src/lib/stores/now-playing-stream.ts
Devin AI 1cda37dafb lint: remove unused set functions from store files (6 fixes)
Co-Authored-By: Justin Edmund <justin@jedmund.com>
2025-11-23 14:42:30 +00:00

148 lines
3.6 KiB
TypeScript

import { writable, derived, get, type Readable } from 'svelte/store'
import { browser } from '$app/environment'
interface NowPlayingUpdate {
albumName: string
artistName: string
isNowPlaying: boolean
nowPlayingTrack?: string
}
interface NowPlayingState {
connected: boolean
updates: Map<string, NowPlayingUpdate>
lastUpdate: Date | null
}
function createNowPlayingStream() {
const { subscribe, update } = writable<NowPlayingState>({
connected: false,
updates: new Map(),
lastUpdate: null
})
let eventSource: EventSource | null = null
let reconnectTimeout: NodeJS.Timeout | null = null
let reconnectAttempts = 0
function connect() {
if (!browser || eventSource?.readyState === EventSource.OPEN) return
// Don't connect in Storybook
if (typeof window !== 'undefined' && window.parent !== window) {
// We're in an iframe, likely Storybook
console.log('Now Playing stream disabled in Storybook')
return
}
// Clean up existing connection
disconnect()
eventSource = new EventSource('/api/lastfm/stream')
eventSource.addEventListener('connected', () => {
console.log('Now Playing stream connected')
reconnectAttempts = 0
update((state) => ({ ...state, connected: true }))
})
eventSource.addEventListener('nowplaying', (event) => {
try {
const updates: NowPlayingUpdate[] = JSON.parse(event.data)
update((state) => {
const newUpdates = new Map(state.updates)
for (const album of updates) {
const key = `${album.artistName}:${album.albumName}`
newUpdates.set(key, album)
}
return {
...state,
updates: newUpdates,
lastUpdate: new Date()
}
})
} catch (error) {
console.error('Error parsing now playing update:', error)
}
})
eventSource.addEventListener('heartbeat', () => {
// Heartbeat received, connection is healthy
})
eventSource.addEventListener('error', (error) => {
console.error('Now Playing stream error:', error)
update((state) => ({ ...state, connected: false }))
// Attempt to reconnect with exponential backoff
if (reconnectAttempts < 5) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
reconnectTimeout = setTimeout(() => {
reconnectAttempts++
connect()
}, delay)
}
})
eventSource.addEventListener('open', () => {
update((state) => ({ ...state, connected: true }))
})
}
function disconnect() {
if (eventSource) {
eventSource.close()
eventSource = null
}
if (reconnectTimeout) {
clearTimeout(reconnectTimeout)
reconnectTimeout = null
}
update((state) => ({ ...state, connected: false }))
}
// Auto-connect in browser (but not in admin)
if (browser && !window.location.pathname.startsWith('/admin')) {
connect()
// Reconnect on visibility change
document.addEventListener('visibilitychange', () => {
const currentState = get({ subscribe })
if (
document.visibilityState === 'visible' &&
!currentState.connected &&
!window.location.pathname.startsWith('/admin')
) {
connect()
}
})
}
return {
subscribe,
connect,
disconnect,
// Helper to check if a specific album is now playing
isAlbumPlaying: derived({ subscribe }, ($state) => (artistName: string, albumName: string) => {
const key = `${artistName}:${albumName}`
const update = $state.updates.get(key)
return update
? {
isNowPlaying: update.isNowPlaying,
nowPlayingTrack: update.nowPlayingTrack
}
: null
}) as Readable<
(
artistName: string,
albumName: string
) => { isNowPlaying: boolean; nowPlayingTrack?: string } | null
>
}
}
export const nowPlayingStream = createNowPlayingStream()