feat(discover): derive newest + most-popular album per artist
_derive_albums now also picks the artist's most-played album (Last.fm, matched into the browsed discography by MBID then normalized title), alongside the newest. Same release -> one row tagged newest,popular. albumReason persisted; run_discovery threads an optional popularity provider. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -143,21 +143,21 @@ def _existing_rg_mbids(conn: psycopg.Connection) -> set[str]:
|
||||
return {r[0] for r in cur.fetchall()}
|
||||
|
||||
|
||||
def _upsert_album(conn: psycopg.Connection, artist: dict, rg) -> int:
|
||||
def _upsert_album(conn: psycopg.Connection, artist: dict, rg, reason: str) -> 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, "
|
||||
'score, "seedCount", sources, "albumReason", status, "dedupeKey", "createdAt", "updatedAt") '
|
||||
"VALUES (gen_random_uuid()::text, 'album', %s, %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() '
|
||||
'"albumReason" = EXCLUDED."albumReason", "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),
|
||||
artist["score"], artist["seed_count"], sorted(artist["sources"]), reason, dedupe),
|
||||
)
|
||||
return cur.rowcount
|
||||
|
||||
@@ -174,8 +174,26 @@ def _album_kind_ok(g, albums_only: bool) -> bool:
|
||||
return is_core_release(g.primary_type, g.secondary_types)
|
||||
|
||||
|
||||
def _norm_title(s: str) -> str:
|
||||
return (s or "").strip().lower()
|
||||
|
||||
|
||||
def _popular_pick(conn, popularity, artist_name: str, core: list) -> str | None:
|
||||
"""The rg_mbid of the artist's most-played qualifying un-owned album (Last.fm), matched
|
||||
into the already-browsed `core` by MBID first then normalized title. None if unavailable."""
|
||||
if popularity is None or not core:
|
||||
return None
|
||||
by_mbid = {g.rg_mbid: g for g in core}
|
||||
by_title = {_norm_title(g.title): g for g in core}
|
||||
for name, mbid, _plays in popularity.top_albums(conn, artist_name):
|
||||
g = (by_mbid.get(mbid) if mbid else None) or by_title.get(_norm_title(name))
|
||||
if g is not None:
|
||||
return g.rg_mbid
|
||||
return None
|
||||
|
||||
|
||||
def _derive_albums(conn: psycopg.Connection, browser: MbBrowser, surfaced: list[dict],
|
||||
cfg: DiscoveryConfig) -> int:
|
||||
cfg: DiscoveryConfig, popularity=None) -> int:
|
||||
if cfg.albums_per_artist <= 0:
|
||||
return 0
|
||||
have = _existing_rg_mbids(conn)
|
||||
@@ -189,9 +207,21 @@ def _derive_albums(conn: psycopg.Connection, browser: MbBrowser, surfaced: list[
|
||||
core = [g for g in groups
|
||||
if _album_kind_ok(g, cfg.albums_only) and g.rg_mbid not in have]
|
||||
core.sort(key=lambda g: g.first_release_date or "", reverse=True)
|
||||
|
||||
# newest picks (existing behavior) + the single most-played album (Last.fm); merged so
|
||||
# an album that is both newest AND most-played is one row tagged "newest,popular".
|
||||
reasons: dict[str, str] = {}
|
||||
for rg in core[: cfg.albums_per_artist]:
|
||||
albums += _upsert_album(conn, artist, rg)
|
||||
have.add(rg.rg_mbid)
|
||||
reasons[rg.rg_mbid] = "newest"
|
||||
popular = _popular_pick(conn, popularity, artist["name"], core)
|
||||
if popular is not None:
|
||||
reasons[popular] = "newest,popular" if reasons.get(popular) == "newest" else "popular"
|
||||
|
||||
for rg in core: # preserve newest-first order among the chosen
|
||||
reason = reasons.get(rg.rg_mbid)
|
||||
if reason is not None:
|
||||
albums += _upsert_album(conn, artist, rg, reason)
|
||||
have.add(rg.rg_mbid)
|
||||
return albums
|
||||
|
||||
|
||||
@@ -276,7 +306,7 @@ def _recompute_suggestions(conn: psycopg.Connection, affected: set[str],
|
||||
|
||||
|
||||
def run_discovery(conn: psycopg.Connection, sources: list[SimilaritySource],
|
||||
browser: MbBrowser, cfg: DiscoveryConfig) -> DiscoveryResult:
|
||||
browser: MbBrowser, cfg: DiscoveryConfig, popularity=None) -> DiscoveryResult:
|
||||
"""Record each seed's similar-artist contributions, then recompute affected
|
||||
suggestions from the summed contributions — so a candidate's score is the sum of
|
||||
every seed's latest contribution, independent of chunk and sweep boundaries."""
|
||||
@@ -304,7 +334,7 @@ def run_discovery(conn: psycopg.Connection, sources: list[SimilaritySource],
|
||||
affected |= _record_contributions(conn, seed_mbid, live, known, cfg)
|
||||
|
||||
surfaced = _recompute_suggestions(conn, affected, cfg)
|
||||
albums = _derive_albums(conn, browser, surfaced, cfg)
|
||||
albums = _derive_albums(conn, browser, surfaced, cfg, popularity)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
for seed_mbid, _seed_name, source in seeds:
|
||||
|
||||
Reference in New Issue
Block a user