Sokhi: songs in one album get mismatched cover art. Root cause is upstream of the repair jobs (which correctly apply one cover per album_id): the standalone import grouped albums by the album NAME hash (artist::album_name), so the SAME release split into multiple album rows whenever the name string drifted, and the cover-art/re-tag jobs then dressed each split row in its own art. Foundation (new imports only; existing rows untouched): a pure, seam-testable helper find_existing_soulsync_album_id() resolves the album row by precedence name-hash id -> source RELEASE id -> (title, artist). When an import carries a metadata-source album id, a differently-named import of the SAME release now unifies into one row instead of splitting. Source-column lookup is allow-listed (it's spliced into SQL) and guarded so a source without a dedicated album column (Deezer) falls through to the name match instead of breaking the import. Deliberate scope: this does NOT merge a track that genuinely matched a SINGLE (a different release id) into its parent album — that needs single->album resolution upstream and is the next step; this is the grouping substrate it will feed. 10 seam tests (canonical unify, single-vs-album stays separate, precedence, allowlist, server-source scope, missing-column fallthrough).
138 lines
5.6 KiB
Python
138 lines
5.6 KiB
Python
"""Seam tests for canonical album grouping (Sokhi: split album rows -> mixed
|
|
cover art). Drives find_existing_soulsync_album_id against a real in-memory
|
|
SQLite albums table — no app singletons, no I/O.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from core.imports.album_grouping import (
|
|
find_existing_soulsync_album_id,
|
|
ALLOWED_ALBUM_SOURCE_COLS,
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def cur():
|
|
conn = sqlite3.connect(":memory:")
|
|
conn.execute(
|
|
"""CREATE TABLE albums (
|
|
id TEXT PRIMARY KEY,
|
|
artist_id TEXT,
|
|
title TEXT,
|
|
server_source TEXT,
|
|
spotify_album_id TEXT,
|
|
itunes_album_id TEXT,
|
|
deezer_id TEXT,
|
|
soul_id TEXT,
|
|
discogs_id TEXT,
|
|
musicbrainz_release_id TEXT
|
|
)"""
|
|
)
|
|
yield conn.cursor()
|
|
conn.close()
|
|
|
|
|
|
def _add(cur, *, id, title, artist_id="art1", server_source="soulsync", **source_ids):
|
|
cols = ["id", "artist_id", "title", "server_source"] + list(source_ids)
|
|
vals = [id, artist_id, title, server_source] + list(source_ids.values())
|
|
cur.execute(
|
|
f"INSERT INTO albums ({', '.join(cols)}) VALUES ({', '.join(['?'] * len(cols))})",
|
|
vals,
|
|
)
|
|
|
|
|
|
def test_empty_db_returns_none(cur):
|
|
assert find_existing_soulsync_album_id(
|
|
cur, name_key_id="nk", artist_id="art1", album_name="Parachutes",
|
|
album_source_col="spotify_album_id", album_source_id="SP1") is None
|
|
|
|
|
|
def test_exact_name_hash_id_wins_first(cur):
|
|
_add(cur, id="nk", title="Parachutes")
|
|
assert find_existing_soulsync_album_id(
|
|
cur, name_key_id="nk", artist_id="art1", album_name="Parachutes") == "nk"
|
|
|
|
|
|
def test_canonical_source_id_unifies_differently_named_imports(cur):
|
|
# Existing row for release SP1 named "Parachutes". A second import of the
|
|
# SAME release id but a drifted name must JOIN it, not split.
|
|
_add(cur, id="existing", title="Parachutes", spotify_album_id="SP1")
|
|
got = find_existing_soulsync_album_id(
|
|
cur, name_key_id="different_hash", artist_id="art1",
|
|
album_name="Parachutes (Deluxe Edition)",
|
|
album_source_col="spotify_album_id", album_source_id="SP1")
|
|
assert got == "existing"
|
|
|
|
|
|
def test_different_release_id_stays_separate(cur):
|
|
# The single-vs-album case: a genuinely different release id must NOT merge
|
|
# (documents the known limit — single->album resolution is a separate step).
|
|
_add(cur, id="album_row", title="Parachutes", spotify_album_id="SP_ALBUM")
|
|
got = find_existing_soulsync_album_id(
|
|
cur, name_key_id="single_hash", artist_id="art1", album_name="Yellow",
|
|
album_source_col="spotify_album_id", album_source_id="SP_SINGLE")
|
|
assert got is None
|
|
|
|
|
|
def test_legacy_name_match_still_groups_without_a_source_id(cur):
|
|
_add(cur, id="byname", title="Parachutes")
|
|
got = find_existing_soulsync_album_id(
|
|
cur, name_key_id="other_hash", artist_id="art1", album_name="parachutes",
|
|
album_source_col=None, album_source_id=None)
|
|
assert got == "byname" # case-insensitive title + artist
|
|
|
|
|
|
def test_source_id_match_is_scoped_to_soulsync_rows(cur):
|
|
_add(cur, id="plexrow", title="Parachutes", server_source="plex", spotify_album_id="SP1")
|
|
got = find_existing_soulsync_album_id(
|
|
cur, name_key_id="nk", artist_id="art1", album_name="X",
|
|
album_source_col="spotify_album_id", album_source_id="SP1")
|
|
assert got is None # the matching row belongs to Plex, not soulsync
|
|
|
|
|
|
def test_non_allowlisted_column_is_ignored(cur):
|
|
# A column not on the allowlist must never be spliced into SQL.
|
|
assert "title" not in ALLOWED_ALBUM_SOURCE_COLS
|
|
_add(cur, id="row", title="Parachutes")
|
|
got = find_existing_soulsync_album_id(
|
|
cur, name_key_id="nk", artist_id="art1", album_name="nope",
|
|
album_source_col="title", album_source_id="Parachutes")
|
|
assert got is None # 'title' ignored as a source col; name 'nope' doesn't match
|
|
|
|
|
|
def test_empty_source_id_skips_canonical_match(cur):
|
|
_add(cur, id="row", title="Parachutes", spotify_album_id="")
|
|
got = find_existing_soulsync_album_id(
|
|
cur, name_key_id="nk", artist_id="art1", album_name="Other",
|
|
album_source_col="spotify_album_id", album_source_id="")
|
|
assert got is None
|
|
|
|
|
|
def test_missing_album_column_falls_through_not_raises(cur):
|
|
# Some sources (Deezer) don't have a dedicated album id column on the albums
|
|
# table; an allow-listed-but-absent column must NOT raise (it broke the whole
|
|
# import once) — it falls through to the name match.
|
|
cur.execute("CREATE TABLE albums_min (id TEXT, artist_id TEXT, title TEXT, server_source TEXT)")
|
|
cur.execute("INSERT INTO albums_min VALUES ('byname','art1','DZ Album','soulsync')")
|
|
# Point the helper at a table missing deezer_id by aliasing via a fresh cursor.
|
|
conn2 = sqlite3.connect(":memory:")
|
|
conn2.execute("CREATE TABLE albums (id TEXT, artist_id TEXT, title TEXT, server_source TEXT)")
|
|
conn2.execute("INSERT INTO albums VALUES ('byname','art1','DZ Album','soulsync')")
|
|
c2 = conn2.cursor()
|
|
got = find_existing_soulsync_album_id(
|
|
c2, name_key_id="nk", artist_id="art1", album_name="DZ Album",
|
|
album_source_col="deezer_id", album_source_id="67890")
|
|
conn2.close()
|
|
assert got == "byname" # deezer_id column absent -> fell through to name match
|
|
|
|
|
|
def test_musicbrainz_release_id_grouping(cur):
|
|
_add(cur, id="mbrow", title="Album", musicbrainz_release_id="mb-123")
|
|
got = find_existing_soulsync_album_id(
|
|
cur, name_key_id="nk2", artist_id="art1", album_name="Album (Remaster)",
|
|
album_source_col="musicbrainz_release_id", album_source_id="mb-123")
|
|
assert got == "mbrow"
|