Fix Track Match search ignoring Track/Artist fields and low result limit

- Frontend was concatenating Track and Artist inputs into a single
  query string, causing Spotify to return mixed results matching
  either word in any field. Now sends track and artist as separate
  params; backend builds field-filtered query (track:X artist:Y).
- Result limit was silently capped at 10 in spotify_client.search_tracks
  via min(limit, 10). Raised to respect requested limit up to
  Spotify's API max of 50.
- iTunes fallback endpoint updated with same field-specific params.
- Legacy ?query= param still supported for backward compatibility.

Fixes #194
This commit is contained in:
Broque Thomas 2026-03-19 08:23:19 -07:00
parent 0a3cca4f1f
commit 99481a0232
3 changed files with 51 additions and 13 deletions

View file

@ -955,7 +955,8 @@ class SpotifyClient:
if use_spotify: if use_spotify:
# Check Spotify cache # Check Spotify cache
cached_results = cache.get_search_results('spotify', 'track', query, min(limit, 10)) effective_limit = min(limit, 50) # Spotify API max is 50
cached_results = cache.get_search_results('spotify', 'track', query, effective_limit)
if cached_results is not None: if cached_results is not None:
tracks = [] tracks = []
for raw in cached_results: for raw in cached_results:
@ -967,7 +968,7 @@ class SpotifyClient:
return tracks return tracks
try: try:
results = self.sp.search(q=query, type='track', limit=min(limit, 10)) results = self.sp.search(q=query, type='track', limit=effective_limit)
tracks = [] tracks = []
raw_items = results['tracks']['items'] raw_items = results['tracks']['items']
@ -979,7 +980,7 @@ class SpotifyClient:
entries = [(td.get('id'), td) for td in raw_items if td.get('id')] entries = [(td.get('id'), td) for td in raw_items if td.get('id')]
if entries: if entries:
cache.store_entities_bulk('spotify', 'track', entries) cache.store_entities_bulk('spotify', 'track', entries)
cache.store_search_results('spotify', 'track', query, min(limit, 10), cache.store_search_results('spotify', 'track', query, effective_limit,
[td.get('id') for td in raw_items if td.get('id')]) [td.get('id') for td in raw_items if td.get('id')])
return tracks return tracks

View file

@ -25069,10 +25069,23 @@ def search_spotify_tracks():
return jsonify({"error": "Spotify not authenticated."}), 401 return jsonify({"error": "Spotify not authenticated."}), 401
try: try:
query = request.args.get('query', '').strip() # Support field-specific search params (track, artist) or legacy combined query
track_q = request.args.get('track', '').strip()
artist_q = request.args.get('artist', '').strip()
legacy_query = request.args.get('query', '').strip()
limit = int(request.args.get('limit', 20)) limit = int(request.args.get('limit', 20))
if not query: # Build Spotify field-filtered query
if track_q or artist_q:
parts = []
if track_q:
parts.append(f'track:{track_q}')
if artist_q:
parts.append(f'artist:{artist_q}')
query = ' '.join(parts)
elif legacy_query:
query = legacy_query
else:
return jsonify({"error": "Query parameter is required"}), 400 return jsonify({"error": "Query parameter is required"}), 400
if use_hydrabase: if use_hydrabase:
@ -25101,10 +25114,22 @@ def search_spotify_tracks():
def search_itunes_tracks(): def search_itunes_tracks():
"""Search for tracks on iTunes - used by discovery fix modal when iTunes is the source""" """Search for tracks on iTunes - used by discovery fix modal when iTunes is the source"""
try: try:
query = request.args.get('query', '').strip() # Support field-specific search params or legacy combined query
track_q = request.args.get('track', '').strip()
artist_q = request.args.get('artist', '').strip()
legacy_query = request.args.get('query', '').strip()
limit = int(request.args.get('limit', 20)) limit = int(request.args.get('limit', 20))
if not query: if track_q or artist_q:
parts = []
if track_q:
parts.append(track_q)
if artist_q:
parts.append(artist_q)
query = ' '.join(parts)
elif legacy_query:
query = legacy_query
else:
return jsonify({"error": "Query parameter is required"}), 400 return jsonify({"error": "Query parameter is required"}), 400
use_hydrabase = _is_hydrabase_active() use_hydrabase = _is_hydrabase_active()

View file

@ -15392,12 +15392,19 @@ async function searchDiscoveryFix() {
resultsContainer.innerHTML = `<div class="loading">🔍 Searching ${sourceLabel}...</div>`; resultsContainer.innerHTML = `<div class="loading">🔍 Searching ${sourceLabel}...</div>`;
try { try {
// Build search query // Build search params — send track and artist separately for field-specific filtering
const query = `${artistInput} ${trackInput}`.trim(); const params = new URLSearchParams();
if (trackInput) params.set('track', trackInput);
if (artistInput) params.set('artist', artistInput);
if (!trackInput && !artistInput) {
resultsContainer.innerHTML = '<div class="no-results">Enter a track name or artist.</div>';
return;
}
params.set('limit', '50');
// Call appropriate search API based on discovery source // Call appropriate search API based on discovery source
const searchEndpoint = useFallback ? '/api/itunes/search_tracks' : '/api/spotify/search_tracks'; const searchEndpoint = useFallback ? '/api/itunes/search_tracks' : '/api/spotify/search_tracks';
const response = await fetch(`${searchEndpoint}?query=${encodeURIComponent(query)}&limit=20`); const response = await fetch(`${searchEndpoint}?${params.toString()}`);
const data = await response.json(); const data = await response.json();
if (data.error) { if (data.error) {
@ -55496,8 +55503,9 @@ async function searchPoolFix() {
const resultsContainer = document.getElementById('pool-fix-results'); const resultsContainer = document.getElementById('pool-fix-results');
if (!trackInput || !resultsContainer) return; if (!trackInput || !resultsContainer) return;
const query = `${artistInput.value.trim()} ${trackInput.value.trim()}`.trim(); const trackVal = trackInput.value.trim();
if (!query) { const artistVal = artistInput.value.trim();
if (!trackVal && !artistVal) {
resultsContainer.innerHTML = '<div class="pool-empty">Enter a search term</div>'; resultsContainer.innerHTML = '<div class="pool-empty">Enter a search term</div>';
return; return;
} }
@ -55505,7 +55513,11 @@ async function searchPoolFix() {
resultsContainer.innerHTML = '<div class="pool-empty">Searching...</div>'; resultsContainer.innerHTML = '<div class="pool-empty">Searching...</div>';
try { try {
const res = await fetch(`/api/spotify/search_tracks?query=${encodeURIComponent(query)}&limit=20`); const params = new URLSearchParams();
if (trackVal) params.set('track', trackVal);
if (artistVal) params.set('artist', artistVal);
params.set('limit', '50');
const res = await fetch(`/api/spotify/search_tracks?${params.toString()}`);
const data = await res.json(); const data = await res.json();
const tracks = data.tracks || []; const tracks = data.tracks || [];