Fix #5 Fix #6 Fix #7
All checks were successful
Build and Push Docker Images / Build Backend Image (push) Successful in 14m9s
Build and Push Docker Images / Build Frontend Image (push) Successful in 7m36s

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
This commit is contained in:
2026-01-27 22:43:32 +01:00
parent 6e1adf7422
commit ed010fa60f
5 changed files with 67 additions and 12 deletions

View File

@@ -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,
)

View File

@@ -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())

View File

@@ -57,25 +57,35 @@ export default function Home() {
const [searchQuery, setSearchQuery] = useState("")
const [isScanning, setIsScanning] = useState(false)
const [scanStatus, setScanStatus] = useState<string>("")
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() {
</div>
<div className="ml-6 flex items-center gap-3">
{/* Sort order toggle */}
<select
value={sortBy}
onChange={(e) => {
setSortBy(e.target.value as 'analyzed_at' | 'random')
setPage(0)
}}
className="px-3 py-2 text-sm bg-white border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 text-slate-700"
>
<option value="analyzed_at">Récents</option>
<option value="random">Aléatoire</option>
</select>
<div className="text-sm text-slate-600">
{tracksData?.total || 0} piste{(tracksData?.total || 0) > 1 ? 's' : ''}
</div>

View File

@@ -127,6 +127,19 @@ export default function FilterPanel({
})
)}
</div>
{/* Exclusive filter toggle */}
{localFilters.instruments && localFilters.instruments.length > 0 && (
<label className="flex items-center gap-2 mt-2 p-2 bg-amber-50 border border-amber-200 rounded-lg cursor-pointer hover:bg-amber-100 transition-colors">
<input
type="checkbox"
checked={localFilters.instruments_exclusive || false}
onChange={(e) => handleFilterChange("instruments_exclusive", e.target.checked || undefined)}
className="w-4 h-4 text-amber-600 border-amber-300 rounded focus:ring-2 focus:ring-amber-500 focus:ring-offset-0"
/>
<span className="text-xs font-medium text-amber-800">Uniquement ces instruments (mode exclusif)</span>
</label>
)}
</div>
{/* Key Filter */}

View File

@@ -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
}