diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts new file mode 100644 index 0000000..c1d25a5 --- /dev/null +++ b/frontend/lib/api.ts @@ -0,0 +1,103 @@ +/** + * API client for Audio Classifier backend + */ +import axios from 'axios' +import type { + Track, + TracksResponse, + SearchResponse, + SimilarTracksResponse, + JobStatus, + AnalyzeFolderRequest, + WaveformData, + Stats, + FilterParams, +} from './types' + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000' + +const apiClient = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}) + +// Tracks +export async function getTracks(params: FilterParams & { skip?: number; limit?: number }): Promise { + const response = await apiClient.get('/api/tracks', { params }) + return response.data +} + +export async function getTrack(id: string): Promise { + const response = await apiClient.get(`/api/tracks/${id}`) + return response.data +} + +export async function deleteTrack(id: string): Promise { + await apiClient.delete(`/api/tracks/${id}`) +} + +// Search +export async function searchTracks( + query: string, + filters?: { genre?: string; mood?: string; limit?: number } +): Promise { + const response = await apiClient.get('/api/search', { + params: { q: query, ...filters }, + }) + return response.data +} + +// Similar +export async function getSimilarTracks(id: string, limit: number = 10): Promise { + const response = await apiClient.get(`/api/tracks/${id}/similar`, { + params: { limit }, + }) + return response.data +} + +// Analysis +export async function analyzeFolder(request: AnalyzeFolderRequest): Promise<{ job_id: string }> { + const response = await apiClient.post('/api/analyze/folder', request) + return response.data +} + +export async function getAnalyzeStatus(jobId: string): Promise { + const response = await apiClient.get(`/api/analyze/status/${jobId}`) + return response.data +} + +export async function deleteJob(jobId: string): Promise { + await apiClient.delete(`/api/analyze/job/${jobId}`) +} + +// Audio +export function getStreamUrl(trackId: string): string { + return `${API_BASE_URL}/api/audio/stream/${trackId}` +} + +export function getDownloadUrl(trackId: string): string { + return `${API_BASE_URL}/api/audio/download/${trackId}` +} + +export async function getWaveform(trackId: string, numPeaks: number = 800): Promise { + const response = await apiClient.get(`/api/audio/waveform/${trackId}`, { + params: { num_peaks: numPeaks }, + }) + return response.data +} + +// Stats +export async function getStats(): Promise { + const response = await apiClient.get('/api/stats') + return response.data +} + +// Health +export async function healthCheck(): Promise<{ status: string }> { + const response = await apiClient.get('/health') + return response.data +} + +export default apiClient diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts new file mode 100644 index 0000000..d3b4614 --- /dev/null +++ b/frontend/lib/types.ts @@ -0,0 +1,111 @@ +/** + * TypeScript type definitions for Audio Classifier + */ + +export interface Track { + id: string + filepath: string + filename: string + duration_seconds: number + file_size_bytes: number + format: string + analyzed_at: string + + features: { + tempo_bpm: number + key: string + time_signature: string + energy: number + danceability: number + valence: number + loudness_lufs: number + spectral_centroid: number + zero_crossing_rate: number + } + + classification: { + genre: { + primary: string + secondary: string[] + confidence: number + } + mood: { + primary: string + secondary: string[] + arousal: number + valence: number + } + instruments: string[] + vocals: { + present: boolean | null + gender: string | null + } + } + + embedding: { + model: string | null + dimension: number | null + } + + metadata: Record +} + +export interface FilterParams { + genre?: string + mood?: string + bpm_min?: number + bpm_max?: number + energy_min?: number + energy_max?: number + has_vocals?: boolean + sort_by?: 'analyzed_at' | 'tempo_bpm' | 'duration_seconds' | 'filename' | 'energy' + sort_desc?: boolean +} + +export interface TracksResponse { + tracks: Track[] + total: number + skip: number + limit: number +} + +export interface SearchResponse { + query: string + tracks: Track[] + total: number +} + +export interface SimilarTracksResponse { + reference_track_id: string + similar_tracks: Track[] + total: number +} + +export interface JobStatus { + job_id: string + status: 'pending' | 'running' | 'completed' | 'failed' + progress: number + total: number + current_file: string | null + errors: Array<{ file?: string; error: string }> + saved_count?: number +} + +export interface AnalyzeFolderRequest { + path: string + recursive: boolean +} + +export interface WaveformData { + peaks: number[] + duration: number + num_peaks: number +} + +export interface Stats { + total_tracks: number + genres: Array<{ genre: string; count: number }> + moods: Array<{ mood: string; count: number }> + average_bpm: number + total_duration_hours: number +} diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts new file mode 100644 index 0000000..c2304dd --- /dev/null +++ b/frontend/lib/utils.ts @@ -0,0 +1,27 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export function formatDuration(seconds: number): string { + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins}:${secs.toString().padStart(2, '0')}` +} + +export function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB` +} + +export function formatBPM(bpm: number): string { + return `${Math.round(bpm)} BPM` +} + +export function formatPercentage(value: number): string { + return `${Math.round(value * 100)}%` +}