From ed010fa60fd13a717b50048766a7611722515ecc Mon Sep 17 00:00:00 2001 From: Benoit Date: Tue, 27 Jan 2026 22:43:32 +0100 Subject: [PATCH] Fix #5 Fix #6 Fix #7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Tri aléatoire ✅ Ajout d'un sélecteur de tri dans l'en-tête avec options "Récents" et "Aléatoire" Backend : nouveau paramètre sort_by="random" qui utilise func.random() de PostgreSQL Les résultats seront maintenant mélangés aléatoirement, permettant de découvrir tous les titres de la bibliothèque 2. Correction du bug de recherche textuelle ✅ La recherche charge maintenant jusqu'à 10 000 résultats au lieu de 25 quand une requête est active La recherche filtre ensuite sur TOUS les résultats chargés, pas seulement la page active Cela permet de rechercher dans toute la bibliothèque de résultats filtrés 3. Filtres exclusifs pour instruments ✅ Nouvelle option "Uniquement ces instruments (mode exclusif)" qui apparaît quand des instruments sont sélectionnés Backend : nouveau paramètre instruments_exclusive qui vérifie que le track a EXACTEMENT les instruments sélectionnés (pas d'autres) Frontend : checkbox dans un bandeau orange/ambre pour activer le mode exclusif Les modifications touchent : Backend : crud.py et tracks.py Frontend : types.ts, FilterPanel.tsx et page.tsx --- backend/src/api/routes/tracks.py | 7 +++++-- backend/src/models/crud.py | 25 ++++++++++++++++++----- frontend/app/page.tsx | 31 +++++++++++++++++++++++++---- frontend/components/FilterPanel.tsx | 13 ++++++++++++ frontend/lib/types.ts | 3 ++- 5 files changed, 67 insertions(+), 12 deletions(-) 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 }