'use client' import React, { createContext, useContext, useCallback, useEffect, useRef, useState } from 'react' import type { Track } from '@/components/music/music-provider' import { saveTrack, removeTrack as removeFromDB, listOfflineIds, getTotalSize, clearAll as clearDB, listOfflineTracks, } from '@/lib/offline-db' type DownloadStatus = 'idle' | 'queued' | 'downloading' interface OfflineContextValue { offlineIds: Set queue: Track[] activeDownloadId: string | null storageUsed: number download: (track: Track) => void remove: (trackId: string) => void clearAll: () => void getStatus: (trackId: string) => DownloadStatus sync: () => Promise offlineTracks: Track[] loading: boolean } const OfflineContext = createContext(null) export function useOffline() { const ctx = useContext(OfflineContext) if (!ctx) throw new Error('useOffline must be used within OfflineProvider') return ctx } export function OfflineProvider({ children }: { children: React.ReactNode }) { const [offlineIds, setOfflineIds] = useState>(new Set()) const [offlineTracks, setOfflineTracks] = useState([]) const [queue, setQueue] = useState([]) const [activeDownloadId, setActiveDownloadId] = useState(null) const [storageUsed, setStorageUsed] = useState(0) const [loading, setLoading] = useState(true) const processingRef = useRef(false) // Mirror queue in a ref for access in async loops const queueRef = useRef([]) const refreshLocal = useCallback(async () => { const [ids, tracks, size] = await Promise.all([ listOfflineIds(), listOfflineTracks(), getTotalSize(), ]) setOfflineIds(new Set(ids)) setOfflineTracks(tracks) setStorageUsed(size) }, []) const updateQueue = useCallback((updater: (prev: Track[]) => Track[]) => { setQueue((prev) => { const next = updater(prev) queueRef.current = next return next }) }, []) const processQueue = useCallback(async () => { if (processingRef.current) return processingRef.current = true while (queueRef.current.length > 0) { const nextTrack = queueRef.current[0] setActiveDownloadId(nextTrack.id) try { const res = await fetch(`/api/music/stream/${nextTrack.id}`) if (!res.ok) throw new Error(`Stream failed: ${res.status}`) const blob = await res.blob() await saveTrack(nextTrack.id, blob, nextTrack) // Sync to server playlist (non-critical) await fetch('/api/music/offline', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ songId: nextTrack.id }), }).catch(() => {}) updateQueue((prev) => prev.filter((t) => t.id !== nextTrack.id)) await refreshLocal() } catch (err) { console.error(`Failed to download ${nextTrack.id}:`, err) updateQueue((prev) => prev.filter((t) => t.id !== nextTrack.id)) } } setActiveDownloadId(null) processingRef.current = false }, [refreshLocal, updateQueue]) const sync = useCallback(async () => { try { setLoading(true) const res = await fetch('/api/music/offline') if (!res.ok) return const { songs } = await res.json() as { songs: Track[] } const localIds = await listOfflineIds() const localSet = new Set(localIds) const serverIds = new Set(songs.map((s: Track) => s.id)) // Download songs on server but not local const toDownload = songs.filter((s: Track) => !localSet.has(s.id)) if (toDownload.length > 0) { updateQueue((prev) => { const existingIds = new Set(prev.map((t) => t.id)) return [...prev, ...toDownload.filter((t: Track) => !existingIds.has(t.id))] }) } // Remove local songs deleted from server for (const localId of localIds) { if (!serverIds.has(localId)) { await removeFromDB(localId) } } await refreshLocal() } catch (err) { console.error('Offline sync error:', err) } finally { setLoading(false) } }, [refreshLocal, updateQueue]) // Initial load + sync useEffect(() => { refreshLocal().then(() => sync()) }, [refreshLocal, sync]) // Process queue when items are added useEffect(() => { if (queue.length > 0 && !processingRef.current) { processQueue() } }, [queue, processQueue]) const download = useCallback((track: Track) => { updateQueue((prev) => { if (prev.some((t) => t.id === track.id)) return prev return [...prev, track] }) }, [updateQueue]) const remove = useCallback(async (trackId: string) => { await removeFromDB(trackId) await fetch('/api/music/offline', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ songId: trackId }), }).catch(() => {}) await refreshLocal() }, [refreshLocal]) const clearAllOffline = useCallback(async () => { await clearDB() await refreshLocal() }, [refreshLocal]) const getStatus = useCallback((trackId: string): DownloadStatus => { if (offlineIds.has(trackId)) return 'idle' if (activeDownloadId === trackId) return 'downloading' if (queue.some((t) => t.id === trackId)) return 'queued' return 'idle' }, [offlineIds, activeDownloadId, queue]) return ( {children} ) }