fix: eliminate double-query in track search
The /api/v1/library/tracks endpoint called search_tracks() to get DatabaseTrack objects, then immediately called api_get_tracks_by_ids() to re-hydrate full rows for serialization. Two round trips per search. Added api_search_tracks() that returns dict rows with all track columns plus artist_name, album_title, and album_thumb_url in a single query. The basic and fuzzy search helpers were refactored to share raw-row implementations, so the existing search_tracks() still returns DatabaseTrack objects for the many internal callers that depend on that shape (matching pipeline, repair worker, web UI search).
This commit is contained in:
parent
2f19af779b
commit
6b6fdba3fd
2 changed files with 56 additions and 36 deletions
|
|
@ -186,15 +186,8 @@ def register_routes(bp):
|
|||
|
||||
try:
|
||||
db = get_database()
|
||||
tracks = db.search_tracks(title=title, artist=artist, limit=limit)
|
||||
if not tracks:
|
||||
return api_success({"tracks": []})
|
||||
|
||||
# Re-query by IDs to get full row data
|
||||
track_ids = [t.id for t in tracks]
|
||||
full_tracks = db.api_get_tracks_by_ids(track_ids)
|
||||
|
||||
return api_success({"tracks": [serialize_track(t, fields) for t in full_tracks]})
|
||||
tracks = db.api_search_tracks(title=title, artist=artist, limit=limit)
|
||||
return api_success({"tracks": [serialize_track(t, fields) for t in tracks]})
|
||||
except Exception as e:
|
||||
return api_error("LIBRARY_ERROR", str(e), 500)
|
||||
|
||||
|
|
|
|||
|
|
@ -5063,12 +5063,41 @@ class MusicDatabase:
|
|||
except Exception as e:
|
||||
logger.error(f"Error searching tracks with title='{title}', artist='{artist}': {e}")
|
||||
return []
|
||||
|
||||
def api_search_tracks(self, title: str = "", artist: str = "", limit: int = 50,
|
||||
server_source: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Search tracks and return full dict rows (all track columns plus artist_name,
|
||||
album_title, album_thumb_url). Avoids the double-query pattern of calling
|
||||
search_tracks() followed by api_get_tracks_by_ids().
|
||||
"""
|
||||
try:
|
||||
if not title and not artist:
|
||||
return []
|
||||
|
||||
conn = self._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
basic_rows = self._search_tracks_basic_rows(cursor, title, artist, limit, server_source)
|
||||
if basic_rows:
|
||||
return [dict(r) for r in basic_rows]
|
||||
|
||||
fuzzy_rows = self._search_tracks_fuzzy_rows(cursor, title, artist, limit, server_source)
|
||||
return [dict(r) for r in fuzzy_rows]
|
||||
except Exception as e:
|
||||
logger.error(f"API: Error searching tracks with title='{title}', artist='{artist}': {e}")
|
||||
return []
|
||||
|
||||
def _search_tracks_basic(self, cursor, title: str, artist: str, limit: int, server_source: str = None) -> List[DatabaseTrack]:
|
||||
"""Basic SQL LIKE search - fastest method"""
|
||||
rows = self._search_tracks_basic_rows(cursor, title, artist, limit, server_source)
|
||||
return self._rows_to_tracks(rows)
|
||||
|
||||
def _search_tracks_basic_rows(self, cursor, title: str, artist: str, limit: int,
|
||||
server_source: Optional[str] = None):
|
||||
"""Basic SQL LIKE search returning raw rows (shared by DatabaseTrack and dict-returning callers)."""
|
||||
where_conditions = []
|
||||
params = []
|
||||
|
||||
|
||||
if title:
|
||||
where_conditions.append("unidecode_lower(tracks.title) LIKE ?")
|
||||
params.append(f"%{self._normalize_for_comparison(title)}%")
|
||||
|
|
@ -5083,13 +5112,13 @@ class MusicDatabase:
|
|||
if server_source:
|
||||
where_conditions.append("tracks.server_source = ?")
|
||||
params.append(server_source)
|
||||
|
||||
|
||||
if not where_conditions:
|
||||
return []
|
||||
|
||||
|
||||
where_clause = " AND ".join(where_conditions)
|
||||
params.append(limit)
|
||||
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT tracks.*, artists.name as artist_name, albums.title as album_title, albums.thumb_url as album_thumb_url
|
||||
FROM tracks
|
||||
|
|
@ -5100,45 +5129,47 @@ class MusicDatabase:
|
|||
LIMIT ?
|
||||
""", params)
|
||||
|
||||
return self._rows_to_tracks(cursor.fetchall())
|
||||
return cursor.fetchall()
|
||||
|
||||
def _search_tracks_fuzzy_fallback(self, cursor, title: str, artist: str, limit: int, server_source: str = None) -> List[DatabaseTrack]:
|
||||
"""Broadest fuzzy search - partial word matching"""
|
||||
rows = self._search_tracks_fuzzy_rows(cursor, title, artist, limit, server_source)
|
||||
return self._rows_to_tracks(rows)
|
||||
|
||||
def _search_tracks_fuzzy_rows(self, cursor, title: str, artist: str, limit: int,
|
||||
server_source: Optional[str] = None):
|
||||
"""Broadest fuzzy search returning raw rows (shared by DatabaseTrack and dict-returning callers)."""
|
||||
# Get broader results by searching for individual words
|
||||
search_terms = []
|
||||
if title:
|
||||
# Split title into words and search for each (normalized for diacritics)
|
||||
title_words = [w.strip() for w in self._normalize_for_comparison(title).split() if len(w.strip()) >= 3]
|
||||
search_terms.extend(title_words)
|
||||
|
||||
if artist:
|
||||
# Split artist into words and search for each (normalized for diacritics)
|
||||
artist_words = [w.strip() for w in self._normalize_for_comparison(artist).split() if len(w.strip()) >= 3]
|
||||
search_terms.extend(artist_words)
|
||||
|
||||
|
||||
if not search_terms:
|
||||
return []
|
||||
|
||||
# Build a query that searches for any of the words
|
||||
|
||||
like_conditions = []
|
||||
params = []
|
||||
|
||||
for term in search_terms[:5]: # Limit to 5 terms to avoid too broad search
|
||||
|
||||
for term in search_terms[:5]:
|
||||
like_conditions.append("(unidecode_lower(tracks.title) LIKE ? OR unidecode_lower(artists.name) LIKE ? OR unidecode_lower(COALESCE(tracks.track_artist, '')) LIKE ?)")
|
||||
params.extend([f"%{term}%", f"%{term}%", f"%{term}%"])
|
||||
|
||||
|
||||
if not like_conditions:
|
||||
return []
|
||||
|
||||
# Build WHERE clause with optional server filter
|
||||
|
||||
where_parts = [f"({' OR '.join(like_conditions)})"]
|
||||
if server_source:
|
||||
where_parts.append("tracks.server_source = ?")
|
||||
params.append(server_source) # Append after LIKE params, before LIMIT
|
||||
|
||||
params.append(server_source)
|
||||
|
||||
where_clause = " AND ".join(where_parts)
|
||||
params.append(limit * 3) # Get more results for scoring
|
||||
|
||||
params.append(limit * 3)
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT tracks.*, artists.name as artist_name, albums.title as album_title, albums.thumb_url as album_thumb_url
|
||||
FROM tracks
|
||||
|
|
@ -5154,23 +5185,19 @@ class MusicDatabase:
|
|||
# Score and filter results
|
||||
scored_results = []
|
||||
for row in rows:
|
||||
# Simple scoring based on how many search terms match
|
||||
score = 0
|
||||
db_title_lower = self._normalize_for_comparison(row['title'])
|
||||
db_artist_lower = self._normalize_for_comparison(row['artist_name'])
|
||||
|
||||
|
||||
for term in search_terms:
|
||||
if term in db_title_lower or term in db_artist_lower:
|
||||
score += 1
|
||||
|
||||
|
||||
if score > 0:
|
||||
scored_results.append((score, row))
|
||||
|
||||
# Sort by score and take top results
|
||||
|
||||
scored_results.sort(key=lambda x: x[0], reverse=True)
|
||||
top_rows = [row for score, row in scored_results[:limit]]
|
||||
|
||||
return self._rows_to_tracks(top_rows)
|
||||
return [row for score, row in scored_results[:limit]]
|
||||
|
||||
def _rows_to_tracks(self, rows) -> List[DatabaseTrack]:
|
||||
"""Convert database rows to DatabaseTrack objects"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue