// Use relative URLs so Vite proxy handles them (enables LAN access) const API_BASE = ''; // Resolve audio URL based on storage type export function getAudioUrl(audioUrl: string | undefined | null, songId?: string): string | undefined { if (!audioUrl) return undefined; // Local storage: already relative, works with proxy if (audioUrl.startsWith('/audio/')) { return audioUrl; } // Already a full URL return audioUrl; } interface ApiOptions { method?: string; body?: unknown; token?: string | null; } async function api(endpoint: string, options: ApiOptions = {}): Promise { const { method = 'GET', body, token } = options; const headers: HeadersInit = { 'Content-Type': 'application/json', }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${API_BASE}${endpoint}`, { method, headers, body: body ? JSON.stringify(body) : undefined, credentials: 'include', }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Request failed' })); const errorMessage = error.error || error.message || 'Request failed'; // Include status code in error for proper handling throw new Error(`${response.status}: ${errorMessage}`); } return response.json(); } // Auth API (simplified - username only) export interface User { id: string; username: string; isAdmin?: boolean; bio?: string; avatar_url?: string; banner_url?: string; createdAt?: string; } export interface AuthResponse { user: User; token: string; } export const authApi = { // Auto-login: Get existing user from database (for local single-user app) auto: (): Promise => api('/api/auth/auto'), setup: (username: string): Promise => api('/api/auth/setup', { method: 'POST', body: { username } }), me: (token: string): Promise<{ user: User }> => api('/api/auth/me', { token }), logout: (): Promise<{ success: boolean }> => api('/api/auth/logout', { method: 'POST' }), refresh: (token: string): Promise => api('/api/auth/refresh', { method: 'POST', token }), updateUsername: (username: string, token: string): Promise => api('/api/auth/username', { method: 'PATCH', body: { username }, token }), }; // Songs API export interface Song { id: string; title: string; lyrics: string; style: string; caption?: string; cover_url?: string; audio_url?: string; audioUrl?: string; duration?: number; bpm?: number; key_scale?: string; time_signature?: string; tags: string[]; is_public: boolean; like_count?: number; view_count?: number; user_id?: string; created_at: string; creator?: string; generation_params?: any; } // Transform songs to have proper audio URLs function transformSongs(songs: Song[]): Song[] { return songs.map(song => { const rawUrl = song.audio_url || song.audioUrl; const resolvedUrl = getAudioUrl(rawUrl, song.id); return { ...song, audio_url: resolvedUrl, audioUrl: resolvedUrl, }; }); } export const songsApi = { getMySongs: async (token: string): Promise<{ songs: Song[] }> => { const result = await api('/api/songs', { token }) as { songs: Song[] }; return { songs: transformSongs(result.songs) }; }, getPublicSongs: async (limit = 20, offset = 0): Promise<{ songs: Song[] }> => { const result = await api(`/api/songs/public?limit=${limit}&offset=${offset}`) as { songs: Song[] }; return { songs: transformSongs(result.songs) }; }, getFeaturedSongs: async (): Promise<{ songs: Song[] }> => { const result = await api('/api/songs/public/featured') as { songs: Song[] }; return { songs: transformSongs(result.songs) }; }, getSong: async (id: string, token?: string | null): Promise<{ song: Song }> => { const result = await api(`/api/songs/${id}`, { token: token || undefined }) as { song: Song }; const rawUrl = result.song.audio_url || result.song.audioUrl; const resolvedUrl = getAudioUrl(rawUrl, result.song.id); return { song: { ...result.song, audio_url: resolvedUrl, audioUrl: resolvedUrl } }; }, getFullSong: async (id: string, token?: string | null): Promise<{ song: Song, comments: any[] }> => { const result = await api(`/api/songs/${id}/full`, { token: token || undefined }) as { song: Song, comments: any[] }; const rawUrl = result.song.audio_url || result.song.audioUrl; const resolvedUrl = getAudioUrl(rawUrl, result.song.id); return { ...result, song: { ...result.song, audio_url: resolvedUrl, audioUrl: resolvedUrl } }; }, createSong: (song: Partial, token: string): Promise<{ song: Song }> => api('/api/songs', { method: 'POST', body: song, token }), updateSong: (id: string, updates: Partial, token: string): Promise<{ song: Song }> => api(`/api/songs/${id}`, { method: 'PATCH', body: updates, token }), deleteSong: (id: string, token: string): Promise<{ success: boolean }> => api(`/api/songs/${id}`, { method: 'DELETE', token }), toggleLike: (id: string, token: string): Promise<{ liked: boolean }> => api(`/api/songs/${id}/like`, { method: 'POST', token }), getLikedSongs: async (token: string): Promise<{ songs: Song[] }> => { const result = await api('/api/songs/liked/list', { token }) as { songs: Song[] }; return { songs: transformSongs(result.songs) }; }, togglePrivacy: (id: string, token: string): Promise<{ isPublic: boolean }> => api(`/api/songs/${id}/privacy`, { method: 'PATCH', token }), trackPlay: (id: string, token?: string | null): Promise<{ viewCount: number }> => api(`/api/songs/${id}/play`, { method: 'POST', token: token || undefined }), getComments: (id: string, token?: string | null): Promise<{ comments: Comment[] }> => api(`/api/songs/${id}/comments`, { token: token || undefined }), addComment: (id: string, content: string, token: string): Promise<{ comment: Comment }> => api(`/api/songs/${id}/comments`, { method: 'POST', body: { content }, token }), deleteComment: (commentId: string, token: string): Promise<{ success: boolean }> => api(`/api/songs/comments/${commentId}`, { method: 'DELETE', token }), }; interface Comment { id: string; song_id: string; user_id: string; username: string; content: string; created_at: string; } // Generation API export interface GenerationParams { // Mode customMode: boolean; songDescription?: string; // Custom Mode prompt?: string; lyrics: string; style: string; title: string; // Common instrumental: boolean; vocalLanguage?: string; // Music Parameters duration?: number; bpm?: number; keyScale?: string; timeSignature?: string; // Generation Settings inferenceSteps?: number; guidanceScale?: number; batchSize?: number; randomSeed?: boolean; seed?: number; thinking?: boolean; audioFormat?: 'mp3' | 'flac'; inferMethod?: 'ode' | 'sde'; shift?: number; // LM Parameters lmTemperature?: number; lmCfgScale?: number; lmTopK?: number; lmTopP?: number; lmNegativePrompt?: string; lmBackend?: 'pt' | 'vllm'; lmModel?: string; // Expert Parameters referenceAudioUrl?: string; sourceAudioUrl?: string; referenceAudioTitle?: string; sourceAudioTitle?: string; audioCodes?: string; repaintingStart?: number; repaintingEnd?: number; instruction?: string; audioCoverStrength?: number; taskType?: string; useAdg?: boolean; cfgIntervalStart?: number; cfgIntervalEnd?: number; customTimesteps?: string; useCotMetas?: boolean; useCotCaption?: boolean; useCotLanguage?: boolean; autogen?: boolean; constrainedDecodingDebug?: boolean; allowLmBatch?: boolean; getScores?: boolean; getLrc?: boolean; scoreScale?: number; lmBatchChunkSize?: number; trackName?: string; completeTrackClasses?: string[]; isFormatCaption?: boolean; } export interface GenerationJob { jobId: string; status: 'pending' | 'queued' | 'running' | 'succeeded' | 'failed'; queuePosition?: number; etaSeconds?: number; progress?: number; stage?: string; result?: { audioUrls: string[]; bpm?: number; duration?: number; keyScale?: string; timeSignature?: string; }; error?: string; } export const generateApi = { startGeneration: (params: GenerationParams, token: string): Promise => api('/api/generate', { method: 'POST', body: params, token }), getStatus: (jobId: string, token: string): Promise => api(`/api/generate/status/${jobId}`, { token }), getHistory: (token: string): Promise<{ jobs: GenerationJob[] }> => api('/api/generate/history', { token }), uploadAudio: async (file: File, token: string): Promise<{ url: string; key: string }> => { const formData = new FormData(); formData.append('audio', file); const response = await fetch(`${API_BASE}/api/generate/upload-audio`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Upload failed' })); throw new Error(error.details || error.error || 'Upload failed'); } return response.json(); }, formatInput: (params: { caption: string; lyrics?: string; bpm?: number; duration?: number; keyScale?: string; timeSignature?: string; temperature?: number; topK?: number; topP?: number; lmModel?: string; lmBackend?: string; }, token: string): Promise<{ success: boolean; caption?: string; lyrics?: string; bpm?: number; duration?: number; key_scale?: string; language?: string; time_signature?: string; status_message?: string; error?: string; }> => api('/api/generate/format', { method: 'POST', body: params, token }), // LoRA management (requires ACE-Step training fork) loadLora: (params: { lora_path: string }, token: string): Promise<{ message: string }> => api('/api/generate/lora/load', { method: 'POST', body: params, token }), unloadLora: (token: string): Promise<{ message: string }> => api('/api/generate/lora/unload', { method: 'POST', token }), setLoraScale: (params: { scale: number }, token: string): Promise<{ message: string }> => api('/api/generate/lora/scale', { method: 'POST', body: params, token }), }; // Users API export interface UserProfile extends User { bio?: string; avatar_url?: string; banner_url?: string; created_at: string; } export const usersApi = { getProfile: (username: string, token?: string | null): Promise<{ user: UserProfile }> => api(`/api/users/${username}`, { token: token || undefined }), getPublicSongs: (username: string): Promise<{ songs: Song[] }> => api(`/api/users/${username}/songs`), getPublicPlaylists: (username: string): Promise<{ playlists: any[] }> => api(`/api/users/${username}/playlists`), getFeaturedCreators: (): Promise<{ creators: Array }> => api('/api/users/public/featured'), updateProfile: (updates: Partial, token: string): Promise<{ user: User }> => api('/api/users/me', { method: 'PATCH', body: updates, token }), uploadAvatar: async (file: File, token: string): Promise<{ user: UserProfile; url: string }> => { const formData = new FormData(); formData.append('avatar', file); const response = await fetch(`${API_BASE}/api/users/me/avatar`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Upload failed' })); throw new Error(error.details || error.error || 'Upload failed'); } return response.json(); }, uploadBanner: async (file: File, token: string): Promise<{ user: UserProfile; url: string }> => { const formData = new FormData(); formData.append('banner', file); const response = await fetch(`${API_BASE}/api/users/me/banner`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); if (!response.ok) { const error = await response.json().catch(() => ({ error: 'Upload failed' })); throw new Error(error.error || 'Upload failed'); } return response.json(); }, toggleFollow: (username: string, token: string): Promise<{ following: boolean, followerCount: number }> => api(`/api/users/${username}/follow`, { method: 'POST', token }), getFollowers: (username: string): Promise<{ followers: User[] }> => api(`/api/users/${username}/followers`), getFollowing: (username: string): Promise<{ following: User[] }> => api(`/api/users/${username}/following`), getStats: (username: string, token?: string | null): Promise<{ followerCount: number, followingCount: number, isFollowing: boolean }> => api(`/api/users/${username}/stats`, { token: token || undefined }), }; // Playlists API export interface Playlist { id: string; name: string; description?: string; cover_url?: string; is_public?: boolean; user_id?: string; created_at?: string; song_count?: number; } export const playlistsApi = { create: (name: string, description: string, isPublic: boolean, token: string): Promise<{ playlist: Playlist }> => api('/api/playlists', { method: 'POST', body: { name, description, isPublic }, token }), getMyPlaylists: (token: string): Promise<{ playlists: Playlist[] }> => api('/api/playlists', { token }), getPlaylist: (id: string, token?: string | null): Promise<{ playlist: Playlist, songs: any[] }> => api(`/api/playlists/${id}`, { token: token || undefined }), getFeaturedPlaylists: (): Promise<{ playlists: Array }> => api('/api/playlists/public/featured'), addSong: (playlistId: string, songId: string, token: string): Promise<{ success: boolean }> => api(`/api/playlists/${playlistId}/songs`, { method: 'POST', body: { songId }, token }), removeSong: (playlistId: string, songId: string, token: string): Promise<{ success: boolean }> => api(`/api/playlists/${playlistId}/songs/${songId}`, { method: 'DELETE', token }), update: (id: string, updates: Partial, token: string): Promise<{ playlist: Playlist }> => api(`/api/playlists/${id}`, { method: 'PATCH', body: updates, token }), delete: (id: string, token: string): Promise<{ success: boolean }> => api(`/api/playlists/${id}`, { method: 'DELETE', token }), }; // Search API export interface SearchResult { songs: Song[]; creators: Array; playlists: Array; } export const searchApi = { search: async (query: string, type?: 'songs' | 'creators' | 'playlists' | 'all'): Promise => { const params = new URLSearchParams({ q: query }); if (type && type !== 'all') params.append('type', type); const result = await api(`/api/search?${params}`) as SearchResult; return { ...result, songs: transformSongs(result.songs || []), }; }, }; // Contact Form API export interface ContactFormData { name: string; email: string; subject: string; message: string; category: 'general' | 'support' | 'business' | 'press' | 'legal'; } export const contactApi = { submit: (data: ContactFormData): Promise<{ success: boolean; message: string; id: string }> => api('/api/contact', { method: 'POST', body: data }), };