Commit Graph

79 Commits

Author SHA1 Message Date
bvandeusen ff5868bdab docs(db/m7-coverage-gauge): note CHECK-constraint sync + bucket invariant
Code-review polish on GetAlbumCoverageRollup's comment block:
- Records the invariant that with_art + pending + settled = total
  (and that pending_no_mbid is a subset of pending, not a fourth
  bucket).
- Notes that the IN ('sidecar','embedded','mbcaa') list must stay
  in sync with the cover_art_source CHECK constraint in migration
  0016 — without this note a future source addition could silently
  undercount with_art.
2026-05-06 09:52:10 -04:00
bvandeusen 98ec64c658 feat(db/m7-coverage-gauge): GetAlbumCoverageRollup query
FILTER-aggregate count over albums.cover_art_source and albums.mbid
returning total, with_art, pending, settled, pending_no_mbid in a
single round-trip. Sub-millisecond on realistic libraries; no index
needed for v1. pending_no_mbid is a subset of pending — the UI
tooltip uses it to show how many "pending" rows are blocked on
missing MBID and won't be moved by another scan.

Also bumps the Makefile sqlc pin from 1.27.0 to 1.31.1 to match the
version that produced all existing committed dbq/*.go files. The
prior pin was stale; running make generate against 1.27.0 silently
rolled the codegen back (cosmetic version headers on most files
plus a real codegen shape change in DeleteArtistIfEmpty).
2026-05-06 09:40:26 -04:00
bvandeusen 1ed6ca0309 feat(db/m7-381): sqlc queries for scan_runs 2026-05-04 20:04:55 -04:00
bvandeusen 031f4bef8b feat(db/m7-381): scan_runs table for admin scan-status view 2026-05-04 20:04:04 -04:00
bvandeusen 34615fffbd feat(server/m7-380): one-shot MBID backfill worker for existing libraries
Adds a boot-time goroutine that walks albums with NULL mbid, re-reads
tags from one track per album via dhowden/tag, and persists album + artist
MBIDs. Healed albums also get their cover_art_source='none' cleared so the
enricher's next batch retries the MBCAA fetch. Caps at 5000 albums per
boot to avoid stalling startup on huge libraries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 19:40:27 -04:00
bvandeusen 6c6c4bc1e4 feat(server/m7-379): heal existing artist+album mbid on rescan 2026-05-04 19:03:12 -04:00
bvandeusen 15170ed7c6 feat(db/m7-353): sqlc queries for album cover enrichment 2026-05-04 14:30:55 -04:00
bvandeusen 458a183099 feat(db/m7-353): add albums.cover_art_source enum column 2026-05-04 14:29:13 -04:00
bvandeusen 2b286a9bfd docs(db/m7-352): note ListPlaylistsByUserAndKind is test-only
Production list-playlists handler reuses Service.List + in-memory kind
filter so other users' public playlists can surface alongside the
caller's filtered own. Comment clarifies the divergence so future
readers don't expect this query to be wired into the API path.
2026-05-04 10:31:09 -04:00
bvandeusen 46a9de8e9a feat(server/m7-352): BuildSystemPlaylists with atomic replace + concurrency guard
Implements T5 of #352 slice 2: adds CreateSystemPlaylist sqlc query,
BuildSystemPlaylists function (For-You + Songs-like mixes, tx atomic
replace, in_flight guard, tieBreakHash determinism), and 7 integration
tests covering activity, quarantine exclusion, atomic replace, concurrency,
daily nonce stability, ListActiveUsers, and stale-in-flight recovery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 08:35:02 -04:00
bvandeusen 58a27e2f0f fix(db/m7-352): cast COALESCE to uuid so sqlc emits typed return 2026-05-04 08:15:21 -04:00
bvandeusen 3fe929cebd feat(db/m7-352): sqlc queries for system playlist runs + seed selection
Adds internal/db/queries/system_playlists.sql with 12 named queries
covering active-user enumeration, per-user run tracking (claim/finish/fail),
seed-artist/track/cover selection, and playlist CRUD by kind.
Runs sqlc generate to emit dbq/system_playlists.sql.go; also updates
playlists.sql.go and models.go to reflect the new kind/system_variant/
seed_artist_id columns added in migration 0015.

Note: plan specified a.cover_path for PickTopAlbumCoverForArtistByUser
but albums uses cover_art_path — corrected in the query file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 08:14:20 -04:00
bvandeusen 36d05310ce feat(db/m7-352): system playlists schema (kind, variant, seed_artist_id, runs table) 2026-05-04 08:03:06 -04:00
bvandeusen 45a17e4e95 feat(server/m7-365): sqlc query for user listening history 2026-05-04 05:46:30 -04:00
bvandeusen 1226cb7583 feat(db): playlists schema for M7 #352 slice 1
Migration 0014 adds playlists + playlist_tracks. track_id is nullable
with ON DELETE SET NULL — tracks can be removed from the library
without silently dropping playlist entries; the denormalized snapshot
(title/artist/album/duration) keeps the row legible afterwards. UI
renders such rows greyed-out.

Indexes: playlists by (user_id, updated_at DESC) and a partial public
index for cross-user discovery; playlist_tracks partial index on
track_id to support the FK SET NULL lookup.

Queries provide CRUD + rollup recompute (track_count, duration_sec)
+ append/remove primitives. Reorder is service-layer orchestrated via
raw tx.Exec; no SQL primitive needed.
2026-05-03 10:03:36 -04:00
bvandeusen 30b0edf97b feat(db): cascade-cleanup queries for M7 #372 track removal
DeleteTrack returns album_id + artist_id so the calling service can
chain the album-empty / artist-empty cascade. DeleteAlbumIfEmpty and
DeleteArtistIfEmpty are no-ops when other rows still reference the
parent — the service treats pgx.ErrNoRows as "not orphaned, skip".
2026-05-02 21:53:33 -04:00
bvandeusen f77245bc41 feat(requests): show ingest progress while requests are in flight
After Lidarr accepts a request the local reconciler imports albums and
tracks as they arrive — but until the request hit 'completed' the
operator had no way to see "is anything happening?" The /requests and
/admin/requests rows now surface a live progress line whenever the
request's matched entity has children in our library.

Backend:
- New CountAlbumsByArtist + CountTracksByArtist sqlc queries.
- requestView gains imported_album_count and imported_track_count.
- New fillProgress helper computes them from the matched entity:
  - kind=artist  → counts albums + tracks under matched_artist_id
  - kind=album   → counts tracks under matched_album_id
  - kind=track   → 1 once matched_track_id is set
  N+1 in the list endpoints; acceptable at admin scale.
- handleListRequests, handleGetRequest, and handleListAdminRequests
  populate the new fields on every response.

Frontend:
- LidarrRequest TS type extended with the two counters.
- Both the operator's /requests page and the admin /admin/requests
  page render an accent-colored line under the row meta when at
  least one counter is non-zero, e.g.:
    Artist:  "5 albums · 47 tracks ingested"
    Album:   "12 tracks ingested"
    Track:   "Track ingested"
- Updated test fixtures to include the new required fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:04:38 -04:00
bvandeusen c265b871c3 feat(admin): metadata-profile picker on integrations page
Persists the operator's metadata-profile choice alongside quality
profile + root folder. Defaults to the first profile Lidarr returns
(usually 'Standard' on a vanilla install) so the common case is
zero-click; operators with custom profiles like 'Singles only' can pick
explicitly.

Backend:
- Migration 0013: adds nullable default_metadata_profile_id to
  lidarr_config. Existing rows get NULL and the service falls back to
  fetch-and-pick-first until they save.
- Updated lidarr_config queries + sqlc + lidarrconfig.Config + admin
  view/put body to round-trip the new field.
- handlePutLidarrConfig requires it (along with QP and root folder)
  when enabled=true — matches the existing missing_defaults gate.
- New GET /api/admin/lidarr/metadata-profiles handler + lidarr.Client
  ListMetadataProfiles (GET /api/v1/metadataprofile, same shape as
  the quality-profile endpoint).
- lidarrrequests.Approve prefers cfg.DefaultMetadataProfileID; falls
  back to the fetch-list path only when 0 (back-compat for upgraders).

Frontend:
- LidarrConfig type + LidarrMetadataProfile type + qk.lidarrMetadataProfiles.
- listMetadataProfiles + createMetadataProfilesQuery client helpers.
- Integrations page: third <select> picker, auto-defaults to first
  profile when the saved value is 0, sends the new field on save and
  clears it on disconnect.
- Updated test fixtures + the duplicate 'Standard' option string in
  the dropdown-populates assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:09:13 -04:00
bvandeusen b31723c5e2 feat(db): add M6a library-list + artist-tracks queries
Appends ListArtistsAlphaWithCovers, ListAlbumsAlphaWithArtist,
CountAlbums, and ListArtistTracksForUser queries; regenerates sqlc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:03:10 -04:00
bvandeusen 5ed3c20b74 feat(db): add M6a rediscover queries (albums + artists, with fallbacks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 09:00:20 -04:00
bvandeusen 10c84a536e feat(db): add M6a home-section queries (recently added, most played, last played)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 08:56:53 -04:00
bvandeusen 277898a49a feat(recommendation): SuggestArtists service for M5c
Add per-user artist-suggestion service ranking out-of-library MBIDs by
signal x similarity. Single-CTE SQL collects user likes (5x weight) and
recency-decayed plays, joins against artist_similarity_unmatched, and
filters in-library candidates plus non-terminal lidarr_requests. The
service resolves top-3 attribution seeds to artist names in a batched
GetArtistsByIDs call so the UI can render "because you liked X" reasons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 06:20:02 -04:00
bvandeusen 2ca09749d9 feat(db): add artist_similarity_unmatched schema (migration 0012) 2026-05-01 05:50:22 -04:00
bvandeusen 559fb5dd2c feat(db): add user-context track query variants honoring quarantine 2026-04-30 19:49:00 -04:00
bvandeusen 71a9bd8dee fix(db): drop redundant quarantine user index + tighten ListQuarantineForUser
- Composite PK (user_id, track_id) already serves WHERE user_id queries;
  the secondary (user_id, created_at DESC) index just amplified writes.
- ListQuarantineForUser now selects only the joined fields the SPA card
  actually renders (~10 columns) rather than embedding three full structs
  (~35 columns); halves wire/DB bandwidth before consumers exist.
- max(q.created_at)::timestamptz cast emits pgtype.Timestamptz instead of
  interface{} so handlers can read latest_at without a type-assert.
2026-04-30 16:54:03 -04:00
bvandeusen a8df73fe42 feat(db): add lidarr_quarantine + actions schema (migration 0011) 2026-04-30 16:48:33 -04:00
bvandeusen a5bb79f2a8 fix(db): clarify lidarr_requests query semantics
- HasNonTerminalRequestForMBID: use named parameter @mbid so the
  generated Go signature is `mbid string` instead of the misleading
  `lidarrArtistMbid string` (the value applies to all three MBID
  columns, not just the artist column)
- CancelLidarrRequest: comment dual role of $2 (ownership guard +
  decided_by audit field) for future readers
- CompleteLidarrRequest: document the per-kind one-of-three contract
  for matched_*_id parameters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 15:19:54 -04:00
bvandeusen 9ceac5c639 feat(db): add lidarr_config + lidarr_requests schema (migration 0010)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 15:10:01 -04:00
bvandeusen b8e3019654 fix(db): split seed_info CTE so similar-artist works without seed genre
The combined seed_info CTE filtered the seed out when its genre was
NULL/empty. Restored the spec's two-CTE design (seed_artist + seed_tags)
so similar-artist matching works regardless of the seed's genre coverage.
Drops the genre workaround from the SimilarArtist test.
2026-04-29 08:43:42 -04:00
bvandeusen 362cb2c9ce feat(db): add LoadRadioCandidatesV2 sqlc query (5-way UNION) 2026-04-29 07:56:57 -04:00
bvandeusen d4e0e5fae3 feat(db): add similarity sqlc queries (list-needing, get-by-mbids, upsert) 2026-04-28 19:58:05 -04:00
bvandeusen fc2d35d33c feat(db): add migration 0009 for similarity (track_similarity + artist_similarity) 2026-04-28 19:57:02 -04:00
bvandeusen 1d1119ac32 feat(api): add GET/PUT /api/me/listenbrainz endpoints
Exposes user ListenBrainz config (token_set bool, enabled, last_scrobbled_at)
via write-only token semantics; enforces no-enable-without-token at the API layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:27:06 -04:00
bvandeusen a0e21f21b6 feat(db): add ListenBrainz user-config and scrobble_queue queries 2026-04-28 08:42:38 -04:00
bvandeusen efc2308348 feat(db): add migration 0008 for scrobble (users LB columns + scrobble_queue table) 2026-04-28 08:40:39 -04:00
bvandeusen cb46b3830f feat(db): add similarity lookup queries (ListActiveContextualLikesForUser, GetCurrentSessionVectorForUser) 2026-04-27 20:36:42 -04:00
bvandeusen d43d8df6d5 feat(db): add session-vector capture queries; LikeTrack returns row count
ListRecentSessionTracks + UpdatePlayEventVector for the vector capture
path inside RecordPlayStarted. LikeTrack switches to :execrows so the
contextual-likes capture can detect insert vs already-exists. Existing
callers updated to ignore the count for now; later tasks consume it.
2026-04-27 11:16:05 -04:00
bvandeusen 92ef53c04d feat(db): add contextual_likes table (M3 session-context capture)
Table was referenced in migration 0005's comment but never created —
this slice fills the gap. Soft-delete via deleted_at column; hot-path
partial index on active rows; GIN index on session_vector for
M3 sub-plan #3's similarity queries.
2026-04-27 11:13:31 -04:00
bvandeusen 3ef560e414 feat(db): add LoadRadioCandidates query
Single SELECT that joins tracks with general_likes (for is_liked) and
an aggregated LATERAL subquery on play_events (for last_played_at,
play_count, skip_count). Excludes seed + tracks played in the last
N hours. Drives the M3 weighted shuffle scoring.
2026-04-27 08:01:50 -04:00
bvandeusen 780beaa248 feat(db): add M2 likes schema (general_likes, _albums, _artists)
Three tables keyed on (user_id, entity_id) with liked_at. Per-table
indexes on (user_id, liked_at DESC) for the recently-liked feed.
sqlc queries cover like/unlike/list-rows/count/list-ids per entity.
2026-04-26 16:20:08 -04:00
bvandeusen 0c7e1b0408 feat(playevents): add Writer with start/end/skipped/synthetic paths
Owns the auto-close-prior step (caps each user at one open row),
spec §6 skip classification rule on play_ended (AND of completion <
0.5 and duration_played < 30s), and skip_events row writes. Synthetic
completed play wires the Subsonic /rest/scrobble?submission=true path
into the same store as native events.
2026-04-26 00:10:02 -04:00
bvandeusen 4fcf2c9616 feat(db): add M2 schema — play_sessions, play_events, skip_events
Tables and indexes per spec §5. session_vector_at_play ships nullable
so M3 doesn't need a follow-up migration. Table is named play_sessions
to avoid collision with the existing sessions (HTTP auth) table from
migration 0004.
2026-04-25 20:45:44 -04:00
bvandeusen fb49a39492 feat(db): add paged + count queries for library reads 2026-04-21 00:01:15 -04:00
bvandeusen 8cdaf8f0f1 feat(auth): RequireUser middleware (cookie or bearer)
Resolves /api/* callers from session cookie first, Authorization
bearer second. Touches last_seen on success for future active-
sessions UI. Adds GetUserByID query used by the middleware.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 20:53:11 -04:00
bvandeusen c8a4a930ea feat(dbq): generate session queries
Adds Insert/Get/Touch/Delete helpers over the sessions table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 20:16:48 -04:00
bvandeusen 1741b57a0b feat(db): add sessions table for /api/* auth
Stores sha256(token) plus user_agent + last_seen_at so future
active-sessions UI doesn't need another migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 19:37:26 -04:00
bvandeusen f8249a12ac sqlc generate for CountTracksByAlbum and SearchTracks (#295) 2026-04-19 19:03:59 +00:00
bvandeusen f3a80347c1 sqlc generate for SearchArtists (#295) 2026-04-19 19:03:30 +00:00
bvandeusen 319ef06a4f sqlc generate for album browse queries (#295) 2026-04-19 19:03:10 +00:00
bvandeusen acd2c7f034 sqlc: add CountTracksByAlbum and SearchTracks for Subsonic browse (#295)
CountTracksByAlbum drives the songCount attr on AlbumRef; SearchTracks
backs the song facet of search3.
2026-04-19 19:02:27 +00:00