Three changes folded into one perf+cleanup pass: 1. Indexed fast path for the per-artist pool fetch. The previous `search_tracks(artist=name)` call hit `unidecode_lower(artists.name) LIKE ?`, a function-in-WHERE that can't use `idx_artists_name`. New `MusicDatabase.get_artist_tracks_indexed` does a two-step lookup: exact-name match (indexed) plus a case-insensitive fallback, then `tracks WHERE artist_id IN (...)` via `idx_tracks_artist_id`. Drops per-artist fetch from seconds to milliseconds for the common case. The sync helper falls back to the old LIKE-based `search_tracks` only when the indexed lookup finds nothing, preserving diacritic recall and `tracks.track_artist` feature-artist matches with zero regression. 2. Public text-normalization helper. Lifted the body of `MusicDatabase._normalize_for_comparison` into `core/text/normalize.py:normalize_for_comparison` so callers outside the database layer (matching engine, sync pool, future import-side comparisons) don't reach across the module boundary into a leading-underscore "private" method. The DB method now delegates, so existing internal call sites stay untouched. Sync's lazy pool now imports the public helper. 3. Artist-name walker extracted. `_artist_name` at module level in `services/sync_service.py` replaces two near-identical inline str-or-dict-or-fallback walkers (one in `sync_playlist`, one in `_find_track_in_media_server`). Returns `''` for None instead of the literal string `'None'`. Plus three small tidies from the same review: - `_POOL_FETCH_LIMIT = 10000` constant in place of the literal at the pool-fetch call site. - Trimmed the verbose docstring + comment block on the pool helper. - Set-intersection predicate for the trigger-shape reset in `core/automation/api.py` instead of a two-line `or` chain. Also removed the duplicate `_get_active_media_client()` call at sync_service.py:212/214 — pre-existing wart that was sitting in the same block I was editing. Tests: 21 new tests across `tests/database/`, `tests/sync/`, and `tests/text/`, plus updates to the existing pool tests to cover the new fast/fallback split. Full suite stays green (3953 passing).
43 lines
1.3 KiB
Python
43 lines
1.3 KiB
Python
"""Tests for `_artist_name` in services/sync_service.py — the helper
|
|
that pulls a string name out of Spotify's bare-string / dict / fallback
|
|
artist representations."""
|
|
|
|
from services.sync_service import _artist_name
|
|
|
|
|
|
def test_bare_string_returned_as_is():
|
|
assert _artist_name('Drake') == 'Drake'
|
|
|
|
|
|
def test_dict_with_name_field():
|
|
assert _artist_name({'name': 'Drake', 'id': '3TVXt'}) == 'Drake'
|
|
|
|
|
|
def test_dict_without_name_field_falls_back_to_str_repr():
|
|
"""Missing name field shouldn't crash — caller should still get a
|
|
string back, even if it's the awkward dict repr."""
|
|
out = _artist_name({'id': '3TVXt'})
|
|
assert isinstance(out, str)
|
|
assert out != ''
|
|
|
|
|
|
def test_dict_with_non_string_name_falls_back():
|
|
"""Defensive — if some endpoint ever returns {name: None} or a list,
|
|
the helper must not propagate the bad type."""
|
|
out = _artist_name({'name': None})
|
|
assert isinstance(out, str)
|
|
|
|
|
|
def test_none_returns_empty_string():
|
|
assert _artist_name(None) == ''
|
|
|
|
|
|
def test_unexpected_type_returns_string_repr():
|
|
"""A weird type (int, custom object) must coerce to a string instead
|
|
of raising — sync iterates a lot of inputs and one bad row shouldn't
|
|
crash the whole loop."""
|
|
assert _artist_name(12345) == '12345'
|
|
|
|
|
|
def test_empty_string_stays_empty():
|
|
assert _artist_name('') == ''
|