feat(discovery): DiscoverySuggestion schema + WatchedArtist.lastDiscoveredAt
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "DiscoveryKind" AS ENUM ('artist', 'album');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "SuggestionStatus" AS ENUM ('pending', 'followed', 'wanted', 'dismissed');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "WatchedArtist" ADD COLUMN "lastDiscoveredAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DiscoverySuggestion" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"kind" "DiscoveryKind" NOT NULL,
|
||||||
|
"artistMbid" TEXT NOT NULL,
|
||||||
|
"artistName" TEXT NOT NULL,
|
||||||
|
"rgMbid" TEXT,
|
||||||
|
"album" TEXT,
|
||||||
|
"primaryType" TEXT,
|
||||||
|
"secondaryTypes" TEXT[],
|
||||||
|
"firstReleaseDate" TEXT,
|
||||||
|
"score" DOUBLE PRECISION NOT NULL,
|
||||||
|
"seedCount" INTEGER NOT NULL,
|
||||||
|
"sources" TEXT[],
|
||||||
|
"status" "SuggestionStatus" NOT NULL DEFAULT 'pending',
|
||||||
|
"dedupeKey" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "DiscoverySuggestion_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DiscoverySuggestion_dedupeKey_key" ON "DiscoverySuggestion"("dedupeKey");
|
||||||
@@ -100,6 +100,7 @@ model WatchedArtist {
|
|||||||
showAllTypes Boolean @default(false)
|
showAllTypes Boolean @default(false)
|
||||||
monitorFrom DateTime @default(now())
|
monitorFrom DateTime @default(now())
|
||||||
lastPolledAt DateTime?
|
lastPolledAt DateTime?
|
||||||
|
lastDiscoveredAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
releases MonitoredRelease[]
|
releases MonitoredRelease[]
|
||||||
}
|
}
|
||||||
@@ -123,3 +124,34 @@ model MonitoredRelease {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
requests Request[]
|
requests Request[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DiscoveryKind {
|
||||||
|
artist
|
||||||
|
album
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SuggestionStatus {
|
||||||
|
pending
|
||||||
|
followed
|
||||||
|
wanted
|
||||||
|
dismissed
|
||||||
|
}
|
||||||
|
|
||||||
|
model DiscoverySuggestion {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
kind DiscoveryKind
|
||||||
|
artistMbid String
|
||||||
|
artistName String
|
||||||
|
rgMbid String?
|
||||||
|
album String?
|
||||||
|
primaryType String?
|
||||||
|
secondaryTypes String[]
|
||||||
|
firstReleaseDate String?
|
||||||
|
score Float
|
||||||
|
seedCount Int
|
||||||
|
sources String[]
|
||||||
|
status SuggestionStatus @default(pending)
|
||||||
|
dedupeKey String @unique
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ beforeEach(async () => {
|
|||||||
await prisma.libraryItem.deleteMany();
|
await prisma.libraryItem.deleteMany();
|
||||||
await prisma.monitoredRelease.deleteMany();
|
await prisma.monitoredRelease.deleteMany();
|
||||||
await prisma.watchedArtist.deleteMany();
|
await prisma.watchedArtist.deleteMany();
|
||||||
|
await prisma.discoverySuggestion.deleteMany();
|
||||||
await prisma.config.deleteMany();
|
await prisma.config.deleteMany();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,9 +27,14 @@ def conn():
|
|||||||
cur.execute('DELETE FROM "LibraryItem"')
|
cur.execute('DELETE FROM "LibraryItem"')
|
||||||
cur.execute('DELETE FROM "MonitoredRelease"')
|
cur.execute('DELETE FROM "MonitoredRelease"')
|
||||||
cur.execute('DELETE FROM "WatchedArtist"')
|
cur.execute('DELETE FROM "WatchedArtist"')
|
||||||
|
cur.execute('DELETE FROM "DiscoverySuggestion"')
|
||||||
cur.execute('DELETE FROM "Config"')
|
cur.execute('DELETE FROM "Config"')
|
||||||
connection.commit()
|
connection.commit()
|
||||||
yield connection
|
yield connection
|
||||||
|
# A test may have left the transaction aborted (e.g. asserting on a
|
||||||
|
# UniqueViolation via pytest.raises without rolling back); reset it so
|
||||||
|
# the cleanup below can run.
|
||||||
|
connection.rollback()
|
||||||
# Clean up rows created during the test (Job cascades from Request).
|
# Clean up rows created during the test (Job cascades from Request).
|
||||||
with connection.cursor() as cur:
|
with connection.cursor() as cur:
|
||||||
cur.execute('DELETE FROM "Job"')
|
cur.execute('DELETE FROM "Job"')
|
||||||
@@ -37,6 +42,7 @@ def conn():
|
|||||||
cur.execute('DELETE FROM "LibraryItem"')
|
cur.execute('DELETE FROM "LibraryItem"')
|
||||||
cur.execute('DELETE FROM "MonitoredRelease"')
|
cur.execute('DELETE FROM "MonitoredRelease"')
|
||||||
cur.execute('DELETE FROM "WatchedArtist"')
|
cur.execute('DELETE FROM "WatchedArtist"')
|
||||||
|
cur.execute('DELETE FROM "DiscoverySuggestion"')
|
||||||
cur.execute('DELETE FROM "Config"')
|
cur.execute('DELETE FROM "Config"')
|
||||||
connection.commit()
|
connection.commit()
|
||||||
connection.close()
|
connection.close()
|
||||||
@@ -62,14 +68,15 @@ def insert_request(conn, artist="Artist", album="Album"):
|
|||||||
|
|
||||||
|
|
||||||
def insert_watched_artist(conn, mbid="mbid", name="Artist", auto_monitor_future=False,
|
def insert_watched_artist(conn, mbid="mbid", name="Artist", auto_monitor_future=False,
|
||||||
monitor_from_sql="now()", last_polled_sql="NULL"):
|
monitor_from_sql="now()", last_polled_sql="NULL",
|
||||||
"""Insert a WatchedArtist; monitor_from_sql / last_polled_sql are raw SQL time expressions."""
|
last_discovered_sql="NULL"):
|
||||||
|
"""Insert a WatchedArtist; *_sql args are raw SQL time expressions."""
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
'INSERT INTO "WatchedArtist" (id, mbid, name, "autoMonitorFuture", '
|
'INSERT INTO "WatchedArtist" (id, mbid, name, "autoMonitorFuture", '
|
||||||
f'"monitorFrom", "lastPolledAt", "createdAt") '
|
f'"monitorFrom", "lastPolledAt", "lastDiscoveredAt", "createdAt") '
|
||||||
f"VALUES (gen_random_uuid()::text, %s, %s, %s, {monitor_from_sql}, {last_polled_sql}, now()) "
|
f"VALUES (gen_random_uuid()::text, %s, %s, %s, {monitor_from_sql}, "
|
||||||
"RETURNING id",
|
f"{last_polled_sql}, {last_discovered_sql}, now()) RETURNING id",
|
||||||
(mbid, name, auto_monitor_future),
|
(mbid, name, auto_monitor_future),
|
||||||
)
|
)
|
||||||
artist_id = cur.fetchone()[0]
|
artist_id = cur.fetchone()[0]
|
||||||
@@ -97,3 +104,23 @@ def insert_monitored_release(conn, watched_artist_id=None, artist_mbid="mbid", a
|
|||||||
rel_id = cur.fetchone()[0]
|
rel_id = cur.fetchone()[0]
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return rel_id
|
return rel_id
|
||||||
|
|
||||||
|
|
||||||
|
def insert_discovery_suggestion(conn, kind="artist", artist_mbid="cand", artist_name="Cand",
|
||||||
|
rg_mbid=None, album=None, score=1.0, seed_count=1,
|
||||||
|
sources=None, status="pending"):
|
||||||
|
"""Insert a DiscoverySuggestion; returns its id."""
|
||||||
|
dedupe = f"{kind}:{artist_mbid}:{rg_mbid or ''}"
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
'INSERT INTO "DiscoverySuggestion" (id, kind, "artistMbid", "artistName", '
|
||||||
|
'"rgMbid", album, score, "seedCount", sources, status, "dedupeKey", '
|
||||||
|
'"createdAt", "updatedAt") '
|
||||||
|
'VALUES (gen_random_uuid()::text, %s::"DiscoveryKind", %s, %s, %s, %s, %s, %s, %s, '
|
||||||
|
'%s::"SuggestionStatus", %s, now(), now()) RETURNING id',
|
||||||
|
(kind, artist_mbid, artist_name, rg_mbid, album, score, seed_count,
|
||||||
|
sources if sources is not None else ["listenbrainz"], status, dedupe),
|
||||||
|
)
|
||||||
|
sid = cur.fetchone()[0]
|
||||||
|
conn.commit()
|
||||||
|
return sid
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import psycopg
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.conftest import insert_discovery_suggestion, insert_watched_artist
|
||||||
|
|
||||||
|
|
||||||
|
def test_suggestion_round_trip(conn):
|
||||||
|
sid = insert_discovery_suggestion(
|
||||||
|
conn, kind="album", artist_mbid="a1", artist_name="Boygenius",
|
||||||
|
rg_mbid="rg1", album="the record", score=2.5, seed_count=2,
|
||||||
|
sources=["listenbrainz"], status="pending",
|
||||||
|
)
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
'SELECT kind::text, "artistName", "rgMbid", album, score, "seedCount", '
|
||||||
|
'sources, status::text FROM "DiscoverySuggestion" WHERE id = %s', (sid,))
|
||||||
|
assert cur.fetchone() == (
|
||||||
|
"album", "Boygenius", "rg1", "the record", 2.5, 2, ["listenbrainz"], "pending")
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedupe_key_is_unique(conn):
|
||||||
|
insert_discovery_suggestion(conn, kind="artist", artist_mbid="dup", rg_mbid=None)
|
||||||
|
with pytest.raises(psycopg.errors.UniqueViolation):
|
||||||
|
insert_discovery_suggestion(conn, kind="artist", artist_mbid="dup", rg_mbid=None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_watched_artist_has_last_discovered_column(conn):
|
||||||
|
insert_watched_artist(conn, mbid="a1", last_discovered_sql="now()")
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute('SELECT "lastDiscoveredAt" IS NOT NULL FROM "WatchedArtist" WHERE mbid = %s', ("a1",))
|
||||||
|
assert cur.fetchone()[0] is True
|
||||||
Reference in New Issue
Block a user