296 lines
8.0 KiB
TypeScript
296 lines
8.0 KiB
TypeScript
'use client';
|
|
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
|
import { getNavidromeAPI, Album, Artist, Song, Playlist } from '@/lib/navidrome';
|
|
|
|
interface NavidromeContextType {
|
|
// Data
|
|
albums: Album[];
|
|
artists: Artist[];
|
|
playlists: Playlist[];
|
|
|
|
// Loading states
|
|
isLoading: boolean;
|
|
albumsLoading: boolean;
|
|
artistsLoading: boolean;
|
|
playlistsLoading: boolean;
|
|
|
|
// Error states
|
|
error: string | null;
|
|
|
|
// Methods
|
|
searchMusic: (query: string) => Promise<{ artists: Artist[]; albums: Album[]; songs: Song[] }>;
|
|
getAlbum: (albumId: string) => Promise<{ album: Album; songs: Song[] }>;
|
|
getArtist: (artistId: string) => Promise<{ artist: Artist; albums: Album[] }>;
|
|
getPlaylist: (playlistId: string) => Promise<{ playlist: Playlist; songs: Song[] }>;
|
|
getAllSongs: () => Promise<Song[]>;
|
|
refreshData: () => Promise<void>;
|
|
createPlaylist: (name: string, songIds?: string[]) => Promise<Playlist>;
|
|
updatePlaylist: (playlistId: string, name?: string, comment?: string, songIds?: string[]) => Promise<void>;
|
|
deletePlaylist: (playlistId: string) => Promise<void>;
|
|
starItem: (id: string, type: 'song' | 'album' | 'artist') => Promise<void>;
|
|
unstarItem: (id: string, type: 'song' | 'album' | 'artist') => Promise<void>;
|
|
scrobble: (songId: string) => Promise<void>;
|
|
}
|
|
|
|
const NavidromeContext = createContext<NavidromeContextType | undefined>(undefined);
|
|
|
|
interface NavidromeProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export const NavidromeProvider: React.FC<NavidromeProviderProps> = ({ children }) => {
|
|
const [albums, setAlbums] = useState<Album[]>([]);
|
|
const [artists, setArtists] = useState<Artist[]>([]);
|
|
const [playlists, setPlaylists] = useState<Playlist[]>([]);
|
|
|
|
const [albumsLoading, setAlbumsLoading] = useState(false);
|
|
const [artistsLoading, setArtistsLoading] = useState(false);
|
|
const [playlistsLoading, setPlaylistsLoading] = useState(false);
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const isLoading = albumsLoading || artistsLoading || playlistsLoading;
|
|
|
|
const api = getNavidromeAPI();
|
|
|
|
const loadAlbums = async () => {
|
|
setAlbumsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const recentAlbums = await api.getAlbums('recent', 50);
|
|
const newestAlbums = await api.getAlbums('newest', 50);
|
|
|
|
// Combine and deduplicate albums
|
|
const allAlbums = [...recentAlbums, ...newestAlbums];
|
|
const uniqueAlbums = allAlbums.filter((album, index, self) =>
|
|
index === self.findIndex(a => a.id === album.id)
|
|
);
|
|
|
|
setAlbums(uniqueAlbums);
|
|
} catch (err) {
|
|
console.error('Failed to load albums:', err);
|
|
setError('Failed to load albums');
|
|
} finally {
|
|
setAlbumsLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadArtists = async () => {
|
|
setArtistsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const artistList = await api.getArtists();
|
|
setArtists(artistList);
|
|
} catch (err) {
|
|
console.error('Failed to load artists:', err);
|
|
setError('Failed to load artists');
|
|
} finally {
|
|
setArtistsLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadPlaylists = async () => {
|
|
setPlaylistsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const playlistList = await api.getPlaylists();
|
|
setPlaylists(playlistList);
|
|
} catch (err) {
|
|
console.error('Failed to load playlists:', err);
|
|
setError('Failed to load playlists');
|
|
} finally {
|
|
setPlaylistsLoading(false);
|
|
}
|
|
};
|
|
|
|
const refreshData = async () => {
|
|
await Promise.all([loadAlbums(), loadArtists(), loadPlaylists()]);
|
|
};
|
|
|
|
const searchMusic = async (query: string) => {
|
|
setError(null);
|
|
try {
|
|
return await api.search(query);
|
|
} catch (err) {
|
|
console.error('Search failed:', err);
|
|
setError('Search failed');
|
|
return { artists: [], albums: [], songs: [] };
|
|
}
|
|
};
|
|
|
|
const getAlbum = async (albumId: string) => {
|
|
setError(null);
|
|
try {
|
|
return await api.getAlbum(albumId);
|
|
} catch (err) {
|
|
console.error('Failed to get album:', err);
|
|
setError('Failed to get album');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const getArtist = async (artistId: string) => {
|
|
setError(null);
|
|
try {
|
|
return await api.getArtist(artistId);
|
|
} catch (err) {
|
|
console.error('Failed to get artist:', err);
|
|
setError('Failed to get artist');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const getPlaylist = async (playlistId: string) => {
|
|
setError(null);
|
|
try {
|
|
return await api.getPlaylist(playlistId);
|
|
} catch (err) {
|
|
console.error('Failed to get playlist:', err);
|
|
setError('Failed to get playlist');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const getAllSongs = async () => {
|
|
setError(null);
|
|
try {
|
|
return await api.getAllSongs();
|
|
} catch (err) {
|
|
console.error('Failed to get all songs:', err);
|
|
setError('Failed to get all songs');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const createPlaylist = async (name: string, songIds?: string[]) => {
|
|
setError(null);
|
|
try {
|
|
const playlist = await api.createPlaylist(name, songIds);
|
|
await loadPlaylists(); // Refresh playlists
|
|
return playlist;
|
|
} catch (err) {
|
|
console.error('Failed to create playlist:', err);
|
|
setError('Failed to create playlist');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const updatePlaylist = async (playlistId: string, name?: string, comment?: string, songIds?: string[]) => {
|
|
setError(null);
|
|
try {
|
|
await api.updatePlaylist(playlistId, name, comment, songIds);
|
|
await loadPlaylists(); // Refresh playlists
|
|
} catch (err) {
|
|
console.error('Failed to update playlist:', err);
|
|
setError('Failed to update playlist');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const deletePlaylist = async (playlistId: string) => {
|
|
setError(null);
|
|
try {
|
|
await api.deletePlaylist(playlistId);
|
|
await loadPlaylists(); // Refresh playlists
|
|
} catch (err) {
|
|
console.error('Failed to delete playlist:', err);
|
|
setError('Failed to delete playlist');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const starItem = async (id: string, type: 'song' | 'album' | 'artist') => {
|
|
setError(null);
|
|
try {
|
|
await api.star(id, type);
|
|
} catch (err) {
|
|
console.error('Failed to star item:', err);
|
|
setError('Failed to star item');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const unstarItem = async (id: string, type: 'song' | 'album' | 'artist') => {
|
|
setError(null);
|
|
try {
|
|
await api.unstar(id, type);
|
|
} catch (err) {
|
|
console.error('Failed to unstar item:', err);
|
|
setError('Failed to unstar item');
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const scrobble = async (songId: string) => {
|
|
try {
|
|
await api.scrobble(songId);
|
|
} catch (err) {
|
|
console.error('Failed to scrobble:', err);
|
|
// Don't set error state for scrobbling failures as they're not critical
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
// Test connection and load initial data
|
|
const initialize = async () => {
|
|
try {
|
|
const isConnected = await api.ping();
|
|
if (isConnected) {
|
|
await refreshData();
|
|
} else {
|
|
setError('Failed to connect to Navidrome server');
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to initialize Navidrome:', err);
|
|
setError('Failed to initialize Navidrome connection');
|
|
}
|
|
};
|
|
|
|
initialize();
|
|
}, []);
|
|
|
|
const value: NavidromeContextType = {
|
|
// Data
|
|
albums,
|
|
artists,
|
|
playlists,
|
|
|
|
// Loading states
|
|
isLoading,
|
|
albumsLoading,
|
|
artistsLoading,
|
|
playlistsLoading,
|
|
|
|
// Error state
|
|
error,
|
|
|
|
// Methods
|
|
searchMusic,
|
|
getAlbum,
|
|
getArtist,
|
|
getPlaylist,
|
|
getAllSongs,
|
|
refreshData,
|
|
createPlaylist,
|
|
updatePlaylist,
|
|
deletePlaylist,
|
|
starItem,
|
|
unstarItem,
|
|
scrobble
|
|
};
|
|
|
|
return (
|
|
<NavidromeContext.Provider value={value}>
|
|
{children}
|
|
</NavidromeContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useNavidrome = (): NavidromeContextType => {
|
|
const context = useContext(NavidromeContext);
|
|
if (context === undefined) {
|
|
throw new Error('useNavidrome must be used within a NavidromeProvider');
|
|
}
|
|
return context;
|
|
};
|