diff --git a/discover-rows.png b/discover-rows.png new file mode 100644 index 0000000..4322319 Binary files /dev/null and b/discover-rows.png differ diff --git a/web/src/app/discover/discover-client.tsx b/web/src/app/discover/discover-client.tsx index 46012ba..2a97086 100644 --- a/web/src/app/discover/discover-client.tsx +++ b/web/src/app/discover/discover-client.tsx @@ -36,6 +36,12 @@ function similarTo(seeds: string[]): string | null { return `Similar to ${shown}${extra > 0 ? ` +${extra} more` : ""}`; } +/** Full-length albums only — hide singles/EPs and secondary-type releases (live/comp) that + * pre-date the albums-only derivation and still linger in the DB. */ +function isFullAlbum(a: Album): boolean { + return (a.primaryType ?? "Album") === "Album" && (a.secondaryTypes?.length ?? 0) === 0; +} + /** Human badge for why an album is surfaced. */ function badgeFor(reason: string | null): string | null { if (reason === "newest") return "Newest"; @@ -214,7 +220,9 @@ export function DiscoverClient() {

No suggestions yet — run Discover, or follow a few artists to seed it.

) : ( )} diff --git a/worker/lyra_worker/discovery.py b/worker/lyra_worker/discovery.py index 3f1f10d..901c52f 100644 --- a/worker/lyra_worker/discovery.py +++ b/worker/lyra_worker/discovery.py @@ -217,6 +217,16 @@ def _derive_albums(conn: psycopg.Connection, browser: MbBrowser, surfaced: list[ if popular is not None: reasons[popular] = "newest,popular" if reasons.get(popular) == "newest" else "popular" + # Re-deriving replaces this artist's album set: drop any pending album suggestion of + # theirs that is no longer a pick (stale singles/EPs from older sweeps, or an album now + # owned), keeping wanted/dismissed rows untouched. Then upsert the current picks. + with conn.cursor() as cur: + cur.execute( + "DELETE FROM \"DiscoverySuggestion\" WHERE kind = 'album' AND status = 'pending' " + 'AND "artistMbid" = %s AND NOT ("rgMbid" = ANY(%s))', + (artist["mbid"], list(reasons.keys())), + ) + for rg in core: # preserve newest-first order among the chosen reason = reasons.get(rg.rg_mbid) if reason is not None: diff --git a/worker/tests/test_discovery_albums.py b/worker/tests/test_discovery_albums.py index 8d3f3e8..7e5dddb 100644 --- a/worker/tests/test_discovery_albums.py +++ b/worker/tests/test_discovery_albums.py @@ -2,7 +2,7 @@ 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 +from tests.conftest import insert_discovery_suggestion, insert_monitored_release, insert_watched_artist def _album_rows(conn): @@ -92,6 +92,34 @@ def test_popular_matches_by_title_when_lastfm_mbid_missing_and_skips_owned(conn) assert _reasons(conn) == {"rg-new": "newest", "rg-match": "popular"} +def test_rederiving_replaces_stale_pending_album_suggestions(conn): + insert_watched_artist(conn, mbid="s1", name="Seed") + # a stale pending album suggestion for c1 that is no longer a pick (e.g. an old single) + insert_discovery_suggestion(conn, kind="album", artist_mbid="c1", rg_mbid="rg-old-single", + album="Old Single", status="pending") + src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "Cand", 0.9)]}) + browser = FakeMbBrowser(releases={"c1": [ + ReleaseGroupInfo("rg-new", "New Album", "Album", (), "2022-01-01"), + ]}) + run_discovery(conn, [src], browser, DiscoveryConfig(albums_per_artist=1)) + # the stale single is gone; only the freshly-derived album remains + assert [r[1] for r in _album_rows(conn)] == ["rg-new"] + + +def test_rederiving_keeps_wanted_album_suggestions(conn): + insert_watched_artist(conn, mbid="s1", name="Seed") + insert_discovery_suggestion(conn, kind="album", artist_mbid="c1", rg_mbid="rg-wanted", + album="Wanted One", status="wanted") + src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "Cand", 0.9)]}) + browser = FakeMbBrowser(releases={"c1": [ + ReleaseGroupInfo("rg-new", "New Album", "Album", (), "2022-01-01"), + ]}) + run_discovery(conn, [src], browser, DiscoveryConfig(albums_per_artist=1)) + with conn.cursor() as cur: # wanted row untouched (only pending stale rows are replaced) + cur.execute('SELECT count(*) FROM "DiscoverySuggestion" WHERE "rgMbid" = %s AND status = \'wanted\'', ("rg-wanted",)) + assert cur.fetchone()[0] == 1 + + def test_no_popularity_provider_yields_newest_only(conn): insert_watched_artist(conn, mbid="s1", name="Seed") src = FakeSimilaritySource(similar={"s1": [SimilarArtist("c1", "Cand", 0.9)]})