soulsync/tests/test_search_tracks_relevance.py
BoulderBadgeDad 27d738e7b1 Fix: Find & Add library search buried exact matches (case-sensitive ordering)
Reported via Find & Add (Billie Eilish "bad guy"): the track was in the library
and on Plex, but never showed in the modal's 20 results. Root cause (proven
against the real 307k-track DB): the search did `ORDER BY tracks.title`, which is
case-SENSITIVE in SQLite (BINARY collation sorts 'B' before 'b'). Billie's title
is lowercase "bad guy"; everyone else's is "Bad Guy", so all the capitalised ones
sorted first, filled the LIMIT, and her exact match landed at ~#25 — cut off.

- search_tracks now ranks by relevance: exact title match first (case-insensitive
  via unidecode_lower), then prefix, then alphabetical — so an exact match can't
  be sorted below the limit by a capital letter. Helps every caller.
- Added a rank-only `rank_artist` hint (never filters): Find & Add already knows
  the source track's artist, so it now passes it and the exact title+artist match
  floats to #1. Filtering was deliberately avoided — if the track is tagged under
  a slightly different artist on the server, a filter would re-hide it.

Verified on the real DB: title-only "bad guy" now surfaces Billie at #4 (was
>#20); with the artist hint she's #1. Seam tests: lowercase exact title isn't
buried; rank hint floats the match without filtering; exact title beats a
superstring title. 10 tests pass.
2026-06-10 17:23:13 -07:00

68 lines
2.9 KiB
Python

"""Find & Add library search relevance (Billie Eilish 'bad guy' report).
Root cause proven against the real DB: `ORDER BY tracks.title` is case-SENSITIVE
(SQLite BINARY sorts 'B' before 'b'), so a lowercase exact title like Billie
Eilish's "bad guy" sorted BELOW every capitalised "Bad Guy" and fell past the
result LIMIT — it never showed in the modal even though it was in the library.
Fix: rank by relevance (exact title first, case-insensitive), and accept a
rank-only artist hint so an exact title+artist match wins — without FILTERING
(filtering would re-hide the track if it's tagged under a slightly different
artist on the server).
"""
from __future__ import annotations
import pytest
from database.music_database import MusicDatabase
@pytest.fixture
def db(tmp_path):
return MusicDatabase(str(tmp_path / "music.db"))
def _insert(db, tid, title, artist_id, artist_name):
with db._get_connection() as conn:
conn.execute("INSERT OR IGNORE INTO artists (id, name) VALUES (?, ?)", (artist_id, artist_name))
conn.execute("INSERT OR IGNORE INTO albums (id, title, artist_id) VALUES (?, ?, ?)", (artist_id, "Alb", artist_id))
conn.execute(
"INSERT INTO tracks (id, album_id, artist_id, title, track_number, duration, file_path) "
"VALUES (?, ?, ?, ?, 1, 180, ?)",
(tid, artist_id, artist_id, title, f"/m/{tid}.mp3"),
)
conn.commit()
def test_lowercase_exact_title_not_buried_by_case(db):
# Capitalised "Bad Guy" tracks would (pre-fix) fill the LIMIT and sort
# the lowercase "bad guy" below them, cutting it off.
_insert(db, 1, "Bad Guy", 1, "Yara")
_insert(db, 2, "Bad Guy", 2, "Zelda")
_insert(db, 3, "bad guy", 3, "Billie Eilish")
names = [t.artist_name for t in db.search_tracks(title="bad guy", limit=2)]
assert "Billie Eilish" in names, "lowercase exact title must not be sorted past the limit"
def test_rank_artist_hint_floats_match_to_top_without_filtering(db):
_insert(db, 1, "Bad Guy", 1, "Aaa Artist")
_insert(db, 2, "Bad Guy", 2, "Bbb Artist")
_insert(db, 3, "bad guy", 3, "Billie Eilish")
results = db.search_tracks(title="bad guy", limit=10, rank_artist="Billie Eilish")
names = [t.artist_name for t in results]
assert names[0] == "Billie Eilish", "the hinted artist's exact match should rank first"
# …but it must NOT filter — the other artists' versions are still there.
assert len(results) == 3
assert {"Aaa Artist", "Bbb Artist"} <= set(names)
def test_exact_title_outranks_superstring_title(db):
# "bad guy" should beat "Bad Guy Necessity" / "Bad Guys" for the query.
_insert(db, 1, "Bad Guy Necessity", 1, "Aardvark") # would sort first alphabetically
_insert(db, 2, "bad guy", 2, "Billie Eilish")
top = db.search_tracks(title="bad guy", limit=5)[0]
assert top.title.lower() == "bad guy" and top.artist_name == "Billie Eilish"