'use client'; import { useState, useEffect, useCallback } from 'react'; import { offlineLibraryManager, type OfflineLibraryStats, type SyncOperation } from '@/lib/offline-library'; import { Album, Artist, Song, Playlist } from '@/lib/navidrome'; import { useNavidrome } from '@/app/components/NavidromeContext'; export interface OfflineLibraryState { isInitialized: boolean; isOnline: boolean; isSyncing: boolean; lastSync: Date | null; stats: OfflineLibraryStats; syncProgress: { current: number; total: number; stage: string; } | null; } export function useOfflineLibrary() { const [state, setState] = useState({ isInitialized: false, isOnline: navigator.onLine, isSyncing: false, lastSync: null, stats: { albums: 0, artists: 0, songs: 0, playlists: 0, lastSync: null, pendingOperations: 0, storageSize: 0 }, syncProgress: null }); const { api } = useNavidrome(); // Initialize offline library useEffect(() => { const initializeOfflineLibrary = async () => { try { const initialized = await offlineLibraryManager.initialize(); if (initialized) { const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, isInitialized: true, stats, lastSync: stats.lastSync })); } } catch (error) { console.error('Failed to initialize offline library:', error); } }; initializeOfflineLibrary(); }, []); // Listen for online/offline events useEffect(() => { const handleOnline = () => { setState(prev => ({ ...prev, isOnline: true })); // Automatically sync when back online if (state.isInitialized && api) { syncPendingOperations(); } }; const handleOffline = () => { setState(prev => ({ ...prev, isOnline: false })); }; window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, [state.isInitialized, api]); // Full library sync from server const syncLibraryFromServer = useCallback(async (): Promise => { if (!api || !state.isInitialized || state.isSyncing) return; try { setState(prev => ({ ...prev, isSyncing: true, syncProgress: { current: 0, total: 100, stage: 'Starting sync...' } })); setState(prev => ({ ...prev, syncProgress: { current: 20, total: 100, stage: 'Syncing albums...' } })); await offlineLibraryManager.syncFromServer(api); setState(prev => ({ ...prev, syncProgress: { current: 80, total: 100, stage: 'Syncing pending operations...' } })); await offlineLibraryManager.syncPendingOperations(api); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, isSyncing: false, syncProgress: null, stats, lastSync: stats.lastSync })); } catch (error) { console.error('Library sync failed:', error); setState(prev => ({ ...prev, isSyncing: false, syncProgress: null })); throw error; } }, [api, state.isInitialized, state.isSyncing]); // Sync only pending operations const syncPendingOperations = useCallback(async (): Promise => { if (!api || !state.isInitialized) return; try { await offlineLibraryManager.syncPendingOperations(api); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); } catch (error) { console.error('Failed to sync pending operations:', error); } }, [api, state.isInitialized]); // Data retrieval methods (offline-first) const getAlbums = useCallback(async (starred?: boolean): Promise => { if (!state.isInitialized) return []; try { // Try offline first const offlineAlbums = await offlineLibraryManager.getAlbums(starred); // If offline data exists, return it if (offlineAlbums.length > 0) { return offlineAlbums; } // If no offline data and we're online, try server if (state.isOnline && api) { const serverAlbums = starred ? await api.getAlbums('starred') : await api.getAlbums('alphabeticalByName', 100); // Cache the results await offlineLibraryManager.storeAlbums(serverAlbums); return serverAlbums; } return []; } catch (error) { console.error('Failed to get albums:', error); return []; } }, [state.isInitialized, state.isOnline, api]); const getArtists = useCallback(async (starred?: boolean): Promise => { if (!state.isInitialized) return []; try { const offlineArtists = await offlineLibraryManager.getArtists(starred); if (offlineArtists.length > 0) { return offlineArtists; } if (state.isOnline && api) { const serverArtists = await api.getArtists(); await offlineLibraryManager.storeArtists(serverArtists); return serverArtists; } return []; } catch (error) { console.error('Failed to get artists:', error); return []; } }, [state.isInitialized, state.isOnline, api]); const getAlbum = useCallback(async (albumId: string): Promise<{ album: Album; songs: Song[] } | null> => { if (!state.isInitialized) return null; try { // Try offline first const offlineData = await offlineLibraryManager.getAlbum(albumId); if (offlineData && offlineData.songs.length > 0) { return offlineData; } // If no offline data and we're online, try server if (state.isOnline && api) { const serverData = await api.getAlbum(albumId); // Cache the results await offlineLibraryManager.storeAlbums([serverData.album]); await offlineLibraryManager.storeSongs(serverData.songs); return serverData; } return offlineData; } catch (error) { console.error('Failed to get album:', error); return null; } }, [state.isInitialized, state.isOnline, api]); const getPlaylists = useCallback(async (): Promise => { if (!state.isInitialized) return []; try { const offlinePlaylists = await offlineLibraryManager.getPlaylists(); if (offlinePlaylists.length > 0) { return offlinePlaylists; } if (state.isOnline && api) { const serverPlaylists = await api.getPlaylists(); await offlineLibraryManager.storePlaylists(serverPlaylists); return serverPlaylists; } return []; } catch (error) { console.error('Failed to get playlists:', error); return []; } }, [state.isInitialized, state.isOnline, api]); // Search (offline-first) const searchOffline = useCallback(async (query: string): Promise<{ artists: Artist[]; albums: Album[]; songs: Song[] }> => { if (!state.isInitialized) { return { artists: [], albums: [], songs: [] }; } try { const offlineResults = await offlineLibraryManager.searchOffline(query); // If we have good offline results, return them const totalResults = offlineResults.artists.length + offlineResults.albums.length + offlineResults.songs.length; if (totalResults > 0) { return offlineResults; } // If no offline results and we're online, try server if (state.isOnline && api) { return await api.search2(query); } return offlineResults; } catch (error) { console.error('Search failed:', error); return { artists: [], albums: [], songs: [] }; } }, [state.isInitialized, state.isOnline, api]); // Offline favorites management const starOffline = useCallback(async (id: string, type: 'song' | 'album' | 'artist'): Promise => { if (!state.isInitialized) return; try { if (state.isOnline && api) { // If online, try server first await api.star(id, type); } // Always update offline data await offlineLibraryManager.starOffline(id, type); // Update stats const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); } catch (error) { console.error('Failed to star item:', error); // If server failed but we're online, still save offline for later sync if (state.isOnline) { await offlineLibraryManager.starOffline(id, type); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); } throw error; } }, [state.isInitialized, state.isOnline, api]); const unstarOffline = useCallback(async (id: string, type: 'song' | 'album' | 'artist'): Promise => { if (!state.isInitialized) return; try { if (state.isOnline && api) { await api.unstar(id, type); } await offlineLibraryManager.unstarOffline(id, type); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); } catch (error) { console.error('Failed to unstar item:', error); if (state.isOnline) { await offlineLibraryManager.unstarOffline(id, type); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); } throw error; } }, [state.isInitialized, state.isOnline, api]); // Playlist management const createPlaylistOffline = useCallback(async (name: string, songIds?: string[]): Promise => { if (!state.isInitialized) { throw new Error('Offline library not initialized'); } try { if (state.isOnline && api) { // If online, try server first const serverPlaylist = await api.createPlaylist(name, songIds); await offlineLibraryManager.storePlaylists([serverPlaylist]); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); return serverPlaylist; } else { // If offline, create locally and queue for sync const offlinePlaylist = await offlineLibraryManager.createPlaylistOffline(name, songIds); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); return offlinePlaylist; } } catch (error) { console.error('Failed to create playlist:', error); // If server failed but we're online, create offline version for later sync if (state.isOnline) { const offlinePlaylist = await offlineLibraryManager.createPlaylistOffline(name, songIds); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); return offlinePlaylist; } throw error; } }, [state.isInitialized, state.isOnline, api]); // Scrobble (offline-capable) const scrobbleOffline = useCallback(async (songId: string): Promise => { if (!state.isInitialized) return; try { if (state.isOnline && api) { await api.scrobble(songId); } else { // Queue for later sync await offlineLibraryManager.addSyncOperation({ type: 'scrobble', entityType: 'song', entityId: songId, data: {} }); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); } } catch (error) { console.error('Failed to scrobble:', error); // Queue for later sync if server failed await offlineLibraryManager.addSyncOperation({ type: 'scrobble', entityType: 'song', entityId: songId, data: {} }); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats })); } }, [state.isInitialized, state.isOnline, api]); // Clear all offline data const clearOfflineData = useCallback(async (): Promise => { if (!state.isInitialized) return; try { await offlineLibraryManager.clearAllData(); const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats, lastSync: null })); } catch (error) { console.error('Failed to clear offline data:', error); throw error; } }, [state.isInitialized]); // Get songs with offline-first approach const getSongs = useCallback(async (albumId?: string, artistId?: string): Promise => { if (!state.isInitialized) return []; try { const offlineSongs = await offlineLibraryManager.getSongs(albumId, artistId); if (offlineSongs.length > 0) { return offlineSongs; } if (state.isOnline && api) { let serverSongs: Song[] = []; if (albumId) { const { songs } = await api.getAlbum(albumId); await offlineLibraryManager.storeSongs(songs); serverSongs = songs; } else if (artistId) { const { albums } = await api.getArtist(artistId); const allSongs: Song[] = []; for (const album of albums) { const { songs } = await api.getAlbum(album.id); allSongs.push(...songs); } await offlineLibraryManager.storeSongs(allSongs); serverSongs = allSongs; } return serverSongs; } return []; } catch (error) { console.error('Failed to get songs:', error); return []; } }, [api, state.isInitialized, state.isOnline]); // Queue sync operation const queueSyncOperation = useCallback(async (operation: Omit): Promise => { if (!state.isInitialized) return; const fullOperation: SyncOperation = { ...operation, id: `${operation.type}-${operation.entityId}-${Date.now()}`, timestamp: Date.now(), retryCount: 0 }; await offlineLibraryManager.addSyncOperation(fullOperation); await refreshStats(); }, [state.isInitialized]); // Update playlist offline const updatePlaylistOffline = useCallback(async (id: string, name?: string, comment?: string, songIds?: string[]): Promise => { if (!state.isInitialized) return; await offlineLibraryManager.updatePlaylist(id, name, comment, songIds); await refreshStats(); }, [state.isInitialized]); // Delete playlist offline const deletePlaylistOffline = useCallback(async (id: string): Promise => { if (!state.isInitialized) return; await offlineLibraryManager.deletePlaylist(id); await refreshStats(); }, [state.isInitialized]); // Refresh stats const refreshStats = useCallback(async (): Promise => { if (!state.isInitialized) return; try { const stats = await offlineLibraryManager.getLibraryStats(); setState(prev => ({ ...prev, stats, lastSync: stats.lastSync })); } catch (error) { console.error('Failed to refresh stats:', error); } }, [state.isInitialized]); return { // State ...state, // Sync methods syncLibraryFromServer, syncPendingOperations, // Data retrieval (offline-first) getAlbums, getArtists, getSongs, getAlbum, getPlaylists, searchOffline, // Offline operations starOffline, unstarOffline, createPlaylistOffline, updatePlaylistOffline, deletePlaylistOffline, scrobbleOffline, queueSyncOperation, // Management clearOfflineData, refreshStats }; }