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).
41 lines
1.2 KiB
Python
41 lines
1.2 KiB
Python
"""Shared text-normalization helpers.
|
|
|
|
Extracted from `MusicDatabase._normalize_for_comparison` so callers
|
|
outside the database layer (matching engine, sync candidate pool,
|
|
import comparisons) don't have to reach across the module boundary
|
|
into a leading-underscore "private" method.
|
|
|
|
Pure functions, no I/O.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from unidecode import unidecode as _unidecode
|
|
_HAS_UNIDECODE = True
|
|
except ImportError:
|
|
_unidecode = None # type: ignore[assignment]
|
|
_HAS_UNIDECODE = False
|
|
logger.warning("unidecode not available, accent matching may be limited")
|
|
|
|
|
|
def normalize_for_comparison(text: str) -> str:
|
|
"""Lowercase + strip whitespace + fold accents to ASCII.
|
|
|
|
``é → e``, ``ñ → n``, ``Björk → bjork``. Used as the dictionary key
|
|
for the sync candidate pool and for fuzzy library lookups where
|
|
diacritic differences must NOT split a single artist into two pool
|
|
entries.
|
|
|
|
Empty / falsy input returns ``""`` so callers can blindly key dicts
|
|
with the result.
|
|
"""
|
|
if not text:
|
|
return ""
|
|
if _HAS_UNIDECODE:
|
|
text = _unidecode(text)
|
|
return text.lower().strip()
|