From e04c1c9795ef04438186a2cf104ba2211355166c Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 14 Jul 2026 22:39:52 +0200 Subject: [PATCH] feat(discover): also seed discovery from owned-album artists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../migration.sql | 7 ++ web/prisma/schema.prisma | 8 ++ web/src/app/api/discover/config/route.ts | 1 + web/src/app/settings/settings-form.tsx | 10 +++ web/src/test/setup.ts | 1 + worker/lyra_worker/discovery.py | 79 ++++++++++++++----- worker/tests/conftest.py | 2 + worker/tests/test_discovery.py | 42 ++++++++++ 8 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 web/prisma/migrations/20260714203518_add_discovery_seed/migration.sql diff --git a/web/prisma/migrations/20260714203518_add_discovery_seed/migration.sql b/web/prisma/migrations/20260714203518_add_discovery_seed/migration.sql new file mode 100644 index 0000000..869551d --- /dev/null +++ b/web/prisma/migrations/20260714203518_add_discovery_seed/migration.sql @@ -0,0 +1,7 @@ +-- CreateTable +CREATE TABLE "DiscoverySeed" ( + "mbid" TEXT NOT NULL, + "lastDiscoveredAt" TIMESTAMP(3), + + CONSTRAINT "DiscoverySeed_pkey" PRIMARY KEY ("mbid") +); diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index ee1ef0c..37dea2d 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -191,6 +191,14 @@ model DiscoverySeedContribution { @@index([seedMbid]) } +// Throttle registry for discovery seeds that are NOT WatchedArtist rows — i.e. artists you own +// albums by (LibraryItem.artistMbid) but don't follow. Mirrors WatchedArtist.lastDiscoveredAt so +// each library-derived seed is swept once per interval, not every sweep. +model DiscoverySeed { + mbid String @id + lastDiscoveredAt DateTime? +} + model ScanWorkItem { id String @id @default(cuid()) scanId String diff --git a/web/src/app/api/discover/config/route.ts b/web/src/app/api/discover/config/route.ts index 81d3403..4c2b235 100644 --- a/web/src/app/api/discover/config/route.ts +++ b/web/src/app/api/discover/config/route.ts @@ -6,6 +6,7 @@ const DEFAULTS: Record = { "discover.similarPerSeed": "20", "discover.albumsPerArtist": "1", "discover.albumsOnly": "true", + "discover.seedFromLibrary": "true", "discover.minScore": "0", "discover.chunkSize": "5", "discover.listenBrainzUrl": "", diff --git a/web/src/app/settings/settings-form.tsx b/web/src/app/settings/settings-form.tsx index 1922e19..8f48202 100644 --- a/web/src/app/settings/settings-form.tsx +++ b/web/src/app/settings/settings-form.tsx @@ -395,6 +395,8 @@ export function SettingsForm() { Similar per seed — candidates pulled per artist. Albums per artist — releases surfaced per suggested artist. Min score — drops weak suggestions.{" "} Albums only — suggest full-length albums only, hiding singles and EPs.{" "} + Seed from library — also find artists similar to ones you own albums by, not + just the artists you follow.{" "} ListenBrainz base URL — leave blank for the default endpoint. @@ -414,6 +416,14 @@ export function SettingsForm() { /> Albums only (hide singles & EPs) +