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:
@@ -0,0 +1,7 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "DiscoverySeed" (
|
||||
"mbid" TEXT NOT NULL,
|
||||
"lastDiscoveredAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "DiscoverySeed_pkey" PRIMARY KEY ("mbid")
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ const DEFAULTS: Record<string, string> = {
|
||||
"discover.similarPerSeed": "20",
|
||||
"discover.albumsPerArtist": "1",
|
||||
"discover.albumsOnly": "true",
|
||||
"discover.seedFromLibrary": "true",
|
||||
"discover.minScore": "0",
|
||||
"discover.chunkSize": "5",
|
||||
"discover.listenBrainzUrl": "",
|
||||
|
||||
@@ -395,6 +395,8 @@ export function SettingsForm() {
|
||||
<b> Similar per seed</b> — candidates pulled per artist. <b>Albums per artist</b> —
|
||||
releases surfaced per suggested artist. <b>Min score</b> — drops weak suggestions.{" "}
|
||||
<b>Albums only</b> — suggest full-length albums only, hiding singles and EPs.{" "}
|
||||
<b>Seed from library</b> — also find artists similar to ones you own albums by, not
|
||||
just the artists you follow.{" "}
|
||||
<b>ListenBrainz base URL</b> — leave blank for the default endpoint.
|
||||
</HelpButton>
|
||||
</div>
|
||||
@@ -414,6 +416,14 @@ export function SettingsForm() {
|
||||
/>
|
||||
Albums only (hide singles & EPs)
|
||||
</label>
|
||||
<label className="toggle" style={{ margin: "6px 0 0" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={discoverCfg["discover.seedFromLibrary"] !== "false"}
|
||||
onChange={(e) => setCfg("discover.seedFromLibrary", e.target.checked ? "true" : "false")}
|
||||
/>
|
||||
Seed from library (similar to albums you own)
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>ListenBrainz base URL (blank = default)</span>
|
||||
<input value={discoverCfg["discover.listenBrainzUrl"] ?? ""}
|
||||
|
||||
@@ -17,6 +17,7 @@ beforeEach(async () => {
|
||||
await prisma.watchedArtist.deleteMany();
|
||||
await prisma.discoverySuggestion.deleteMany();
|
||||
await prisma.discoverySeedContribution.deleteMany();
|
||||
await prisma.discoverySeed.deleteMany();
|
||||
await prisma.config.deleteMany();
|
||||
await prisma.apiCache.deleteMany(); // read-through cache must not leak across tests
|
||||
await prisma.unmatchedAlbum.deleteMany();
|
||||
|
||||
Reference in New Issue
Block a user