soulsync/core/imports/album_grouping.py
BoulderBadgeDad 2ecbd8badc lint: log the skipped album source-id lookup instead of a bare try/except/pass (ruff S110)
The Deezer missing-column fallthrough in find_existing_soulsync_album_id used a
bare 'except: pass', which ruff flags as S110. Log it at debug instead — same
fail-safe behaviour, no swallowed-exception lint warning.
2026-06-18 12:09:22 -07:00

98 lines
4 KiB
Python

"""Canonical album grouping for the SoulSync standalone import.
SoulSync grouped imported tracks into albums by the album NAME string
(``_stable_soulsync_id("artist::album_name")``). That splits one release into
several album rows whenever the name string drifts between imports (case,
punctuation, ``(Deluxe Edition)`` suffixes, source-A-vs-B spelling), and every
downstream tool (Library Re-tag, Cover-Art Filler) then dresses each split row
in its own cover — so songs that belong to one album end up with different art
(Sokhi).
This module is the pure, seam-testable heart of "group by canonical id, not
name": when an imported track carries a metadata-source RELEASE id, prefer
matching an existing album row by that id over the fragile name string, so the
SAME release always lands in ONE album row regardless of how its name was typed.
Scope (deliberate): this unifies differently-named imports of the SAME release.
It does NOT merge a track that genuinely matched a SINGLE release (a different
release id) into its parent album — that needs single->album resolution upstream
and is a separate change. New imports only; existing rows are left untouched.
Pure SQL-over-a-cursor; no app singletons, so it tests against an in-memory DB.
"""
from __future__ import annotations
from typing import Any, Optional
from utils.logging_config import get_logger
logger = get_logger("imports.album_grouping")
# Album source-id columns this grouping may key on. An allowlist (not arbitrary
# interpolation) — the column name IS spliced into SQL, so it must be a known,
# trusted identifier. Mirrors get_library_source_id_columns()' 'album' values.
ALLOWED_ALBUM_SOURCE_COLS = frozenset({
"spotify_album_id",
"itunes_album_id",
"deezer_id",
"soul_id",
"discogs_id",
"musicbrainz_release_id",
})
def find_existing_soulsync_album_id(
cursor: Any,
*,
name_key_id: str,
artist_id: str,
album_name: str,
album_source_col: Optional[str] = None,
album_source_id: Optional[str] = None,
) -> Optional[str]:
"""Resolve the existing ``soulsync`` album row a track should join, or None
(caller inserts a new row keyed by ``name_key_id``).
Match precedence:
1. ``name_key_id`` — the exact prior stable-name-hash id (unchanged
behaviour: a re-import with the identical name hits its own row).
2. ``album_source_col == album_source_id`` — CANONICAL grouping: an
existing row already carrying THIS release's source id, so a
differently-named import of the same release unifies instead of
splitting. Only when the column is allow-listed and the id is non-empty.
3. ``(title, artist_id)`` — the legacy name match (kept so nothing that
grouped before stops grouping now).
"""
cursor.execute(
"SELECT id FROM albums WHERE id = ? AND server_source = 'soulsync'",
(name_key_id,),
)
row = cursor.fetchone()
if row:
return row[0]
if album_source_col in ALLOWED_ALBUM_SOURCE_COLS and album_source_id:
try:
cursor.execute(
f"SELECT id FROM albums WHERE {album_source_col} = ? "
"AND server_source = 'soulsync' LIMIT 1",
(album_source_id,),
)
row = cursor.fetchone()
if row:
return row[0]
except Exception as exc:
# That source has no dedicated album column on this DB (e.g. Deezer
# doesn't split per-entity id columns) — fall through to the name
# match rather than break the import. Mirrors the guarded source-id
# UPDATE the caller already does on insert.
logger.debug("album source-id lookup skipped (%s): %s", album_source_col, exc)
cursor.execute(
"SELECT id FROM albums WHERE title COLLATE NOCASE = ? AND artist_id = ? "
"AND server_source = 'soulsync' LIMIT 1",
(album_name, artist_id),
)
row = cursor.fetchone()
return row[0] if row else None