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:
|
try:
|
||||||
db = get_database()
|
db = get_database()
|
||||||
tracks = db.search_tracks(title=title, artist=artist, limit=limit)
|
tracks = db.api_search_tracks(title=title, artist=artist, limit=limit)
|
||||||
if not tracks:
|
return api_success({"tracks": [serialize_track(t, fields) for t in 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]})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return api_error("LIBRARY_ERROR", str(e), 500)
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5064,8 +5064,37 @@ class MusicDatabase:
|
||||||
logger.error(f"Error searching tracks with title='{title}', artist='{artist}': {e}")
|
logger.error(f"Error searching tracks with title='{title}', artist='{artist}': {e}")
|
||||||
return []
|
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]:
|
def _search_tracks_basic(self, cursor, title: str, artist: str, limit: int, server_source: str = None) -> List[DatabaseTrack]:
|
||||||
"""Basic SQL LIKE search - fastest method"""
|
"""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 = []
|
where_conditions = []
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
|
|
@ -5100,44 +5129,46 @@ class MusicDatabase:
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""", params)
|
""", 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]:
|
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"""
|
"""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
|
# Get broader results by searching for individual words
|
||||||
search_terms = []
|
search_terms = []
|
||||||
if title:
|
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]
|
title_words = [w.strip() for w in self._normalize_for_comparison(title).split() if len(w.strip()) >= 3]
|
||||||
search_terms.extend(title_words)
|
search_terms.extend(title_words)
|
||||||
|
|
||||||
if artist:
|
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]
|
artist_words = [w.strip() for w in self._normalize_for_comparison(artist).split() if len(w.strip()) >= 3]
|
||||||
search_terms.extend(artist_words)
|
search_terms.extend(artist_words)
|
||||||
|
|
||||||
if not search_terms:
|
if not search_terms:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Build a query that searches for any of the words
|
|
||||||
like_conditions = []
|
like_conditions = []
|
||||||
params = []
|
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 ?)")
|
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}%"])
|
params.extend([f"%{term}%", f"%{term}%", f"%{term}%"])
|
||||||
|
|
||||||
if not like_conditions:
|
if not like_conditions:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Build WHERE clause with optional server filter
|
|
||||||
where_parts = [f"({' OR '.join(like_conditions)})"]
|
where_parts = [f"({' OR '.join(like_conditions)})"]
|
||||||
if server_source:
|
if server_source:
|
||||||
where_parts.append("tracks.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)
|
where_clause = " AND ".join(where_parts)
|
||||||
params.append(limit * 3) # Get more results for scoring
|
params.append(limit * 3)
|
||||||
|
|
||||||
cursor.execute(f"""
|
cursor.execute(f"""
|
||||||
SELECT tracks.*, artists.name as artist_name, albums.title as album_title, albums.thumb_url as album_thumb_url
|
SELECT tracks.*, artists.name as artist_name, albums.title as album_title, albums.thumb_url as album_thumb_url
|
||||||
|
|
@ -5154,7 +5185,6 @@ class MusicDatabase:
|
||||||
# Score and filter results
|
# Score and filter results
|
||||||
scored_results = []
|
scored_results = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
# Simple scoring based on how many search terms match
|
|
||||||
score = 0
|
score = 0
|
||||||
db_title_lower = self._normalize_for_comparison(row['title'])
|
db_title_lower = self._normalize_for_comparison(row['title'])
|
||||||
db_artist_lower = self._normalize_for_comparison(row['artist_name'])
|
db_artist_lower = self._normalize_for_comparison(row['artist_name'])
|
||||||
|
|
@ -5166,11 +5196,8 @@ class MusicDatabase:
|
||||||
if score > 0:
|
if score > 0:
|
||||||
scored_results.append((score, row))
|
scored_results.append((score, row))
|
||||||
|
|
||||||
# Sort by score and take top results
|
|
||||||
scored_results.sort(key=lambda x: x[0], reverse=True)
|
scored_results.sort(key=lambda x: x[0], reverse=True)
|
||||||
top_rows = [row for score, row in scored_results[:limit]]
|
return [row for score, row in scored_results[:limit]]
|
||||||
|
|
||||||
return self._rows_to_tracks(top_rows)
|
|
||||||
|
|
||||||
def _rows_to_tracks(self, rows) -> List[DatabaseTrack]:
|
def _rows_to_tracks(self, rows) -> List[DatabaseTrack]:
|
||||||
"""Convert database rows to DatabaseTrack objects"""
|
"""Convert database rows to DatabaseTrack objects"""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue