From f3f321511dbf9f004333b02d2754edae57b8271b Mon Sep 17 00:00:00 2001 From: Benoit Date: Fri, 26 Dec 2025 20:10:39 +0100 Subject: [PATCH] =?UTF-8?q?Feature:=20S=C3=A9lection=20multiple=20d'instru?= =?UTF-8?q?ments=20dans=20les=20filtres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend: - FilterPanel: Remplacer select par checkboxes pour instruments - Zone scrollable (max-height 12rem) pour la liste - Affichage des instruments sélectionnés dans résumé filtres actifs Backend: - API tracks: Nouveau paramètre instruments (List[str]) - Backward compatible avec ancien paramètre instrument - CRUD: Filtrage AND (track doit avoir TOUS les instruments) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/src/api/routes/tracks.py | 9 ++++- backend/src/models/crud.py | 10 +++-- frontend/components/FilterPanel.tsx | 59 ++++++++++++++++++++--------- frontend/lib/types.ts | 1 + 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/backend/src/api/routes/tracks.py b/backend/src/api/routes/tracks.py index ec516eb..be1e794 100644 --- a/backend/src/api/routes/tracks.py +++ b/backend/src/api/routes/tracks.py @@ -24,6 +24,7 @@ async def get_tracks( has_vocals: Optional[bool] = None, key: Optional[str] = None, instrument: Optional[str] = None, + instruments: Optional[List[str]] = Query(None), tempo_range: Optional[str] = Query(None, regex="^(slow|medium|fast)$"), sort_by: str = Query("analyzed_at", regex="^(analyzed_at|tempo_bpm|duration_seconds|filename|energy)$"), sort_desc: bool = True, @@ -42,7 +43,8 @@ async def get_tracks( energy_max: Maximum energy has_vocals: Filter by vocal presence key: Filter by musical key - instrument: Filter by instrument + instrument: Filter by instrument (deprecated, use instruments) + instruments: Filter by multiple instruments (must have ALL) tempo_range: Filter by tempo range (slow: <100, medium: 100-140, fast: >140) sort_by: Field to sort by sort_desc: Sort descending @@ -61,6 +63,9 @@ async def get_tracks( elif tempo_range == "fast": bpm_min = 140.0 if bpm_min is None else max(bpm_min, 140.0) + # Use instruments if provided, otherwise fall back to instrument + final_instruments = instruments if instruments else ([instrument] if instrument else None) + tracks, total = crud.get_tracks( db=db, skip=skip, @@ -73,7 +78,7 @@ async def get_tracks( energy_max=energy_max, has_vocals=has_vocals, key=key, - instrument=instrument, + instruments=final_instruments, sort_by=sort_by, sort_desc=sort_desc, ) diff --git a/backend/src/models/crud.py b/backend/src/models/crud.py index 0bc42c5..a381661 100644 --- a/backend/src/models/crud.py +++ b/backend/src/models/crud.py @@ -104,7 +104,7 @@ def get_tracks( energy_max: Optional[float] = None, has_vocals: Optional[bool] = None, key: Optional[str] = None, - instrument: Optional[str] = None, + instruments: Optional[List[str]] = None, sort_by: str = "analyzed_at", sort_desc: bool = True, ) -> Tuple[List[AudioTrack], int]: @@ -122,7 +122,7 @@ def get_tracks( energy_max: Maximum energy (0-1) has_vocals: Filter by vocal presence key: Filter by musical key - instrument: Filter by instrument + instruments: Filter by instruments (track must have ALL instruments in the list) sort_by: Field to sort by sort_desc: Sort descending if True @@ -168,8 +168,10 @@ def get_tracks( if key: query = query.filter(AudioTrack.key == key) - if instrument: - query = query.filter(AudioTrack.instruments.any(instrument)) + if instruments: + # Track must have ALL specified instruments + for instrument in instruments: + query = query.filter(AudioTrack.instruments.any(instrument)) # Get total count before pagination total = query.count() diff --git a/frontend/components/FilterPanel.tsx b/frontend/components/FilterPanel.tsx index 43fc441..72674a4 100644 --- a/frontend/components/FilterPanel.tsx +++ b/frontend/components/FilterPanel.tsx @@ -93,23 +93,40 @@ export default function FilterPanel({ - {/* Instrument Filter */} + {/* Instrument Filter - Multiple Selection */}
- +
+ {availableInstruments.length === 0 ? ( +

Aucun instrument disponible

+ ) : ( + availableInstruments.map((instrument) => { + const isSelected = localFilters.instruments?.includes(instrument) || false + return ( + + ) + }) + )} +
{/* Key Filter */} @@ -165,10 +182,16 @@ export default function FilterPanel({ {localFilters.mood} )} - {localFilters.instrument && ( -
- Instrument: - {localFilters.instrument} + {localFilters.instruments && localFilters.instruments.length > 0 && ( +
+ Instruments: +
+ {localFilters.instruments.map((instrument) => ( + + {instrument} + + ))} +
)} {localFilters.key && ( diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 3626704..5076724 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -60,6 +60,7 @@ export interface FilterParams { has_vocals?: boolean key?: string instrument?: string + instruments?: string[] // Multiple instruments filter tempo_range?: 'slow' | 'medium' | 'fast' // Lent (<100), Moyen (100-140), Rapide (>140) sort_by?: 'analyzed_at' | 'tempo_bpm' | 'duration_seconds' | 'filename' | 'energy' sort_desc?: boolean