feat(discover): also seed discovery from owned-album artists
Discovery seeded only from followed artists. Now it also seeds from artists you own albums by (LibraryItem.artistMbid, captured since the MBID own-state work) but don't follow — so your library shapes suggestions, not just your follows. - New DiscoverySeed throttle table (migration add_discovery_seed) mirroring WatchedArtist.lastDiscoveredAt, so each library-derived seed is swept once per interval (WatchedArtist seeds keep their own throttle). - _seed_artists merges both seed sources least-recently-swept first, capped at chunk_size; run_discovery stamps the right throttle per seed and evicts contributions from seeds that are neither followed nor owned. - Artists you already have (followed OR owned) are excluded from the suggestion candidates — discovery surfaces only genuinely new artists. - discover.seedFromLibrary config (default true) + a Discovery settings toggle. NOTE: existing LibraryItem rows have null artistMbid (populated on new import/scan), so a re-scan is needed before old albums seed discovery. worker 236, web 187 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,48 @@ def _artist_rows(conn):
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def _insert_library_item(conn, artist, artist_mbid, album="Owned Album"):
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
'INSERT INTO "LibraryItem" (id, artist, album, path, source, format, "qualityClass", '
|
||||
'"artistMbid", "importedAt") '
|
||||
"VALUES (gen_random_uuid()::text, %s, %s, %s, 'scan', 'FLAC', 2, %s, now())",
|
||||
(artist, album, f"/music/{artist}/{album}", artist_mbid),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_seeds_discovery_from_owned_album_artists(conn):
|
||||
# An artist you own an album by but don't follow becomes a discovery seed.
|
||||
_insert_library_item(conn, "Owned Artist", "lib-seed")
|
||||
src = FakeSimilaritySource(similar={"lib-seed": [SimilarArtist("c1", "New Find", 0.9)]})
|
||||
result = run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig())
|
||||
assert result.seeds == 1
|
||||
assert [r[0] for r in _artist_rows(conn)] == ["c1"] # surfaced from the library seed
|
||||
with conn.cursor() as cur: # library seed throttled via DiscoverySeed, not WatchedArtist
|
||||
cur.execute('SELECT "lastDiscoveredAt" IS NOT NULL FROM "DiscoverySeed" WHERE mbid = %s', ("lib-seed",))
|
||||
assert cur.fetchone()[0] is True
|
||||
|
||||
|
||||
def test_library_seeding_can_be_disabled(conn):
|
||||
_insert_library_item(conn, "Owned Artist", "lib-seed")
|
||||
src = FakeSimilaritySource(similar={"lib-seed": [SimilarArtist("c1", "New Find", 0.9)]})
|
||||
result = run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig(seed_from_library=False))
|
||||
assert result.seeds == 0 and _artist_rows(conn) == [] # library not seeded
|
||||
|
||||
|
||||
def test_owned_artist_is_not_suggested_as_a_candidate(conn):
|
||||
# You own an album by "c1"; a followed seed finds c1 similar. c1 must not be suggested back.
|
||||
insert_watched_artist(conn, mbid="s1", name="Seed")
|
||||
_insert_library_item(conn, "Owned", "c1")
|
||||
src = FakeSimilaritySource(similar={
|
||||
"s1": [SimilarArtist("c1", "Owned", 0.9), SimilarArtist("c2", "New", 0.8)],
|
||||
"c1": [], # the library seed itself finds nothing new here
|
||||
})
|
||||
run_discovery(conn, [src], FakeMbBrowser(), DiscoveryConfig())
|
||||
assert [r[0] for r in _artist_rows(conn)] == ["c2"] # c1 excluded (already owned)
|
||||
|
||||
|
||||
def test_aggregates_scores_across_seeds(conn):
|
||||
insert_watched_artist(conn, mbid="s1", name="Seed One")
|
||||
insert_watched_artist(conn, mbid="s2", name="Seed Two")
|
||||
|
||||
Reference in New Issue
Block a user