feat(discovery): derive core-album suggestions for surfaced artists
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ from dataclasses import dataclass
|
|||||||
import psycopg
|
import psycopg
|
||||||
|
|
||||||
from lyra_worker.browser import MbBrowser
|
from lyra_worker.browser import MbBrowser
|
||||||
|
from lyra_worker.release_filter import is_core_release
|
||||||
from lyra_worker.similarity.base import SimilaritySource
|
from lyra_worker.similarity.base import SimilaritySource
|
||||||
|
|
||||||
_TRUE = {"1", "true", "yes", "on"}
|
_TRUE = {"1", "true", "yes", "on"}
|
||||||
@@ -82,9 +83,50 @@ def _upsert_artist(conn: psycopg.Connection, cand: dict) -> int:
|
|||||||
return cur.rowcount
|
return cur.rowcount
|
||||||
|
|
||||||
|
|
||||||
|
def _existing_rg_mbids(conn: psycopg.Connection) -> set[str]:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute('SELECT "rgMbid" FROM "MonitoredRelease"')
|
||||||
|
return {r[0] for r in cur.fetchall()}
|
||||||
|
|
||||||
|
|
||||||
|
def _upsert_album(conn: psycopg.Connection, artist: dict, rg) -> int:
|
||||||
|
dedupe = f"album:{artist['mbid']}:{rg.rg_mbid}"
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
'INSERT INTO "DiscoverySuggestion" (id, kind, "artistMbid", "artistName", '
|
||||||
|
'"rgMbid", album, "primaryType", "secondaryTypes", "firstReleaseDate", '
|
||||||
|
'score, "seedCount", sources, status, "dedupeKey", "createdAt", "updatedAt") '
|
||||||
|
"VALUES (gen_random_uuid()::text, 'album', %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, "
|
||||||
|
"'pending', %s, now(), now()) "
|
||||||
|
'ON CONFLICT ("dedupeKey") DO UPDATE SET score = EXCLUDED.score, '
|
||||||
|
'"updatedAt" = now() '
|
||||||
|
"WHERE \"DiscoverySuggestion\".status = 'pending'",
|
||||||
|
(artist["mbid"], artist["name"], rg.rg_mbid, rg.title,
|
||||||
|
rg.primary_type or None, list(rg.secondary_types), rg.first_release_date or None,
|
||||||
|
artist["score"], artist["seed_count"], sorted(artist["sources"]), dedupe),
|
||||||
|
)
|
||||||
|
return cur.rowcount
|
||||||
|
|
||||||
|
|
||||||
def _derive_albums(conn: psycopg.Connection, browser: MbBrowser, surfaced: list[dict],
|
def _derive_albums(conn: psycopg.Connection, browser: MbBrowser, surfaced: list[dict],
|
||||||
cfg: DiscoveryConfig) -> int:
|
cfg: DiscoveryConfig) -> int:
|
||||||
return 0 # replaced in the album-derivation task
|
if cfg.albums_per_artist <= 0:
|
||||||
|
return 0
|
||||||
|
have = _existing_rg_mbids(conn)
|
||||||
|
albums = 0
|
||||||
|
for artist in surfaced:
|
||||||
|
try:
|
||||||
|
groups = browser.browse_release_groups(artist["mbid"])
|
||||||
|
except Exception as e: # one artist's browse failure must not abort the sweep
|
||||||
|
print(f"worker: discovery album browse failed for {artist['mbid']}: {e}", flush=True)
|
||||||
|
continue
|
||||||
|
core = [g for g in groups
|
||||||
|
if is_core_release(g.primary_type, g.secondary_types) and g.rg_mbid not in have]
|
||||||
|
core.sort(key=lambda g: g.first_release_date or "", reverse=True)
|
||||||
|
for rg in core[: cfg.albums_per_artist]:
|
||||||
|
albums += _upsert_album(conn, artist, rg)
|
||||||
|
have.add(rg.rg_mbid)
|
||||||
|
return albums
|
||||||
|
|
||||||
|
|
||||||
def run_discovery(conn: psycopg.Connection, sources: list[SimilaritySource],
|
def run_discovery(conn: psycopg.Connection, sources: list[SimilaritySource],
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
_CORE_PRIMARY = {"Album", "Single", "EP"}
|
||||||
|
|
||||||
|
|
||||||
|
def is_core_release(primary_type: str, secondary_types) -> bool:
|
||||||
|
"""A 'core' studio release: Album/Single/EP with no secondary types (live, comp, etc.)."""
|
||||||
|
return (primary_type or "") in _CORE_PRIMARY and len(secondary_types or ()) == 0
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from lyra_worker.adapters.fakes import FakeMbBrowser, FakeSimilaritySource
|
||||||
|
from lyra_worker.browser import ReleaseGroupInfo
|
||||||
|
from lyra_worker.discovery import DiscoveryConfig, run_discovery
|
||||||
|
from lyra_worker.similarity.base import SimilarArtist
|
||||||
|
from tests.conftest import insert_monitored_release, insert_watched_artist
|
||||||
|
|
||||||
|
|
||||||
|
def _album_rows(conn):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute('SELECT "artistMbid", "rgMbid", album, "primaryType" '
|
||||||
|
'FROM "DiscoverySuggestion" WHERE kind = \'album\' ORDER BY album')
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
def test_derives_most_recent_core_album_for_surfaced_artist(conn):
|
||||||
|
insert_watched_artist(conn, mbid="s1", name="Seed")
|
||||||
|
src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "Cand", 0.9)]})
|
||||||
|
browser = FakeMbBrowser(releases={"c1": [
|
||||||
|
ReleaseGroupInfo("rg-old", "Debut", "Album", (), "2001-01-01"),
|
||||||
|
ReleaseGroupInfo("rg-new", "Latest", "Album", (), "2020-01-01"),
|
||||||
|
ReleaseGroupInfo("rg-live", "At The Hall", "Album", ("Live",), "2021-01-01"),
|
||||||
|
]})
|
||||||
|
result = run_discovery(conn, [src], browser, DiscoveryConfig(albums_per_artist=1))
|
||||||
|
assert result.albums == 1
|
||||||
|
assert _album_rows(conn) == [("c1", "rg-new", "Latest", "Album")] # newest core, live excluded
|
||||||
|
|
||||||
|
|
||||||
|
def test_album_skips_release_groups_already_in_library(conn):
|
||||||
|
insert_watched_artist(conn, mbid="s1", name="Seed")
|
||||||
|
insert_monitored_release(conn, artist_mbid="c1", rg_mbid="rg-have", album="Owned")
|
||||||
|
src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "Cand", 0.9)]})
|
||||||
|
browser = FakeMbBrowser(releases={"c1": [
|
||||||
|
ReleaseGroupInfo("rg-have", "Owned", "Album", (), "2019-01-01"),
|
||||||
|
ReleaseGroupInfo("rg-fresh", "Fresh", "Album", (), "2018-01-01"),
|
||||||
|
]})
|
||||||
|
run_discovery(conn, [src], browser, DiscoveryConfig(albums_per_artist=1))
|
||||||
|
assert _album_rows(conn) == [("c1", "rg-fresh", "Fresh", "Album")] # owned rg excluded
|
||||||
|
|
||||||
|
|
||||||
|
def test_albums_per_artist_zero_derives_none(conn):
|
||||||
|
insert_watched_artist(conn, mbid="s1", name="Seed")
|
||||||
|
src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "Cand", 0.9)]})
|
||||||
|
browser = FakeMbBrowser(releases={"c1": [ReleaseGroupInfo("rg1", "A", "Album", (), "2020")]})
|
||||||
|
assert run_discovery(conn, [src], browser, DiscoveryConfig(albums_per_artist=0)).albums == 0
|
||||||
|
assert _album_rows(conn) == []
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from lyra_worker.release_filter import is_core_release
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_studio_types_pass():
|
||||||
|
assert is_core_release("Album", ()) is True
|
||||||
|
assert is_core_release("Single", []) is True
|
||||||
|
assert is_core_release("EP", ()) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_secondary_types_and_non_core_fail():
|
||||||
|
assert is_core_release("Album", ("Live",)) is False
|
||||||
|
assert is_core_release("Album", ("Compilation",)) is False
|
||||||
|
assert is_core_release("Broadcast", ()) is False
|
||||||
|
assert is_core_release("", ()) is False
|
||||||
Reference in New Issue
Block a user