diff --git a/backend/src/api/routes/tracks.py b/backend/src/api/routes/tracks.py index be1e794..09aa040 100644 --- a/backend/src/api/routes/tracks.py +++ b/backend/src/api/routes/tracks.py @@ -25,8 +25,9 @@ async def get_tracks( key: Optional[str] = None, instrument: Optional[str] = None, instruments: Optional[List[str]] = Query(None), + instruments_exclusive: bool = False, 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_by: str = Query("analyzed_at", regex="^(analyzed_at|tempo_bpm|duration_seconds|filename|energy|random)$"), sort_desc: bool = True, db: Session = Depends(get_db), ): @@ -45,8 +46,9 @@ async def get_tracks( key: Filter by musical key instrument: Filter by instrument (deprecated, use instruments) instruments: Filter by multiple instruments (must have ALL) + instruments_exclusive: If True, track must have ONLY the specified instruments tempo_range: Filter by tempo range (slow: <100, medium: 100-140, fast: >140) - sort_by: Field to sort by + sort_by: Field to sort by (use 'random' for random order) sort_desc: Sort descending db: Database session @@ -79,6 +81,7 @@ async def get_tracks( has_vocals=has_vocals, key=key, instruments=final_instruments, + instruments_exclusive=instruments_exclusive, sort_by=sort_by, sort_desc=sort_desc, ) diff --git a/backend/src/models/crud.py b/backend/src/models/crud.py index a381661..22b0c74 100644 --- a/backend/src/models/crud.py +++ b/backend/src/models/crud.py @@ -105,6 +105,7 @@ def get_tracks( has_vocals: Optional[bool] = None, key: Optional[str] = None, instruments: Optional[List[str]] = None, + instruments_exclusive: bool = False, sort_by: str = "analyzed_at", sort_desc: bool = True, ) -> Tuple[List[AudioTrack], int]: @@ -123,7 +124,8 @@ def get_tracks( has_vocals: Filter by vocal presence key: Filter by musical key instruments: Filter by instruments (track must have ALL instruments in the list) - sort_by: Field to sort by + instruments_exclusive: If True, track must have ONLY the specified instruments (no others) + sort_by: Field to sort by (use 'random' for random order) sort_desc: Sort descending if True Returns: @@ -169,15 +171,28 @@ def get_tracks( query = query.filter(AudioTrack.key == key) if instruments: - # Track must have ALL specified instruments - for instrument in instruments: - query = query.filter(AudioTrack.instruments.any(instrument)) + if instruments_exclusive: + # Track must have ONLY the specified instruments (exact match) + # First, track must have ALL specified instruments + for instrument in instruments: + query = query.filter(AudioTrack.instruments.any(instrument)) + # Second, track's instruments array length must match the specified instruments count + # This is PostgreSQL specific - we check that the array has exactly the same elements + from sqlalchemy import func + query = query.filter(func.array_length(AudioTrack.instruments, 1) == len(instruments)) + else: + # Track must have ALL specified instruments (but can have others too) + for instrument in instruments: + query = query.filter(AudioTrack.instruments.any(instrument)) # Get total count before pagination total = query.count() # Apply sorting - if hasattr(AudioTrack, sort_by): + if sort_by == "random": + # Random ordering using PostgreSQL's RANDOM() function + query = query.order_by(func.random()) + elif hasattr(AudioTrack, sort_by): sort_column = getattr(AudioTrack, sort_by) if sort_desc: query = query.order_by(sort_column.desc()) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index fe525a3..44f3001 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -57,25 +57,35 @@ export default function Home() { const [searchQuery, setSearchQuery] = useState("") const [isScanning, setIsScanning] = useState(false) const [scanStatus, setScanStatus] = useState("") + const [sortBy, setSortBy] = useState<'analyzed_at' | 'random'>('analyzed_at') const limit = 25 const { data: tracksData, isLoading: isLoadingTracks } = useQuery({ - queryKey: ['tracks', filters, page], - queryFn: () => getTracks({ ...filters, skip: page * limit, limit }), + queryKey: ['tracks', filters, page, sortBy, searchQuery], + queryFn: () => getTracks({ + ...filters, + skip: page * limit, + limit: searchQuery ? 10000 : limit, // Load more for search to work across all results + sort_by: sortBy + }), }) // Filter tracks by search query on client side const filteredTracks = useMemo(() => { if (!tracksData?.tracks) return [] - if (!searchQuery.trim()) return tracksData.tracks + if (!searchQuery.trim()) { + // No search query: paginate normally + return tracksData.tracks.slice(0, limit) + } + // Search across ALL loaded 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]) + }, [tracksData?.tracks, searchQuery, limit]) const filterOptions = useMemo(() => { if (!tracksData?.tracks || tracksData.tracks.length === 0) { @@ -174,6 +184,19 @@ export default function Home() {
+ {/* Sort order toggle */} + +
{tracksData?.total || 0} piste{(tracksData?.total || 0) > 1 ? 's' : ''}
diff --git a/frontend/components/FilterPanel.tsx b/frontend/components/FilterPanel.tsx index 72674a4..57f0949 100644 --- a/frontend/components/FilterPanel.tsx +++ b/frontend/components/FilterPanel.tsx @@ -127,6 +127,19 @@ export default function FilterPanel({ }) )}
+ + {/* Exclusive filter toggle */} + {localFilters.instruments && localFilters.instruments.length > 0 && ( + + )} {/* Key Filter */} diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 5076724..82abc3c 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -61,8 +61,9 @@ export interface FilterParams { key?: string instrument?: string instruments?: string[] // Multiple instruments filter + instruments_exclusive?: boolean // Only the selected instruments (no others) tempo_range?: 'slow' | 'medium' | 'fast' // Lent (<100), Moyen (100-140), Rapide (>140) - sort_by?: 'analyzed_at' | 'tempo_bpm' | 'duration_seconds' | 'filename' | 'energy' + sort_by?: 'analyzed_at' | 'tempo_bpm' | 'duration_seconds' | 'filename' | 'energy' | 'random' sort_desc?: boolean }