diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index d719847..98f226d 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -2,12 +2,12 @@ import { useState, useMemo } from "react" import { useQuery } from "@tanstack/react-query" -import { getTracks, getStats } from "@/lib/api" +import { getTracks } from "@/lib/api" import type { FilterParams, Track } from "@/lib/types" import FilterPanel from "@/components/FilterPanel" import AudioPlayer from "@/components/AudioPlayer" -// Helper function to format Discogs genre labels (e.g., "Pop---Ballad" -> ["Pop", "Ballad"]) +// Helper function to format Discogs genre labels function formatGenre(genre: string): { category: string; subgenre: string } { const parts = genre.split('---') return { @@ -24,21 +24,17 @@ function extractFilterOptions(tracks: Track[]) { const keys = new Set() tracks.forEach(track => { - // Extract genre category (before "---") const genreCategory = formatGenre(track.classification.genre.primary).category genres.add(genreCategory) - // Extract primary mood if (track.classification.mood.primary) { moods.add(track.classification.mood.primary) } - // Extract instruments track.classification.instruments?.forEach(instrument => { instruments.add(instrument) }) - // Extract key if (track.features.key) { keys.add(track.features.key) } @@ -56,21 +52,27 @@ export default function Home() { const [filters, setFilters] = useState({}) const [page, setPage] = useState(0) const [currentTrack, setCurrentTrack] = useState(null) - const limit = 50 + const [searchQuery, setSearchQuery] = useState("") + const limit = 25 const { data: tracksData, isLoading: isLoadingTracks } = useQuery({ queryKey: ['tracks', filters, page], queryFn: () => getTracks({ ...filters, skip: page * limit, limit }), }) - const { data: stats } = useQuery({ - queryKey: ['stats'], - queryFn: getStats, - }) + // Filter tracks by search query on client side + const filteredTracks = useMemo(() => { + if (!tracksData?.tracks) return [] + if (!searchQuery.trim()) return tracksData.tracks + + const query = searchQuery.toLowerCase() + return tracksData.tracks.filter(track => + track.filename.toLowerCase().includes(query) || + track.metadata?.title?.toLowerCase().includes(query) || + track.metadata?.artist?.toLowerCase().includes(query) + ) + }, [tracksData?.tracks, searchQuery]) - // Extract filter options from displayed tracks - // For better UX, we use the current page's tracks to populate filters - // TODO: Add a dedicated API endpoint to get all unique filter values const filterOptions = useMemo(() => { if (!tracksData?.tracks || tracksData.tracks.length === 0) { return { genres: [], moods: [], instruments: [], keys: [] } @@ -78,212 +80,205 @@ export default function Home() { return extractFilterOptions(tracksData.tracks) }, [tracksData]) + const totalPages = tracksData ? Math.ceil(tracksData.total / limit) : 0 + return ( -
+
{/* Header */} -
-
-

Audio Classifier

-

Intelligent music library management

+
+
+
+
+

Bibliothèque Musicale

+

Gestion intelligente de votre collection audio

+
+ + {/* Search bar */} +
+
+ setSearchQuery(e.target.value)} + className="w-full px-4 py-2 pl-10 bg-slate-50 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm" + /> + + + +
+
+ +
+ {tracksData?.total || 0} piste{(tracksData?.total || 0) > 1 ? 's' : ''} +
+
- {/* Main Content */} -
- {/* Stats */} - {stats && ( -
-
-

Total Tracks

-

{stats.total_tracks}

-
-
-

Avg BPM

-

{stats.average_bpm}

-
-
-

Total Hours

-

{stats.total_duration_hours}h

-
-
-

Genres

-

{stats.genres.length}

-
+ {/* Main content with sidebar */} +
+ {/* Sidebar */} + - {/* Filter Panel */} - { - setFilters(newFilters) - setPage(0) // Reset to first page when filters change - }} - availableGenres={filterOptions.genres} - availableMoods={filterOptions.moods} - availableInstruments={filterOptions.instruments} - availableKeys={filterOptions.keys} - /> + {/* Tracks list */} +
+
+ {isLoadingTracks ? ( +
+
Chargement...
+
+ ) : filteredTracks.length === 0 ? ( +
+ + + +

Aucune piste trouvée

+

Essayez de modifier vos filtres ou votre recherche

+
+ ) : ( + <> +
+ {filteredTracks.map((track) => ( +
+
+ {/* Play button */} + - {/* Tracks List */} -
-
-

Music Library

-

- {tracksData?.total || 0} tracks total -

-
+ {/* Track info */} +
+

+ {track.metadata?.title || track.filename} +

+ {track.metadata?.artist && ( +

{track.metadata.artist}

+ )} - {isLoadingTracks ? ( -
Loading...
- ) : tracksData?.tracks.length === 0 ? ( -
- No tracks found. Start by analyzing your audio library! -
- ) : ( -
- {tracksData?.tracks.map((track) => ( -
-
-
-

{track.filename}

+
+ {/* Genre */} + {(() => { + const genre = formatGenre(track.classification.genre.primary) + return ( + <> + + {genre.category} + + {genre.subgenre && ( + + {genre.subgenre} + + )} + + ) + })()} - {/* Primary metadata */} -
- {(() => { - const genre = formatGenre(track.classification.genre.primary) - return ( - <> - - {genre.category} - - {genre.subgenre && ( - - {genre.subgenre} + {/* Mood */} + + {track.classification.mood.primary} + + + {/* Key & BPM */} + + {track.features.key} + + + {Math.round(track.features.tempo_bpm)} BPM + + + {/* Duration */} + + {Math.floor(track.duration_seconds / 60)}:{String(Math.floor(track.duration_seconds % 60)).padStart(2, '0')} + +
+ + {/* Instruments */} + {track.classification.instruments && track.classification.instruments.length > 0 && ( +
+ Instruments: + {track.classification.instruments.slice(0, 5).map((instrument, i) => ( + + {instrument} + ))} + {track.classification.instruments.length > 5 && ( + +{track.classification.instruments.length - 5} )} - - ) - })()} - - {track.classification.mood.primary} - - - {Math.round(track.features.tempo_bpm)} BPM - - - {track.features.key} - - - {Math.floor(track.duration_seconds / 60)}:{String(Math.floor(track.duration_seconds % 60)).padStart(2, '0')} - -
- - {/* Secondary genres */} - {track.classification.genre.secondary && track.classification.genre.secondary.length > 0 && ( -
- Also: - {track.classification.genre.secondary.slice(0, 3).map((g, i) => { - const genre = formatGenre(g) - return ( - - {genre.category}{genre.subgenre && ` • ${genre.subgenre}`} - - ) - })} -
- )} - - {/* Secondary moods */} - {track.classification.mood.secondary && track.classification.mood.secondary.length > 0 && ( -
- Moods: - {track.classification.mood.secondary.map((mood, i) => ( - - {mood} - - ))} -
- )} - - {/* Instruments */} - {track.classification.instruments && track.classification.instruments.length > 0 && ( -
- Instruments: - {track.classification.instruments.slice(0, 6).map((instrument, i) => ( - - {instrument} - - ))} - {track.classification.instruments.length > 6 && ( - +{track.classification.instruments.length - 6} more +
)}
- )} +
-
- - - Download - -
-
+ ))}
- ))} -
- )} - {/* Pagination */} - {tracksData && tracksData.total > limit && ( -
- - - Page {page + 1} of {Math.ceil(tracksData.total / limit)} - - -
- )} -
+ {/* Pagination */} + {totalPages > 1 && ( +
+ - {/* Instructions */} -
-

Getting Started

-
    -
  1. Make sure the backend is running (docker-compose up)
  2. -
  3. Use the API to analyze your audio library: -
    -                {`curl -X POST http://localhost:8000/api/analyze/folder \\
    -  -H "Content-Type: application/json" \\
    -  -d '{"path": "/audio/your_music", "recursive": true}'`}
    -              
    -
  4. -
  5. Refresh this page to see your analyzed tracks
  6. -
-
-
+
+ + Page {page + 1} sur {totalPages} + +
- {/* Fixed Audio Player at bottom - always visible */} + +
+ )} + + )} +
+
+
+ + {/* Fixed Audio Player at bottom */}
diff --git a/frontend/components/FilterPanel.tsx b/frontend/components/FilterPanel.tsx index 7776c8b..43fc441 100644 --- a/frontend/components/FilterPanel.tsx +++ b/frontend/components/FilterPanel.tsx @@ -22,7 +22,6 @@ export default function FilterPanel({ }: FilterPanelProps) { const [localFilters, setLocalFilters] = useState(filters) - // Update local filters when parent changes useEffect(() => { setLocalFilters(filters) }, [filters]) @@ -39,208 +38,156 @@ export default function FilterPanel({ onFiltersChange(emptyFilters) } - const hasActiveFilters = Object.keys(localFilters).length > 0 + const hasActiveFilters = Object.keys(localFilters).filter(key => + localFilters[key as keyof FilterParams] !== undefined && + localFilters[key as keyof FilterParams] !== "" + ).length > 0 return ( -
-
-

Filters

- {hasActiveFilters && ( - - )} -
- -
- {/* Genre Filter */} -
- - -
- - {/* Mood Filter */} -
- - -
- - {/* Instrument Filter */} -
- - -
- - {/* Key Filter */} -
- - -
- - {/* Tempo Range Filter */} -
- - -
- - {/* Sort By */} -
- - -
- - {/* Sort Order */} -
- - -
-
- - {/* Active Filters Display */} +
+ {/* Clear all button */} {hasActiveFilters && ( -
- {localFilters.genre && ( - - Genre: {localFilters.genre} - - - )} - {localFilters.mood && ( - - Mood: {localFilters.mood} - - - )} - {localFilters.instrument && ( - - Instrument: {localFilters.instrument} - - - )} - {localFilters.key && ( - - Key: {localFilters.key} - - - )} - {localFilters.tempo_range && ( - - Tempo: {localFilters.tempo_range} - - - )} + + )} + + {/* Genre Filter */} +
+ + +
+ + {/* Mood Filter */} +
+ + +
+ + {/* Instrument Filter */} +
+ + +
+ + {/* Key Filter */} +
+ + +
+ + {/* Tempo Range Filter */} +
+ + +
+ + {/* Active filters summary */} + {hasActiveFilters && ( +
+

Filtres actifs:

+
+ {localFilters.genre && ( +
+ Genre: + {localFilters.genre} +
+ )} + {localFilters.mood && ( +
+ Ambiance: + {localFilters.mood} +
+ )} + {localFilters.instrument && ( +
+ Instrument: + {localFilters.instrument} +
+ )} + {localFilters.key && ( +
+ Tonalité: + {localFilters.key} +
+ )} + {localFilters.tempo_range && ( +
+ Tempo: + + {localFilters.tempo_range === 'slow' && 'Lent'} + {localFilters.tempo_range === 'medium' && 'Moyen'} + {localFilters.tempo_range === 'fast' && 'Rapide'} + +
+ )} +
)}